Files
ComfyUI-Lora-Manager/static/css/layout.css
T
Will Miao c0165919b3 fix(theme): readable on-accent text color and unified accent aliases
Solid-accent buttons used the page text color (--lora-text) or white,
which falls below WCAG AA on the mid-bright accent (L 52-72%) in every
theme/preset — e.g. ~1.7:1 in dark+solarized, making primary buttons
hard to read. Add --color-accent-contrast (dark, low-chroma, same hue,
derived per preset) aliased as --text-on-accent, and switch ~90
accent-background selectors (primary/confirm buttons, nav items, chips,
badges, checkmarks) to it.

Also define the previously dangling --lora-accent-l/c/h, --lora-accent-hover
and --lora-accent-light aliases (fixing ~40 dead declarations), replace the
hardcoded rgba(--lora-accent-rgb, 79 70 229) fallbacks with oklch channel
syntax, and add the missing margin above .template-custom-input in
settings.
2026-10-10 20:41:10 +08:00

748 lines
18 KiB
CSS

.page-content {
height: calc(100vh - 48px); /* Full height minus header */
margin-top: 48px; /* Push down below header */
width: 100%;
position: relative;
overflow-x: hidden; /* Prevent horizontal scrolling */
overflow-y: scroll; /* Enable vertical scrolling */
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 var(--space-2);
position: relative;
z-index: var(--z-base);
}
/* Sticky controls container */
.controls {
position: sticky;
top: -54px;
z-index: calc(var(--z-header) - 1);
background: var(--bg-color);
padding: var(--space-1) 0;
box-shadow: var(--shadow-xs);
}
/* Wrapper around the controls bar and breadcrumb nav. With the sticky-controls
setting off it is transparent to layout (display: contents), preserving the
original behavior (only the breadcrumb stays visible). When enabled, the whole
wrapper sticks as one unit so the two bars can never drift apart. */
.sticky-topbar {
display: contents;
}
body.sticky-controls .sticky-topbar {
display: block;
position: sticky;
top: 0;
z-index: calc(var(--z-header) - 1);
background: var(--bg-color);
box-shadow: var(--shadow-xs);
}
/* Responsive container for larger screens */
@media (min-width: 2150px) {
.container {
max-width: 1800px;
}
}
@media (min-width: 3000px) {
.container {
max-width: 2400px;
}
}
.controls-right {
display: flex;
align-items: center;
gap: 8px;
/* Push to the right of the row. Because it is also the flex item that is
allowed to drop to a second row, an auto margin keeps it right-aligned on
either row — no width: 100% / viewport breakpoint needed. */
margin-left: auto;
}
.actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
/* Wrap only when the controls genuinely cannot fit, instead of at a fixed
viewport width. Viewport-based wrapping wasted space on high-DPI displays
(e.g. a 2560px monitor at 200% scaling reports a ~1280px CSS viewport even
when the window is maximized). */
flex-wrap: wrap;
width: 100%;
}
.action-buttons {
display: flex;
align-items: center;
gap: var(--space-2);
/* Let the group shrink rather than overflow so .controls-right only wraps
when it really has to. */
flex-wrap: wrap;
flex-shrink: 1;
min-width: 0;
}
/* Action button styling */
.control-group {
position: relative;
}
.control-group button {
/* Keeps the toolbar visually even without forcing the row to overflow (the
old 100px floor pushed the total past the container on wide screens). */
min-width: 90px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
border-radius: var(--border-radius-xs);
padding: 4px 10px;
border: 1px solid var(--border-color);
background: var(--card-bg);
color: var(--text-color);
font-size: 0.85em;
transition: var(--transition-base);
cursor: pointer;
box-shadow: var(--shadow-xs);
}
.control-group button:hover,
.control-group button:focus-visible {
border-color: var(--lora-accent);
background: var(--bg-color);
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
outline: none;
}
.control-group button:active {
transform: translateY(0);
box-shadow: var(--shadow-xs);
}
.control-group button i {
opacity: 0.8;
transition: opacity 0.2s ease;
}
.control-group button:hover i,
.control-group button:focus-visible i {
opacity: 1;
}
.control-group button:disabled {
cursor: not-allowed;
opacity: 0.6;
pointer-events: none;
}
.control-group button.loading,
.dropdown-toggle.loading {
cursor: wait;
opacity: 0.7;
pointer-events: none;
}
/* Controls */
.control-group button.favorite-filter,
.control-group button.update-filter {
position: relative;
overflow: hidden;
}
.control-group button.favorite-filter.active {
background: var(--lora-accent);
color: var(--text-on-accent);
}
.control-group button.favorite-filter i {
margin-right: 4px;
color: var(--favorite-color);
}
.control-group button.update-filter i {
margin-right: 4px;
color: var(--lora-accent);
}
.control-group button.update-filter.active {
background: var(--lora-accent);
color: var(--text-on-accent);
}
.control-group button.update-filter.active i {
color: var(--text-on-accent);
}
.update-filter-group .dropdown-main.update-filter.active + .dropdown-toggle {
background: var(--lora-accent);
border-color: var(--lora-accent);
color: var(--text-on-accent);
}
.update-filter-group .dropdown-main.update-filter.active + .dropdown-toggle i {
color: inherit;
}
/* Active state for buttons that can be toggled */
.control-group button.active {
background: var(--lora-accent);
color: var(--text-on-accent);
border-color: var(--lora-accent);
}
/* Recipes layout toggle (grid / masonry) — segmented control in the toolbar */
.layout-toggle-group {
display: flex;
gap: 0;
}
.layout-toggle-group .layout-toggle-btn {
min-width: 36px;
width: 36px;
padding: 4px 0;
border-radius: 0;
}
.layout-toggle-group .layout-toggle-btn:first-child {
border-radius: var(--border-radius-xs) 0 0 var(--border-radius-xs);
border-right: none;
}
.layout-toggle-group .layout-toggle-btn:last-child {
border-radius: 0 var(--border-radius-xs) var(--border-radius-xs) 0;
}
.layout-toggle-group .layout-toggle-btn:hover,
.layout-toggle-group .layout-toggle-btn:focus-visible {
transform: none;
box-shadow: var(--shadow-xs);
}
/* Keyboard shortcut indicator styling */
.shortcut-key {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 16px;
height: 16px;
padding: 0 4px;
font-size: 10px;
font-weight: 500;
line-height: 1;
text-transform: uppercase;
border-radius: var(--border-radius-xs);
background-color: var(--shortcut-bg);
border: 1px solid var(--shortcut-border);
color: var(--shortcut-text);
opacity: 0.8;
transition: var(--transition-base);
}
.control-group button:hover .shortcut-key {
opacity: 1;
background-color: var(--shortcut-bg-hover);
border-color: var(--shortcut-border-hover);
}
/* Invert the keycap on active (accent-filled) buttons for contrast.
Must come after the hover rule above so it wins on active+hover. */
.control-group button.active .shortcut-key,
.control-group button.active:hover .shortcut-key {
color: var(--lora-accent);
background: rgba(255, 255, 255, 0.92);
border-color: transparent;
opacity: 1;
}
/* Ensure correct vertical alignment for text+shortcut */
.control-group button span {
display: inline-flex;
align-items: center;
gap: 6px;
}
/* Select dropdown styling */
.control-group select {
min-width: 100px;
padding: 4px 26px 4px 10px;
border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color);
background-color: var(--card-bg);
color: var(--text-color);
font-size: 0.85em;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 6px center;
background-size: 14px;
cursor: pointer;
transition: var(--transition-base);
box-shadow: var(--shadow-xs);
}
/* Style for optgroups */
.control-group select optgroup {
font-weight: 600;
font-style: normal;
color: var(--text-color);
background-color: var(--card-bg);
}
.control-group select option {
padding: 4px 8px;
background-color: var(--card-bg);
color: var(--text-color);
}
/* Dark theme optgroup styling */
[data-theme="dark"] .control-group select optgroup {
background-color: var(--card-bg);
color: var(--text-color);
}
[data-theme="dark"] .control-group select option {
background-color: var(--card-bg);
color: var(--text-color);
}
.control-group select:hover {
border-color: var(--lora-accent);
background-color: var(--bg-color);
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
}
.control-group select:focus {
outline: none;
border-color: var(--lora-accent);
box-shadow: 0 0 0 2px oklch(var(--lora-accent) / 0.15);
}
/* Disabled sort dropdown — used when VLM custom filter is active */
.control-group select:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: var(--bg-color);
border-color: var(--border-color);
box-shadow: none;
transform: none;
}
.control-group select:disabled:hover {
border-color: var(--border-color);
background-color: var(--bg-color);
transform: none;
box-shadow: none;
}
/* === Sort dropdown — decoupled trigger width ===========================
The native <select> sizes its trigger to the widest <option>, wasting
horizontal space when a short option is selected. This custom trigger
sizes to the currently selected text only; the dropdown menu sizes to
its content independently. The native <select> is kept in the DOM
(visually hidden) so existing JS that reads/writes `.value` / `.disabled`
and dynamically adds/removes <option>s keeps working. */
.sort-dropdown-group {
position: relative;
display: flex;
}
.sort-trigger {
display: flex;
align-items: center;
gap: 6px;
min-width: 100px;
max-width: 240px;
padding: 4px 10px;
border-radius: var(--border-radius-xs);
border: 1px solid var(--border-color);
background: var(--card-bg);
color: var(--text-color);
font-size: 0.85em;
cursor: pointer;
transition: var(--transition-base);
box-shadow: var(--shadow-xs);
}
.sort-trigger:hover,
.sort-trigger:focus-visible {
border-color: var(--lora-accent);
background: var(--bg-color);
transform: translateY(-1px);
box-shadow: var(--shadow-lg);
outline: none;
}
.sort-trigger:active {
transform: translateY(0);
box-shadow: var(--shadow-xs);
}
.sort-trigger__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sort-trigger__caret {
opacity: 0.8;
transition: transform var(--transition-base);
flex-shrink: 0;
}
.sort-dropdown-group.active .sort-trigger__caret {
transform: rotate(180deg);
}
.sort-dropdown-group.active .sort-trigger {
border-color: var(--lora-accent);
box-shadow: 0 0 0 2px color-mix(in oklch, var(--lora-accent) 15%, transparent);
}
/* Disabled state — mirrors the native :disabled look (used when VLM is active) */
.sort-dropdown-group.is-disabled .sort-trigger {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
background: var(--bg-color);
border-color: var(--border-color);
box-shadow: none;
transform: none;
}
/* Dropdown menu — sizes to its content, independent of trigger width.
Inherits base .dropdown-menu styling; capped for very long i18n text. */
.sort-dropdown-menu {
min-width: max-content;
max-width: 320px;
width: max-content;
}
/* Optgroup label rendered as a section header */
.sort-dropdown-group .sort-optgroup-label {
padding: 8px 12px 4px;
font-size: 0.75em;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
cursor: default;
user-select: none;
}
.sort-dropdown-group .sort-optgroup-label:first-child {
padding-top: 4px;
}
/* Option items */
.sort-dropdown-group .sort-option {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
color: var(--text-color);
cursor: pointer;
transition: background-color 0.2s ease;
white-space: nowrap;
}
.sort-dropdown-group .sort-option::before {
content: '';
width: 14px;
flex-shrink: 0;
text-align: center;
font-weight: 700;
}
.sort-dropdown-group .sort-option:hover {
background-color: color-mix(in oklch, var(--lora-accent) 10%, transparent);
}
.sort-dropdown-group .sort-option.is-selected {
color: var(--lora-accent);
font-weight: 600;
}
.sort-dropdown-group .sort-option.is-selected::before {
content: '\2713';
color: var(--lora-accent);
}
/* Visually hidden native <select> — kept in the DOM for programmatic access.
High-specificity selector overrides .control-group select { min-width: 100px }. */
.control-group .sort-select-native {
position: absolute;
width: 1px;
height: 1px;
min-width: 0;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
opacity: 0;
pointer-events: none;
}
/* Ensure hidden class works properly */
.hidden {
display: none !important;
}
/* Icon-only button style */
.icon-only {
min-width: unset !important;
width: 32px !important;
padding: 0 !important;
height: 32px !important;
}
/* Back to Top Button */
.back-to-top {
position: fixed;
bottom: 85px;
right: 30px;
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--card-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
visibility: hidden;
transform: translateY(10px);
transition: var(--transition-slow);
z-index: var(--z-overlay);
box-shadow: var(--shadow-sm);
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.back-to-top:hover {
background: var(--lora-accent);
color: var(--text-on-accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
/* Prevent text selection in control and header areas */
.control-group button,
.control-group select,
.bulk-operations-panel,
.app-header,
.header-branding,
.app-title,
.main-nav,
.nav-item,
.header-actions button,
.header-controls {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Dropdown Button Styling */
.dropdown-group {
position: relative;
display: flex;
}
.dropdown-main {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: 1px solid var(--border-color);
}
.dropdown-toggle {
width: 24px !important;
min-width: unset !important;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
min-width: max(100%, max-content);
width: max-content;
padding: 5px 0;
margin: 2px 0 0;
font-size: 0.85em;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-xs);
box-shadow: var(--shadow-xl);
}
.dropdown-group.active .dropdown-menu {
display: block;
}
/* Elevate the controls stacking context above breadcrumb nav when a dropdown is open,
so the dropdown menu isn't obscured. Only active when dropdown is shown to avoid
the entire controls bar (which can wrap to 2 rows on narrow viewports) covering
the sticky breadcrumb. */
.controls:has(.dropdown-group.active) {
z-index: var(--z-header);
}
.dropdown-item {
display: block;
padding: 6px 15px;
clear: both;
font-weight: 400;
color: var(--text-color);
cursor: pointer;
transition: background-color 0.2s ease;
}
.dropdown-item.disabled {
cursor: default;
opacity: 0.6;
pointer-events: none;
}
.dropdown-item:hover {
background-color: oklch(var(--lora-accent) / 0.1);
}
.dropdown-item i {
margin-right: 8px;
width: 16px;
text-align: center;
}
/* Intermediate breakpoint: tighten the controls so the whole bar still fits on
one row at common laptop/high-DPI widths. The buttons are allowed to shrink to
their content (min-width: 0) here, which is what reclaims the space the old
100px floor plus a forced wrap used to waste. .controls-right is deliberately
NOT forced onto its own row: it stays inline while it fits and only drops to a
second row (staying right-aligned through its auto margin) when it does not. */
@media (max-width: 1500px) {
.action-buttons {
gap: var(--space-1);
}
.control-group button {
min-width: 0;
padding: 4px 8px;
}
.control-group select {
min-width: 0;
}
}
@media (max-width: 768px) {
.actions {
gap: var(--space-2);
}
.action-buttons {
width: 100%;
}
/* Narrow screens: let the right-hand group wrap below the buttons, still
right-aligned. */
.controls-right {
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--space-1);
}
.control-group button:hover {
transform: none; /* Disable hover effects on mobile */
}
.control-group select:hover {
transform: none; /* Disable hover effects on mobile */
}
.back-to-top {
bottom: 60px; /* Give some extra space from bottom on mobile */
}
.dropdown-menu {
left: auto;
right: 0; /* Align to right on mobile */
}
/* Adjust controls padding on mobile */
.controls {
padding: 10px 0;
}
}
/* Refresh dropdown: per-root scan scope (issue #1108). The list can hold one
entry per configured model root, so it scrolls instead of growing past the
viewport. Offline roots stay clickable (clicking explains the state) and are
only visually de-emphasised — unlike .dropdown-item.disabled, which swallows
the click via pointer-events: none. */
.dropdown-separator {
height: 1px;
margin: 5px 0;
background-color: var(--border-color);
}
.dropdown-section-title {
padding: 4px 15px 2px;
font-size: 0.85em;
font-weight: 600;
color: var(--text-color);
opacity: 0.6;
}
.dropdown-scope-list {
max-height: 45vh;
overflow-y: auto;
}
.scan-root-item.is-offline {
cursor: default;
opacity: 0.6;
}
.scan-root-label {
margin-right: 12px;
}
.scan-root-count,
.scan-root-offline {
float: right;
margin-left: 16px;
font-size: 0.9em;
opacity: 0.7;
}
.scan-root-item.is-offline .scan-root-offline {
opacity: 0.9;
}