Files
ComfyUI-Lora-Manager/static/css/components/recipe-modal.css
T
Will Miao bccd494a56 feat(recipes): explain empty LoRA lists with collapsible "Why no LoRAs?" panel
Record import provenance on every recipe: a new import_info block
(channel, machine-readable no-LoRA reason, diagnostic details) built at
import time across all channels (batch import, single URL, local file,
upload, widget save, re-imports) and persisted in the recipe JSON plus
the SQLite persistent cache (new import_info_json column with ALTER
TABLE migration).

The recipe modal renders the empty LoRA list with a collapsed details
panel showing the import method, the reason (CivitAI API returned no
LoRA resource data, API meta missing, no embedded metadata, ComfyUI
workflow metadata, video, unparsable format), and recorded diagnostics.
Legacy recipes without import_info fall back to heuristics labeled as
inferred. Genuine no-LoRA generations show no panel.

CivitAI images are always classified by API meta shape: the onsite
generator writes A1111-style EXIF without LoRA references, so parsed
EXIF cannot prove "no LoRAs used".

Adds recipes.resources.noLoras* i18n keys (all 10 locales) plus
frontend vitest and backend pytest coverage.
2026-08-30 16:28:41 +08:00

1546 lines
34 KiB
CSS

.recipe-modal-header {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
border-bottom: 1px solid var(--lora-border);
padding-bottom: var(--space-2);
margin-bottom: var(--space-3);
position: relative;
}
/* Header row: title + nav controls. Padding reserves space for the
absolutely positioned nav buttons (see .modal-nav-controls in lora-modal.css). */
.recipe-modal-header-row {
box-sizing: border-box;
width: 100%;
position: relative;
padding-right: 152px;
}
/* 56px right offset keeps the nav buttons clear of the close (x) button,
which is absolutely positioned at the modal-content top-right corner. */
.recipe-modal-header-row .modal-nav-controls {
right: 56px;
}
#recipeTagsContainer {
width: 100%;
}
.recipe-modal-header h2 {
margin: 0 0 var(--space-1);
padding: var(--space-1);
border-radius: var(--border-radius-xs);
font-size: 1.5em;
font-weight: 600;
line-height: 1.2;
color: var(--text-color);
max-height: 2.8em;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
width: calc(100% - 20px);
}
/* Editable content styles */
.editable-content {
position: relative;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.editable-content.hide {
display: none;
}
.editable-content .content-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.edit-icon {
background: none;
border: none;
color: var(--text-color);
opacity: 0;
cursor: pointer;
padding: 4px 8px;
margin-left: 8px;
border-radius: var(--border-radius-xs);
transition: var(--transition-base);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.editable-content:hover .edit-icon {
opacity: 0.6;
}
.edit-icon:hover {
opacity: 1 !important;
background: var(--lora-surface);
}
/* Content editor styles */
.content-editor {
display: none;
width: 100%;
padding: 4px 0;
}
.content-editor.active {
display: flex;
align-items: center;
gap: 8px;
}
.content-editor input {
flex: 1;
background: var(--bg-color);
border: 1px solid var(--lora-border);
border-radius: var(--border-radius-xs);
padding: 6px 8px;
font-size: 1em;
color: var(--text-color);
min-width: 0;
}
/* Remove obsolete button styles */
.editor-actions {
display: none;
}
#recipeModal .modal-content {
display: flex;
flex-direction: column;
/* Content-sized shell: grows with content up to the viewport limit, inner panes scroll past it */
box-sizing: border-box; /* Include padding/border so the shell never exceeds the viewport */
width: min(1600px, 94vw);
max-width: min(1600px, 94vw);
height: auto;
max-height: calc(100vh - var(--header-height, 48px) - 2rem);
overflow: hidden;
}
#recipeModal .modal-body {
display: grid;
grid-template-columns: 320px minmax(0, 1fr) 420px;
gap: var(--space-3);
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
/* Recipe Header Actions */
.recipe-header-actions {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
width: 100%;
margin-bottom: var(--space-1);
flex-shrink: 0;
min-height: 0;
}
.recipe-header-actions:empty {
display: none;
}
.recipe-source-url-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--surface-subtle);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: var(--border-radius-sm);
color: var(--text-color);
cursor: pointer;
font-weight: 500;
font-size: 0.9em;
transition: var(--transition-base);
white-space: nowrap;
}
[data-theme="dark"] .recipe-source-url-btn {
background: var(--surface-subtle);
border: 1px solid var(--lora-border);
}
.recipe-source-url-btn:hover {
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
border-color: var(--lora-accent);
transform: translateY(-1px);
}
.recipe-source-url-btn i {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
/* Icon-only companion to the Send button: same pill style as its neighbors,
just without the text label. */
.modal-copy-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 10px;
background: var(--surface-subtle);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: var(--border-radius-sm);
color: var(--text-color);
cursor: pointer;
font-size: 0.9em;
transition: var(--transition-base);
}
[data-theme="dark"] .modal-copy-btn {
background: var(--surface-subtle);
border: 1px solid var(--lora-border);
}
.modal-copy-btn:hover {
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1);
border-color: var(--lora-accent);
transform: translateY(-1px);
}
.modal-copy-btn:active {
transform: translateY(0);
}
.modal-copy-btn i {
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-height: 860px) {
.recipe-header-actions {
padding-bottom: 4px;
}
}
/* Left Column: Preview */
.recipe-media-column {
display: flex;
flex-direction: column;
gap: var(--space-2);
min-height: 0;
overflow-y: auto;
overflow-x: hidden; /* Guard against sub-pixel overflow from bordered children */
}
/* Recipe Preview */
.recipe-preview-container {
width: 100%;
box-sizing: border-box; /* Keep the 1px border inside the column width */
height: auto;
max-height: 42vh;
border-radius: var(--border-radius-sm);
overflow: hidden;
background: var(--lora-surface);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
flex-shrink: 0;
}
.recipe-preview-container img,
.recipe-preview-container video {
max-width: 100%;
max-height: 42vh;
object-fit: contain;
}
.recipe-preview-media {
max-width: 100%;
max-height: 42vh;
object-fit: contain;
}
/* Source URL container */
.source-url-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
transition: transform 0.3s ease;
transform: translateY(100%);
}
.recipe-preview-container:hover .source-url-container {
transform: translateY(0);
}
.source-url-container.active {
transform: translateY(0);
}
.source-url-content {
display: flex;
align-items: center;
color: #fff;
flex: 1;
overflow: hidden;
font-size: 0.85em;
}
.source-url-icon {
margin-right: 8px;
flex-shrink: 0;
}
.source-url-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
flex: 1;
}
.source-url-edit-btn {
background: none;
border: none;
color: #fff;
cursor: pointer;
padding: 4px;
margin-left: 8px;
border-radius: var(--border-radius-xs);
opacity: 0.7;
transition: opacity 0.2s ease;
flex-shrink: 0;
}
.source-url-edit-btn:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.1);
}
/* Source URL editor */
.source-url-editor {
display: none;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: var(--bg-color);
border-top: 1px solid var(--border-color);
padding: 12px;
flex-direction: column;
gap: 10px;
z-index: 5;
}
.source-url-editor.active {
display: flex;
}
.source-url-input {
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
background: var(--bg-color);
color: var(--text-color);
font-size: 0.9em;
}
.source-url-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.source-url-cancel-btn,
.source-url-save-btn {
padding: 6px 12px;
border-radius: var(--border-radius-xs);
font-size: 0.85em;
cursor: pointer;
border: none;
transition: var(--transition-base);
}
.source-url-cancel-btn {
background: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.source-url-save-btn {
background: var(--lora-accent);
color: white;
}
.source-url-cancel-btn:hover {
background: var(--lora-surface);
}
.source-url-save-btn:hover {
background: color-mix(in oklch, var(--lora-accent), black 10%);
}
/* Generation Parameters */
.recipe-gen-params {
display: flex;
flex-direction: column;
min-height: 0;
overflow-y: auto;
}
.gen-params-header-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-2);
padding-bottom: var(--space-1);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
gap: 8px;
}
.gen-params-header-row h3 {
margin: 0;
font-size: 1.2em;
color: var(--text-color);
}
/* Inline toggle for lora strip setting */
.lora-strip-toggle {
flex-shrink: 0;
gap: 6px;
}
.lora-strip-toggle .inline-toggle-label {
font-size: 0.78em;
white-space: nowrap;
opacity: 0.7;
transition: opacity 0.2s;
}
.lora-strip-toggle:hover .inline-toggle-label {
opacity: 1;
}
.lora-strip-toggle .toggle-switch {
width: 32px;
height: 16px;
}
.lora-strip-toggle .toggle-slider:before {
height: 10px;
width: 10px;
left: 3px;
bottom: 3px;
}
.lora-strip-toggle .toggle-switch input:checked + .toggle-slider:before {
transform: translateX(16px);
}
.gen-params-container {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.param-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.param-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.param-header label {
font-weight: 500;
color: var(--text-color);
}
.param-actions {
display: flex;
align-items: center;
gap: 4px;
}
.copy-btn,
.edit-btn {
background: none;
border: none;
color: var(--text-color);
opacity: 0.6;
cursor: pointer;
padding: 4px 8px;
border-radius: var(--border-radius-xs);
transition: var(--transition-base);
}
.copy-btn:hover,
.edit-btn:hover {
opacity: 1;
background: var(--lora-surface);
}
.param-content {
background: var(--lora-surface);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
padding: var(--space-2);
color: var(--text-color);
font-size: 0.9em;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.param-content.hide {
display: none;
}
.param-content.is-placeholder {
color: color-mix(in oklch, var(--text-color), transparent 35%);
font-style: italic;
}
.param-editor {
display: none;
flex-direction: column;
gap: 10px;
}
.param-editor.active {
display: flex;
}
.param-textarea {
width: 100%;
max-width: 100%;
min-height: 140px;
resize: vertical;
background: var(--bg-color);
border: 1px solid var(--lora-border);
border-radius: var(--border-radius-xs);
padding: 10px 12px;
font-size: 0.9em;
line-height: 1.5;
color: var(--text-color);
font-family: inherit;
box-sizing: border-box;
overflow-x: hidden;
}
.param-editor-hint {
font-size: 0.78em;
line-height: 1.4;
color: color-mix(in oklch, var(--text-color), transparent 35%);
}
/* Other Parameters */
.other-params {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: var(--space-1);
}
.param-tag {
background: var(--lora-surface);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
padding: 4px 8px;
font-size: 0.85em;
color: var(--text-color);
display: flex;
align-items: center;
gap: 6px;
}
.param-tag .param-name {
font-weight: 500;
opacity: 0.8;
}
/* Right Column: Resources */
.recipe-bottom-section {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
.recipe-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-2);
padding-bottom: var(--space-1);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
}
.recipe-section-header h3 {
margin: 0;
font-size: 1.2em;
color: var(--text-color);
display: flex;
align-items: center;
gap: 8px;
}
.recipe-status {
display: inline-flex;
align-items: center;
font-size: 0.85em;
padding: 4px 8px;
border-radius: var(--border-radius-xs);
margin-left: var(--space-1);
}
.recipe-status.ready {
background: oklch(var(--lora-accent) / 0.1);
color: var(--lora-accent);
}
.recipe-status.missing {
background: oklch(var(--lora-error) / 0.1);
color: var(--lora-error);
}
.recipe-status i {
margin-right: 4px;
}
.recipe-section-actions {
display: flex;
align-items: center;
gap: var(--space-1);
}
/* View LoRAs button */
.view-loras-btn {
background: none;
border: none;
color: var(--text-color);
opacity: 0.7;
cursor: pointer;
padding: 4px 8px;
border-radius: var(--border-radius-xs);
transition: var(--transition-base);
display: flex;
align-items: center;
justify-content: center;
}
.view-loras-btn:hover {
opacity: 1;
background: var(--lora-surface);
color: var(--lora-accent);
}
.send-recipe-btn {
background: none;
border: none;
color: var(--text-color);
opacity: 0.7;
cursor: pointer;
padding: 4px 8px;
border-radius: var(--border-radius-xs);
transition: var(--transition-base);
display: flex;
align-items: center;
justify-content: center;
}
.send-recipe-btn:hover {
opacity: 1;
background: var(--lora-surface);
color: var(--lora-accent);
}
#recipeLorasCount {
font-size: 0.9em;
color: var(--text-color);
opacity: 0.8;
display: flex;
align-items: center;
gap: 6px;
}
#recipeLorasCount i {
font-size: 1em;
}
/* LoRAs List */
.recipe-loras-list {
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
flex: 1;
padding-top: 4px; /* Add padding to prevent first item from being cut off when hovered */
}
.recipe-resources-list {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
min-height: 0;
}
/* Empty LoRA list + collapsible "Why no LoRAs?" explanation */
.no-loras {
color: var(--text-muted);
font-size: 0.9em;
padding: var(--space-2) 0;
}
.no-loras-reason {
margin: var(--space-1) 0 var(--space-2);
border: 1px solid var(--lora-border);
border-radius: var(--border-radius-xs);
background: var(--lora-surface);
font-size: 0.85em;
}
.no-loras-reason summary {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
cursor: pointer;
color: var(--text-muted);
user-select: none;
list-style: none;
}
/* Hide the native disclosure triangle; rotate the icon instead. */
.no-loras-reason summary::-webkit-details-marker {
display: none;
}
.no-loras-reason summary i {
transition: transform 0.15s ease;
}
.no-loras-reason[open] summary i {
transform: rotate(90deg);
}
.no-loras-reason summary:hover {
color: var(--text-color);
}
.no-loras-reason-body {
padding: 0 var(--space-3) var(--space-3);
color: var(--text-muted);
}
.no-loras-reason-body ul {
margin: 0;
padding-left: var(--space-5);
}
.no-loras-reason-body li {
margin: var(--space-1) 0;
}
.no-loras-bullet-label {
color: var(--text-color);
font-weight: 600;
}
.no-loras-inferred-note {
font-style: italic;
}
.recipe-checkpoint-container {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.version-divider {
height: 1px;
background: var(--border-color);
margin: var(--space-1) 0;
}
.recipe-lora-item {
display: flex;
/* The reconnect panel is a full-width child that wraps below the
thumbnail + content row. */
flex-wrap: wrap;
gap: var(--space-2);
padding: 10px var(--space-2);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
background: var(--bg-color);
/* Add will-change to create a new stacking context and force hardware acceleration */
will-change: transform;
/* Create a new containing block for absolutely positioned descendants */
transform: translateZ(0);
/* Rows are not clickable by default; only in-library rows navigate */
cursor: default;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
/* Click affordance (pointer + hover lift) is reserved for rows that
actually navigate: in-library items open the local detail view. */
.recipe-lora-item.exists-locally {
cursor: pointer;
}
.recipe-lora-item.exists-locally:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-header);
border-color: var(--lora-accent);
}
.recipe-lora-item.exists-locally:focus-visible {
outline: 2px solid var(--lora-accent);
outline-offset: 2px;
}
.recipe-lora-item.exists-locally {
background: oklch(var(--lora-accent) / 0.05);
border-left: 4px solid var(--lora-accent);
}
.recipe-lora-item.checkpoint-item {
cursor: default;
padding-top: 8px;
padding-bottom: 8px;
align-items: flex-start;
}
.recipe-lora-item.checkpoint-item.exists-locally {
cursor: pointer;
}
.recipe-lora-item.missing-locally {
border-left: 4px solid var(--lora-error);
}
.recipe-lora-item.is-deleted {
background: rgba(127, 127, 127, 0.05);
border-left: 4px solid #777;
opacity: 0.8;
}
.recipe-lora-thumbnail {
width: 46px;
height: 46px;
flex-shrink: 0;
border-radius: var(--border-radius-xs);
overflow: hidden;
background: var(--bg-color);
display: flex;
align-items: center;
justify-content: center;
}
.recipe-lora-thumbnail img,
.recipe-lora-thumbnail video {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumbnail-video {
width: 100%;
height: 100%;
object-fit: cover;
}
.recipe-lora-content {
display: flex;
flex-direction: column;
gap: 3px;
flex: 1;
min-width: 0;
}
.recipe-lora-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-2);
position: relative;
min-height: 28px;
/* Ensure badges don't move during scroll in Chrome */
transform: translateZ(0);
}
.recipe-lora-title {
display: flex;
/* Top-align so the inline Civitai link stays glued to the FIRST line
even when a long model name wraps to two lines. */
align-items: flex-start;
gap: 6px;
flex: 1;
min-width: 0; /* Allow the clamped title to shrink next to the badge */
}
.recipe-lora-content h4 {
margin: 0;
font-size: 1em;
color: var(--text-color);
/* Shrink (for the 2-line clamp) but don't grow: the inline Civitai link
should sit right after the name, not pushed to the far edge. */
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /* Limit to 2 lines */
-webkit-box-orient: vertical;
line-height: 1.3;
}
.recipe-lora-info {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
font-size: 0.85em;
margin-top: 4px;
padding-right: 4px;
}
.recipe-lora-info .base-model {
background: oklch(var(--lora-accent) / 0.1);
color: var(--lora-accent);
padding: 2px 8px;
border-radius: var(--border-radius-xs);
}
.recipe-lora-version {
font-size: 0.85em;
color: var(--text-color);
opacity: 0.7;
}
.recipe-lora-weight {
background: var(--lora-surface);
padding: 2px 8px;
border-radius: var(--border-radius-xs);
font-size: 0.85em;
color: var(--lora-accent);
}
/* Restore icon for manually reconnected entries: its presence on the info
row doubles as the "was reconnected" marker. */
.lora-undo-reconnect {
margin-left: auto;
background: none;
border: none;
color: var(--text-color);
opacity: 0.55;
cursor: pointer;
padding: 2px 4px;
border-radius: var(--border-radius-xs);
font-size: 0.95em;
line-height: 1;
transition: var(--transition-base);
}
.lora-undo-reconnect:hover,
.lora-undo-reconnect:focus-visible {
opacity: 1;
color: var(--lora-accent);
background: var(--lora-surface);
}
.local-badge,
.missing-badge,
.invalid-hash-badge {
position: absolute;
right: 0;
top: 0;
/* Force hardware acceleration for Chrome */
transform: translateZ(0);
backface-visibility: hidden;
}
/* Specific styles for recipe modal badges - update z-index */
.recipe-lora-header .local-badge,
.recipe-lora-header .missing-badge,
.recipe-lora-header .invalid-hash-badge {
z-index: 2; /* Ensure the badge is above other elements */
backface-visibility: hidden;
}
.missing-badge {
display: inline-flex;
align-items: center;
background: var(--lora-error);
color: white;
padding: 3px 6px;
border-radius: var(--border-radius-xs);
font-size: 0.75em;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.missing-badge i {
margin-right: 4px;
font-size: 0.9em;
}
/* Deleted badge with reconnect functionality */
.deleted-badge {
display: inline-flex;
align-items: center;
background: #777;
color: white;
padding: 3px 6px;
border-radius: var(--border-radius-xs);
font-size: 0.75em;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.deleted-badge i {
margin-right: 4px;
font-size: 0.9em;
}
/* Unresolvable-hash badge: the entry has identity fields, but its hash is
not registered on CivitAI (stale or invalid). */
.invalid-hash-badge {
display: inline-flex;
align-items: center;
background: var(--lora-warning);
color: white;
padding: 3px 6px;
border-radius: var(--border-radius-xs);
font-size: 0.75em;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.invalid-hash-badge i {
margin-right: 4px;
font-size: 0.9em;
}
/* Deleted badge is a pure status indicator; the reconnect action lives on
an explicit ghost button in the item's action row. */
/* LoRA reconnect container: an inline extension of the item, not a nested
card — a dashed separator reads lighter than another bordered box inside
an already bordered item. It is a direct child of .recipe-lora-item and
spans the full row (thumbnail column included). */
.lora-reconnect-container {
display: none;
flex-direction: column;
flex-basis: 100%;
/* Flex items default to min-width:auto — never let content force the
panel wider than the row. */
min-width: 0;
border-top: 1px dashed var(--border-color);
padding-top: 10px;
gap: 10px;
}
.lora-reconnect-container.active {
display: flex;
}
.reconnect-instructions {
display: flex;
flex-direction: column;
gap: 5px;
}
.reconnect-instructions p {
margin: 0;
font-size: 0.95em;
font-weight: 500;
color: var(--text-color);
}
.reconnect-instructions small {
color: var(--text-color);
opacity: 0.7;
font-size: 0.85em;
}
.reconnect-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.reconnect-input {
box-sizing: border-box;
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
background: var(--bg-color);
color: var(--text-color);
font-size: 0.95em;
}
.reconnect-error {
display: none;
margin: 0;
color: var(--lora-error);
font-size: 0.85em;
}
.reconnect-error.active {
display: block;
}
.reconnect-suggestions {
display: flex;
flex-direction: column;
gap: 4px;
}
.reconnect-suggestions:empty {
display: none;
}
.reconnect-suggestions-loading,
.reconnect-suggestions-empty {
font-size: 0.85em;
color: var(--text-color);
opacity: 0.7;
padding: 4px 2px;
}
.reconnect-suggestion {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
/* Buttons default to content-box: without this, width:100% + padding +
border overflows the panel by 18px and forces a horizontal scrollbar. */
box-sizing: border-box;
padding: 6px 8px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
background: var(--lora-surface, var(--bg-color));
color: var(--text-color);
font-size: 0.95em;
text-align: left;
cursor: pointer;
transition: var(--transition-base);
}
.reconnect-suggestion:hover,
.reconnect-suggestion:focus-visible {
border-color: var(--lora-accent);
}
.reconnect-suggestion-preview {
width: 40px;
height: 40px;
border-radius: var(--border-radius-xs);
object-fit: cover;
flex-shrink: 0;
background: var(--bg-color);
}
.reconnect-suggestion-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.reconnect-suggestion-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reconnect-suggestion-secondary {
font-size: 0.9em;
opacity: 0.7;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reconnect-suggestion-reason {
flex-shrink: 0;
padding: 2px 6px;
border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color);
color: var(--lora-accent);
font-size: 0.85em;
white-space: nowrap;
}
.reconnect-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.reconnect-cancel-btn,
.reconnect-confirm-btn {
padding: 6px 12px;
border-radius: var(--border-radius-xs);
font-size: 0.85em;
cursor: pointer;
border: none;
transition: var(--transition-base);
}
.reconnect-cancel-btn {
background: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.reconnect-confirm-btn {
background: var(--lora-accent);
color: white;
}
.reconnect-cancel-btn:hover {
background: var(--lora-surface);
}
.reconnect-confirm-btn:hover {
background: color-mix(in oklch, var(--lora-accent), black 10%);
}
/* Recipe status partial state */
.recipe-status.partial {
background: rgba(127, 127, 127, 0.1);
color: #777;
}
/* Title input specific styles */
.title-input {
font-size: 1.2em;
line-height: 1.2;
font-weight: 500;
}
/* Responsive adjustments */
@media (max-width: 1500px) {
#recipeModal .modal-body {
grid-template-columns: 300px minmax(0, 1fr) 380px;
}
}
@media (max-width: 1000px) {
#recipeModal .modal-body {
display: flex;
flex-direction: column;
gap: var(--space-2);
overflow-y: auto;
}
.recipe-media-column {
overflow-y: visible;
flex-shrink: 0;
}
.recipe-preview-container,
.recipe-preview-container img,
.recipe-preview-container video,
.recipe-preview-media {
max-height: 40vh;
}
.recipe-gen-params {
overflow-y: visible;
flex-shrink: 0;
}
.recipe-bottom-section {
flex: none;
}
.recipe-loras-list {
max-height: 45vh;
}
}
@media (max-height: 860px) {
#recipeModal .modal-content {
padding-top: var(--space-2);
padding-bottom: var(--space-2);
}
.recipe-modal-header {
padding-bottom: var(--space-1);
margin-bottom: var(--space-2);
}
.recipe-modal-header h2 {
font-size: 1.3em;
max-height: 2.4em;
}
#recipeTagsContainer {
margin-bottom: 6px;
}
.recipe-preview-container,
.recipe-preview-container img,
.recipe-preview-container video,
.recipe-preview-media {
max-height: 32vh;
}
.recipe-gen-params h3 {
margin-bottom: var(--space-1);
font-size: 1.05em;
}
.gen-params-container {
gap: var(--space-1);
}
.param-content {
padding: 10px;
}
.param-textarea {
min-height: 100px;
}
.other-params {
margin-top: 0;
gap: 6px;
}
.recipe-section-header {
margin-bottom: var(--space-1);
}
}
.badge-container {
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
flex-shrink: 0;
z-index: 2;
}
/* Badges are pure status indicators; actions live in .recipe-lora-actions */
.badge-container .local-badge,
.badge-container .missing-badge,
.badge-container .deleted-badge,
.badge-container .invalid-hash-badge {
position: static; /* Override absolute positioning */
transform: none; /* Remove the transform */
}
/* Tonal (soft) status badges: a tinted fill + colored text reads calmer
than solid blocks when several rows stack, and matches the tonal
"N missing" summary pill above the list. */
.badge-container .local-badge {
background: oklch(var(--lora-accent) / 0.12);
color: var(--lora-accent);
border: 1px solid oklch(var(--lora-accent) / 0.35);
}
.badge-container .missing-badge {
background: oklch(var(--lora-error) / 0.14);
color: var(--lora-error);
border: 1px solid oklch(var(--lora-error) / 0.35);
}
.badge-container .deleted-badge {
background: rgba(127, 127, 127, 0.15);
color: var(--text-muted);
border: 1px solid rgba(127, 127, 127, 0.35);
}
.badge-container .invalid-hash-badge {
background: oklch(var(--lora-warning) / 0.14);
color: var(--lora-warning);
border: 1px solid oklch(var(--lora-warning) / 0.35);
}
/* Pin the recipe status-badge family (missing / deleted / invalid-hash) to its
compact size. import-modal.css defines unscoped .missing-badge/.deleted-badge
and is loaded AFTER this file, so without this higher-specificity rule its
padding/font-size would clobber recipe modal's, leaving invalid-hash-badge
(which has no import counterpart) at a different size. local-badge
intentionally keeps the global shared.css size. */
#recipeModal .badge-container .missing-badge,
#recipeModal .badge-container .deleted-badge,
#recipeModal .badge-container .invalid-hash-badge {
padding: 3px 6px;
font-size: 0.75em;
}
/* 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;
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.clickable:hover {
background: oklch(var(--lora-error) / 0.22);
box-shadow: 0 0 0 2px oklch(var(--lora-error) / 0.25);
}
.recipe-status.missing.clickable:focus-visible {
outline: 2px solid var(--lora-error);
outline-offset: 2px;
}
.recipe-checkpoint-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
font-size: 0.85em;
margin-bottom: 2px;
}
/* Checkpoint type is low-information text (the entry's position above the
divider already implies "checkpoint"), so it renders as plain muted text
instead of a chip competing with the base-model chip. */
.recipe-checkpoint-meta .checkpoint-type-text {
color: var(--text-muted);
}
.recipe-resource-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 2px;
}
.resource-action {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color);
background: var(--bg-color);
color: var(--text-color);
font-size: 0.9em;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}
.resource-action.compact {
padding: 4px 10px;
font-size: 0.88em;
}
.resource-action:hover {
background: var(--lora-surface);
transform: translateY(-1px);
}
.resource-action.primary {
background: var(--lora-accent);
color: white;
border-color: var(--lora-accent);
}
.resource-action.primary:hover {
background: color-mix(in oklch, var(--lora-accent), black 10%);
}
/* Ghost variant: secondary remediation actions (e.g. Reconnect), matching
the ghost action pattern used in the versions tab. */
.resource-action.ghost {
background: transparent;
color: var(--lora-accent);
border-color: oklch(var(--lora-accent) / 0.4);
}
.resource-action.ghost:hover {
background: oklch(var(--lora-accent) / 0.1);
border-color: var(--lora-accent);
}
/* Per-item action row: remediation lives next to the status badge that
surfaced the problem (download / reconnect / external link). Right-aligned
so the reading order stays: name → status → meta → actions. */
.recipe-lora-actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
margin-top: 6px;
}
/* External-link affordance, mirroring .version-civitai-link in the
versions tab: leaving the app is always an explicit, signposted action. */
.recipe-civitai-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 999px;
color: var(--text-muted);
text-decoration: none;
flex: 0 0 auto;
transition: color 0.2s ease, background-color 0.2s ease, transform 0.2s ease;
}
.recipe-civitai-link:hover,
.recipe-civitai-link:focus-visible {
color: var(--lora-accent);
background: color-mix(in oklch, var(--lora-accent) 12%, transparent);
transform: translateY(-1px);
outline: none;
}
/* In titles, size the icon box to the first line box (1em * 1.3 line-height)
so it aligns with the first line of both short and wrapped names. */
.recipe-lora-title .recipe-civitai-link {
width: 20px;
height: calc(1em * 1.3);
}