mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-28 16:31:28 -03:00
feat(recipes): improve recipe LoRA status indicators and missing-badge affordance (#1076)
- Recipe card: compact status pill with state icon + available/total fraction (e.g. "2/3"), pinned to the footer bottom-right like model card actions; status is encoded by icon + color, never color alone - Recipe modal: "N missing" is now a real <button> with a persistent border, leading download icon, focus-visible ring and aria-label; clicking opens the download-missing flow - Fix context menu missing-LoRA detection selector after badge refactor - i18n: add recipes.status/loraStatus keys with translations for all 10 locales, and fill pending rate-limit translations
This commit is contained in:
@@ -671,23 +671,32 @@ body.hide-card-version .hl-badge {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Compact LoRA status pill: state icon + available/total fraction (e.g. "2/3").
|
||||
The icon switches by state (warning/check/layers) so status never relies on
|
||||
color alone; the tooltip spells out the full details. */
|
||||
.lora-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
flex-shrink: 0;
|
||||
/* Pin to the bottom-right corner of the footer, matching how model card
|
||||
footer .card-actions behave when the title wraps to multiple lines */
|
||||
align-self: flex-end;
|
||||
font-size: 0.85em;
|
||||
position: relative;
|
||||
padding: 2px 8px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.lora-count.ready {
|
||||
background: rgba(46, 204, 113, 0.3);
|
||||
border-color: rgba(46, 204, 113, 0.6);
|
||||
}
|
||||
|
||||
.lora-count.missing {
|
||||
background: rgba(231, 76, 60, 0.3);
|
||||
background: rgba(231, 76, 60, 0.35);
|
||||
border-color: rgba(231, 76, 60, 0.65);
|
||||
}
|
||||
|
||||
.placeholder-message {
|
||||
|
||||
@@ -1153,47 +1153,26 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Add styles for missing LoRAs download feature */
|
||||
.recipe-status.missing {
|
||||
/* Missing LoRAs status is a real button: the affordance must be visible at
|
||||
rest (persistent border), not only on hover. */
|
||||
.recipe-status.missing.clickable {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
font: inherit;
|
||||
background: oklch(var(--lora-error) / 0.12);
|
||||
color: var(--lora-error);
|
||||
border: 1px solid oklch(var(--lora-error) / 0.45);
|
||||
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.recipe-status.missing:hover {
|
||||
background-color: rgba(var(--lora-warning-rgb, 255, 165, 0), 0.2);
|
||||
.recipe-status.missing.clickable:hover {
|
||||
background: oklch(var(--lora-error) / 0.22);
|
||||
box-shadow: 0 0 0 2px oklch(var(--lora-error) / 0.25);
|
||||
}
|
||||
|
||||
.recipe-status.missing .missing-tooltip {
|
||||
position: absolute;
|
||||
display: none;
|
||||
background-color: var(--card-bg);
|
||||
color: var(--text-color);
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: var(--shadow-header);
|
||||
z-index: var(--z-overlay);
|
||||
width: max-content;
|
||||
max-width: 200px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: normal;
|
||||
margin-left: -100px;
|
||||
margin-top: -65px;
|
||||
}
|
||||
|
||||
.recipe-status.missing:hover .missing-tooltip {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.recipe-status.clickable {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
.recipe-status.clickable:hover {
|
||||
background-color: rgba(var(--lora-warning-rgb, 255, 165, 0), 0.2);
|
||||
.recipe-status.missing.clickable:focus-visible {
|
||||
outline: 2px solid var(--lora-error);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.recipe-checkpoint-meta {
|
||||
|
||||
@@ -37,8 +37,7 @@ export class RecipeContextMenu extends BaseContextMenu {
|
||||
|
||||
if (recipeId && missingLorasItem) {
|
||||
// Check if this card has missing LoRAs
|
||||
const loraCountElement = card.querySelector('.lora-count');
|
||||
const hasMissingLoras = loraCountElement && loraCountElement.classList.contains('missing');
|
||||
const hasMissingLoras = Boolean(card.querySelector('.lora-count.missing'));
|
||||
|
||||
// Show/hide the download missing LoRAs option based on missing status
|
||||
if (hasMissingLoras) {
|
||||
|
||||
@@ -45,6 +45,13 @@ class RecipeCard {
|
||||
const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted).length;
|
||||
const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0;
|
||||
|
||||
// Compact status pill: state icon + available/total fraction.
|
||||
// Icon switches by state so status never relies on color alone.
|
||||
const availableLorasCount = lorasCount - missingLorasCount;
|
||||
const loraCountStateClass = missingLorasCount > 0 ? 'missing' : (allLorasAvailable ? 'ready' : '');
|
||||
const loraCountIcon = missingLorasCount > 0 ? 'fa-exclamation-triangle' : (allLorasAvailable ? 'fa-check' : 'fa-layer-group');
|
||||
const loraCountLabel = lorasCount > 0 ? `${availableLorasCount}/${lorasCount}` : `${lorasCount}`;
|
||||
|
||||
// Ensure file_url exists, fallback to API URL if needed
|
||||
let previewUrl = this.recipe.file_url;
|
||||
if (!previewUrl) {
|
||||
@@ -128,9 +135,8 @@ class RecipeCard {
|
||||
<span class="model-name">${this.recipe.title}</span>
|
||||
</div>
|
||||
${!isDuplicatesMode ? `
|
||||
<div class="lora-count ${allLorasAvailable ? 'ready' : (lorasCount > 0 ? 'missing' : '')}"
|
||||
title="${this.getLoraStatusTitle(lorasCount, missingLorasCount)}">
|
||||
<i class="fas fa-layer-group"></i> ${lorasCount}
|
||||
<div class="lora-count ${loraCountStateClass}" title="${this.getLoraStatusTitle(lorasCount, missingLorasCount)}">
|
||||
<i class="fas ${loraCountIcon}" aria-hidden="true"></i> ${loraCountLabel}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -149,9 +155,17 @@ class RecipeCard {
|
||||
}
|
||||
|
||||
getLoraStatusTitle(totalCount, missingCount) {
|
||||
if (totalCount === 0) return "No LoRAs in this recipe";
|
||||
if (missingCount === 0) return "All LoRAs available - Ready to use";
|
||||
return `${missingCount} of ${totalCount} LoRAs missing`;
|
||||
if (totalCount === 0) {
|
||||
return translate('recipes.loraStatus.none', {}, 'No LoRAs in this recipe');
|
||||
}
|
||||
if (missingCount === 0) {
|
||||
return translate('recipes.loraStatus.allAvailable', {}, 'All LoRAs available - Ready to use');
|
||||
}
|
||||
return translate(
|
||||
'recipes.loraStatus.missing',
|
||||
{ missing: missingCount, total: totalCount },
|
||||
`${missingCount} of ${totalCount} LoRAs missing`
|
||||
);
|
||||
}
|
||||
|
||||
async toggleFavorite(card) {
|
||||
|
||||
@@ -299,21 +299,6 @@ class RecipeModal {
|
||||
tooltip.style.left = (badgeRect.right - tooltip.offsetWidth) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// Add tooltip positioning for missing badge
|
||||
if (event.target.closest('.recipe-status.missing')) {
|
||||
const badge = event.target.closest('.recipe-status.missing');
|
||||
const tooltip = badge.querySelector('.missing-tooltip');
|
||||
|
||||
if (tooltip) {
|
||||
// Get badge position
|
||||
const badgeRect = badge.getBoundingClientRect();
|
||||
|
||||
// Position the tooltip
|
||||
tooltip.style.top = (badgeRect.bottom + 4) + 'px';
|
||||
tooltip.style.left = (badgeRect.left) + 'px';
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
@@ -872,30 +857,34 @@ class RecipeModal {
|
||||
let statusHTML = '';
|
||||
if (totalCount > 0) {
|
||||
if (allLorasAvailable && deletedLorasCount === 0) {
|
||||
statusHTML = `<div class="recipe-status ready"><i class="fas fa-check-circle"></i> Ready to use</div>`;
|
||||
statusHTML = `<div class="recipe-status ready"><i class="fas fa-check-circle" aria-hidden="true"></i> ${translate('recipes.status.ready', {}, 'Ready to use')}</div>`;
|
||||
} else if (missingLorasCount > 0) {
|
||||
statusHTML = `<div class="recipe-status missing">
|
||||
<i class="fas fa-exclamation-triangle"></i> ${missingLorasCount} missing
|
||||
<div class="missing-tooltip">Click to download missing LoRAs</div>
|
||||
</div>`;
|
||||
// Rendered as a real button so the affordance is visible without
|
||||
// hover and the control is keyboard/screen-reader accessible.
|
||||
// Leading download icon: the red tint + "missing" text already
|
||||
// encode the state, so the icon's job is to hint the action.
|
||||
statusHTML = `<button type="button" class="recipe-status missing clickable"
|
||||
title="${translate('recipes.status.downloadMissingTooltip', {}, 'Click to download missing LoRAs')}"
|
||||
aria-label="${translate('recipes.status.downloadMissing', { count: missingLorasCount }, `Download ${missingLorasCount} missing LoRAs`)}">
|
||||
<i class="fas fa-download" aria-hidden="true"></i> ${translate('recipes.status.missingCount', { count: missingLorasCount }, `${missingLorasCount} missing`)}
|
||||
</button>`;
|
||||
} else if (deletedLorasCount > 0 && missingLorasCount === 0) {
|
||||
statusHTML = `<div class="recipe-status partial"><i class="fas fa-info-circle"></i> ${deletedLorasCount} deleted</div>`;
|
||||
statusHTML = `<div class="recipe-status partial"><i class="fas fa-info-circle" aria-hidden="true"></i> ${translate('recipes.status.deletedCount', { count: deletedLorasCount }, `${deletedLorasCount} deleted`)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
lorasCountElement.innerHTML = `<i class="fas fa-layer-group"></i> ${totalCount} ${totalCount === 1 ? 'LoRA' : 'LoRAs'} ${statusHTML}`;
|
||||
|
||||
const missingStatus = lorasCountElement.querySelector('.recipe-status.missing');
|
||||
if (missingStatus && missingLorasCount > 0) {
|
||||
missingStatus.addEventListener('click', () => this.showDownloadMissingLorasModal());
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const viewRecipeLorasBtn = document.getElementById('viewRecipeLorasBtn');
|
||||
if (viewRecipeLorasBtn) {
|
||||
viewRecipeLorasBtn.addEventListener('click', () => this.navigateToLorasPage());
|
||||
}
|
||||
|
||||
const missingStatus = document.querySelector('.recipe-status.missing');
|
||||
if (missingStatus && missingLorasCount > 0) {
|
||||
missingStatus.classList.add('clickable');
|
||||
missingStatus.addEventListener('click', () => this.showDownloadMissingLorasModal());
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user