Files
willmiao-ComfyUI-Lora-Manager/static/css/components/header.css
T
Will Miao 06e230ee39 fix(ui): keep a full search field in the phone header
The <=600px header collapsed the search to a floating 32px circle, which
left ~250px of empty bar at 600px and hid the primary action behind a tap
even though the width supported a real field.

- The phone breakpoint moves to 520px: at 521px the icon nav and the
  200px search floor still coexist, and 480-600px keep their tabs.
- Below that the search is an always-visible pill that fills the row and
  the current page name sits next to the logo, so the bar still says
  where you are now that the tabs live in the menu.
- Drops the click-to-expand overlay and its Escape handler: no fixed
  positioning, no transparent placeholder, no breakpoint duplicated in JS.
- Active menu entry uses an accent tint; the hover token it used before
  has no dark-mode value.

Verified 320-1920px: no clipping or overlap, page title appears at
381-520px and the menu carries all seven tabs.
2026-10-29 20:50:42 +08:00

967 lines
22 KiB
CSS

.app-header {
background: var(--card-bg);
border-bottom: 1px solid var(--border-color);
position: fixed;
top: 0;
z-index: var(--z-header);
height: var(--header-height, 48px);
/* Reduced height */
width: 100%;
box-shadow: var(--shadow-md);
/* Slightly stronger shadow */
}
.header-container {
max-width: none;
margin: 0 auto;
padding: 0 15px;
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
gap: 1rem;
}
/* Left section: Logo + Navigation */
.header-left {
display: flex;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
/* Right section: Controls */
.header-right {
display: flex;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
/* Logo and title styling */
.header-branding {
display: flex;
align-items: center;
flex-shrink: 0;
}
.logo-link {
display: flex;
align-items: center;
text-decoration: none;
color: var(--text-color);
gap: 8px;
}
.app-logo {
width: 24px;
height: 24px;
}
.app-title {
font-size: 1rem;
font-weight: 600;
margin: 0;
}
/* Current page name, shown next to the logo on phone widths where the tab bar
lives in the hamburger menu - without it the bar gives no sense of place. */
.page-title {
display: none;
margin-left: 8px;
max-width: 7rem;
overflow: hidden;
font-size: 0.9rem;
font-weight: 600;
color: var(--text-color);
white-space: nowrap;
text-overflow: ellipsis;
}
/* Navigation styling */
.main-nav {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.nav-item {
padding: 0.25rem 0.75rem;
border-radius: var(--border-radius-xs);
color: var(--text-color);
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
white-space: nowrap;
}
/* Opt-in pages (e.g. Other Models) hide their nav entry until enabled.
A class is used instead of [hidden] because .nav-item sets display: flex. */
.nav-item--hidden {
display: none;
}
.nav-item:hover,
.nav-item:focus-visible {
background-color: var(--lora-surface-hover, oklch(95% 0.02 256));
color: var(--lora-accent);
}
.nav-item:hover i,
.nav-item:hover span,
.nav-item:focus-visible i,
.nav-item:focus-visible span {
color: inherit;
}
.nav-item.active {
background-color: var(--lora-accent);
color: var(--text-on-accent);
}
/* Header search - Centered with VS Code command palette style */
.header-search {
flex: 1;
display: flex;
justify-content: center;
max-width: 600px;
/* No hard floor: the field shrinks with the available space instead of parking
at a fixed width and crowding its own placeholder (see the 1366px query). */
min-width: 0;
margin: 0 auto;
transition: opacity 0.2s ease;
}
/* VS Code command palette style search container */
.header-search .search-container {
width: 100%;
max-width: 600px;
min-width: 0;
position: relative;
display: flex;
align-items: center;
background: var(--input-bg, var(--card-bg));
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm, 6px);
transition: border-color var(--transition-base), box-shadow var(--transition-base);
box-shadow: var(--shadow-header);
overflow: hidden;
}
.header-search .search-container:focus-within {
border-color: var(--lora-accent);
box-shadow: var(--shadow-header), 0 0 0 1px var(--lora-accent);
}
.header-search input {
flex: 1;
width: 100%;
padding: 0.5rem 0.75rem;
padding-left: 2.25rem !important;
/* Reserve exactly the inline chrome so typed text never runs under it:
cue(58) + clear(28) + toggles(28 + 28 + 4 gap) + edges(8 + 8) = 126px.
Below 1366px the cue is hidden and the reservation drops to 68px.
!important is required: search-filter.css loads later and sets its own
right padding at equal specificity (.search-container input). */
padding-right: 7.875rem !important;
border: none;
background: transparent;
color: var(--text-color);
font-size: 0.95rem;
outline: none;
}
.header-search input::placeholder {
color: var(--text-muted);
}
.header-search .search-icon {
position: absolute;
left: 0.75rem;
color: var(--text-muted);
font-size: 0.9rem;
pointer-events: none;
}
.header-search .search-options-toggle,
.header-search .search-filter-toggle {
position: absolute;
right: 0.5rem;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--border-radius-xs, 4px);
transition: background-color var(--transition-base), color var(--transition-base);
}
.header-search .search-options-toggle {
right: 2.25rem;
}
/* Clear button: sit immediately left of the search-options toggle */
.header-search .search-clear {
position: absolute;
right: 4.25rem; /* 2.25rem (options toggle) + 28px toggle width + 4px gap */
top: 50%;
transform: translateY(-50%);
width: 28px;
height: 28px;
display: none;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--border-radius-xs, 4px);
padding: 0;
line-height: 1;
transition: background-color var(--transition-base), color var(--transition-base);
}
.header-search .search-clear.visible {
display: flex;
}
.header-search .search-clear:hover {
background: color-mix(in oklch, var(--text-muted) 15%, transparent);
color: var(--lora-accent);
}
/* Keyboard shortcut cue: shown when search is empty, hidden when typing */
.header-search .search-shortcut-cue {
position: absolute;
right: 4.25rem; /* same slot as clear button */
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
gap: 2px;
pointer-events: none;
font-family: inherit;
font-size: 0.7rem;
line-height: 1;
color: var(--text-muted);
opacity: 0.7;
white-space: nowrap;
transition: opacity 0.2s ease;
}
.header-search .search-shortcut-cue kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 4px;
font-family: inherit;
font-size: 0.68rem;
font-weight: 500;
color: var(--shortcut-text);
background: var(--shortcut-bg);
border: 1px solid var(--shortcut-border);
box-shadow: var(--shortcut-shadow);
border-radius: var(--border-radius-xs, 3px);
line-height: 1;
}
.header-search .search-shortcut-cue.hidden {
display: none;
}
.header-search.disabled .search-shortcut-cue {
display: none;
}
.header-search .search-options-toggle:hover,
.header-search .search-filter-toggle:hover,
.header-search .search-filter-toggle:focus-visible {
background: var(--lora-surface-hover, oklch(95% 0.02 256));
color: var(--lora-accent);
outline: none;
}
.header-search .filter-badge {
position: absolute;
top: 2px;
right: 2px;
width: 8px;
height: 8px;
background: var(--lora-accent);
border-radius: 50%;
font-size: 0;
}
/* Disabled state for header search */
.header-search.disabled {
opacity: 0.5;
pointer-events: none;
}
.header-search.disabled input {
background-color: var(--input-disabled-bg, #f5f5f5);
color: var(--text-muted);
cursor: not-allowed;
}
.header-search.disabled button {
background-color: var(--button-disabled-bg, #e0e0e0);
color: var(--text-muted);
cursor: not-allowed;
}
.header-search.disabled .search-icon {
color: var(--text-muted);
}
/* Dark theme specific styles for disabled header search */
[data-theme="dark"] .header-search.disabled input {
background-color: #3a3a3a;
color: #888888;
border-color: #555555;
}
[data-theme="dark"] .header-search.disabled button {
background-color: #3a3a3a;
color: #888888;
border-color: #555555;
}
[data-theme="dark"] .header-search.disabled .search-icon {
color: #888888;
}
[data-theme="dark"] .header-search.disabled .fas {
color: #888888;
}
/* Header controls (formerly corner controls) */
.header-controls {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.header-controls>div {
width: 32px;
height: 32px;
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;
transition: background-color var(--transition-base), color var(--transition-base), transform var(--transition-base);
position: relative;
}
.header-controls>div:hover {
background: var(--lora-accent);
color: var(--text-on-accent);
transform: translateY(-2px);
}
.theme-toggle {
position: relative;
}
.theme-toggle .light-icon,
.theme-toggle .dark-icon,
.theme-toggle .auto-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.theme-toggle .dark-icon {
opacity: 1;
}
.theme-toggle.theme-light .light-icon {
opacity: 1;
}
.theme-toggle.theme-light .dark-icon,
.theme-toggle.theme-light .auto-icon {
opacity: 0;
}
.theme-toggle.theme-dark .dark-icon {
opacity: 1;
}
.theme-toggle.theme-dark .light-icon,
.theme-toggle.theme-dark .auto-icon {
opacity: 0;
}
.theme-toggle.theme-auto .auto-icon {
opacity: 1;
}
.theme-toggle.theme-auto .light-icon,
.theme-toggle.theme-auto .dark-icon {
opacity: 0;
}
.theme-popover {
display: none;
position: fixed;
background: var(--surface-base, #ffffff);
border: 1px solid var(--border-base, #e0e0e0);
border-radius: var(--radius-md, 8px);
box-shadow: var(--shadow-xl, 0 4px 16px rgba(0, 0, 0, 0.15));
padding: 12px;
min-width: 220px;
z-index: calc(var(--z-overlay) + 1);
animation: theme-popover-in 0.15s ease-out;
}
.theme-popover.active {
display: block;
}
@keyframes theme-popover-in {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.theme-popover-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.theme-popover-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary, #6c757d);
}
.theme-popover-divider {
height: 1px;
background: var(--border-base, #e0e0e0);
margin: 10px 0;
}
.theme-popover-modes {
display: flex;
gap: 6px;
}
.theme-mode-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 4px;
border: 1px solid var(--border-base, #e0e0e0);
border-radius: var(--radius-sm, 6px);
background: var(--surface-elevated, #ffffff);
color: var(--text-primary, #333333);
cursor: pointer;
font-size: 0.75rem;
transition: background-color var(--transition-base, 200ms ease),
border-color var(--transition-base, 200ms ease),
color var(--transition-base, 200ms ease);
}
.theme-mode-btn i {
font-size: 0.9rem;
}
.theme-mode-btn:hover {
background: var(--surface-hover, oklch(95% 0.02 256));
border-color: var(--color-accent, oklch(68% 0.28 256));
}
.theme-mode-btn.active {
background: var(--color-accent-subtle, oklch(68% 0.28 256 / 0.12));
border-color: var(--color-accent, oklch(68% 0.28 256));
color: var(--color-accent, oklch(68% 0.28 256));
}
.theme-popover-presets {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.theme-preset-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px 4px;
border: 1px solid var(--border-base, #e0e0e0);
border-radius: var(--radius-sm, 6px);
background: var(--surface-elevated, #ffffff);
color: var(--text-primary, #333333);
cursor: pointer;
font-size: 0.7rem;
transition: background-color var(--transition-base, 200ms ease),
border-color var(--transition-base, 200ms ease),
color var(--transition-base, 200ms ease);
}
.theme-preset-btn:hover {
background: var(--surface-hover, oklch(95% 0.02 256));
border-color: var(--color-accent, oklch(68% 0.28 256));
}
.theme-preset-btn.active {
background: var(--color-accent-subtle, oklch(68% 0.28 256 / 0.12));
border-color: var(--color-accent, oklch(68% 0.28 256));
color: var(--color-accent, oklch(68% 0.28 256));
}
.preset-swatch {
display: inline-block;
width: 22px;
height: 22px;
border-radius: var(--radius-xs, 4px);
border: 1px solid var(--border-subtle, oklch(72% 0.03 256 / 0.45));
flex-shrink: 0;
transition: transform var(--transition-base, 200ms ease),
box-shadow var(--transition-base, 200ms ease);
}
/* Solid accent colors — each swatch shows the theme's accent color directly.
This matches the app's flat, token-driven design language instead of using
decorative gradients that clash with the matte aesthetic. */
.preset-swatch-default {
background: oklch(68% 0.28 256);
}
.preset-swatch-nord {
background: oklch(62% 0.18 213);
}
.preset-swatch-midnight {
background: oklch(52% 0.15 300);
}
.preset-swatch-monokai {
background: oklch(72% 0.24 190);
}
.preset-swatch-dracula {
background: oklch(68% 0.24 265);
}
.preset-swatch-solarized {
background: oklch(55% 0.18 175);
}
.theme-preset-btn.active .preset-swatch {
box-shadow: 0 0 0 2px var(--color-accent, oklch(68% 0.28 256));
}
.theme-preset-btn:hover .preset-swatch {
transform: scale(1.08);
}
/* Dark mode: use each preset's dark-mode accent lightness for visibility.
These match the --color-accent-l values from :root[data-theme="dark"][data-theme-preset="..."]
in tokens/colors.css so the swatch accurately previews what the theme looks like. */
[data-theme="dark"] .preset-swatch-default {
background: oklch(68% 0.28 256);
}
[data-theme="dark"] .preset-swatch-nord {
background: oklch(68% 0.18 213);
}
[data-theme="dark"] .preset-swatch-midnight {
background: oklch(68% 0.14 300);
}
[data-theme="dark"] .preset-swatch-monokai {
background: oklch(72% 0.24 190);
}
[data-theme="dark"] .preset-swatch-dracula {
background: oklch(72% 0.24 265);
}
[data-theme="dark"] .preset-swatch-solarized {
background: oklch(60% 0.18 175);
}
/* Badge styling */
.update-badge {
position: absolute;
top: -3px;
right: -3px;
width: 8px;
height: 8px;
background-color: var(--lora-error);
border-radius: 50%;
border: 2px solid var(--card-bg);
transition: opacity var(--transition-base);
pointer-events: none;
opacity: 0;
}
.update-badge.visible {
opacity: 1;
}
/* Hamburger menu button - hidden by default */
.hamburger-menu-btn {
display: none;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--card-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color var(--transition-base), color var(--transition-base);
flex-shrink: 0;
}
.hamburger-menu-btn:hover,
.hamburger-menu-btn:focus-visible {
background: var(--lora-surface-hover, oklch(95% 0.02 256));
color: var(--lora-accent);
outline: none;
}
.hamburger-dropdown .dropdown-item:hover,
.hamburger-dropdown .dropdown-item:focus-visible {
background: var(--lora-surface-hover, oklch(95% 0.02 256));
color: var(--lora-accent);
outline: none;
}
/* Hamburger dropdown menu */
.hamburger-dropdown {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm, 6px);
box-shadow: var(--shadow-toast);
padding: 0.5rem;
min-width: 160px;
z-index: var(--z-dropdown, 200);
}
.hamburger-dropdown.active {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.hamburger-dropdown .dropdown-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
border-radius: var(--border-radius-xs, 4px);
color: var(--text-color);
cursor: pointer;
transition: background-color var(--transition-base), color var(--transition-base);
font-size: 0.9rem;
white-space: nowrap;
}
.hamburger-dropdown .dropdown-item:hover {
background: var(--lora-surface-hover, oklch(95% 0.02 256));
color: var(--lora-accent);
}
.hamburger-dropdown .dropdown-item i {
width: 20px;
text-align: center;
}
.hamburger-dropdown .dropdown-divider {
height: 1px;
background: var(--border-color);
margin: 0.25rem 0;
}
/* Page tabs inside the hamburger menu. They mirror the main nav for phone
widths, where the nav itself is hidden (see the <=520px block below);
everywhere else they stay out of the menu. */
.hamburger-dropdown .hamburger-nav-item,
.hamburger-dropdown .hamburger-nav-divider {
display: none;
}
/* SettingsManager hides the opt-in entries (Other, Workflows) on both copies. */
.hamburger-dropdown .hamburger-nav-item[hidden] {
display: none !important;
}
.hamburger-nav-item.active {
color: var(--lora-accent);
/* Accent tint instead of the hover surface token, which has no dark-mode
value (its fallback is a fixed light grey). */
background: color-mix(in oklab, var(--lora-accent) 14%, transparent);
font-weight: 600;
}
/* The menu entries are anchors (the actions are divs), so drop the UA underline. */
.hamburger-dropdown a.hamburger-nav-item {
text-decoration: none;
}
/* Responsive ladder for the header.
Worst case measured in the browser (both opt-in pages enabled, 7 tabs):
icon + label nav ≈ 783px, labels only ≈ 619px (517px once the 1366px tier
below applies). The bar also spends a fixed ~424px on padding, branding,
the five round controls and gaps. Every tier below therefore exists to keep
the search field usable - at 1280px the old ladder left it 73px wide, i.e.
nothing but its two inline toggles. */
/* ≤1500px: nav icons are a wide-screen affordance. The labels keep the tabs
scannable on their own, and dropping the icons hands ~168px back to the
search field. Above 1500px there is room for both. */
@media (max-width: 1500px) {
.nav-item {
gap: 0;
}
.nav-item i {
display: none;
}
}
/* ≤1366px: compact chrome (tighter gaps, padding and nav font), and drop the
Ctrl+F cue - it is pure decoration and the widest thing inside the field.
The shortcut itself keeps working; only the visual hint goes away. */
@media (max-width: 1366px) {
.header-container {
gap: 0.75rem;
padding: 0 12px;
}
.main-nav {
gap: 0.25rem;
}
.nav-item {
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
}
.header-controls {
gap: 6px;
}
.header-search .search-shortcut-cue {
display: none;
}
.header-search input {
padding-right: 4.25rem !important;
}
}
/* ≤1150px: the app title is the cheapest ~137px left in the bar - the logo
still identifies the app, while the nav labels are what people navigate by. */
@media (max-width: 1150px) {
.app-title {
display: none !important;
}
}
/* ≤1100px: collapse the five round controls into the hamburger. Its dropdown
already carries the same five actions, so nothing is lost, and the bar
reclaims ~192px for the nav and the search field. */
@media (max-width: 1100px) {
.header-controls {
display: none !important;
}
.hamburger-menu-btn {
display: flex !important;
}
.hamburger-dropdown {
display: none;
}
.hamburger-dropdown.active {
display: flex;
}
}
/* ≤950px: give the search field the whole flexible row and tighten the nav. */
@media (max-width: 950px) {
.header-container {
padding: 0 10px;
gap: 0.5rem;
}
.header-search {
max-width: none;
margin: 0;
flex: 1;
min-width: 200px;
}
.main-nav {
gap: 0.25rem;
margin-right: 0;
}
.nav-item {
padding: 0.25rem 0.35rem;
font-size: 0.8rem;
}
}
/* Below 780px the labelled nav (461px) can no longer coexist with the 200px
search floor and the hamburger: the row overflowed and clipped the
hamburger by ~40px at 720px. Collapse the labels to icons (190px) and give
the width back to the search field. The tooltip + aria-label on each tab
keep the icon-only state usable. */
@media (max-width: 780px) {
.main-nav {
display: flex;
gap: 0.15rem;
margin-right: 0;
}
.nav-item {
padding: 0.25rem 0.4rem;
}
.nav-item span {
display: none;
}
.nav-item i {
display: block;
}
}
/* Responsive: Compact mode at 768px */
@media (max-width: 768px) {
.header-search input {
padding: 0.4rem 0.6rem;
padding-left: 2rem !important;
padding-right: 4.5rem !important;
font-size: 0.9rem;
}
.header-search .search-container {
border-radius: var(--border-radius-xs, 4px);
}
}
/* Small tablets: keep the icon tabs, tighten the chrome. */
@media (max-width: 600px) {
.header-container {
padding: 0 8px;
gap: 0.4rem;
}
.nav-item {
padding: 0.25rem;
font-size: 0.75rem;
}
.nav-item i {
font-size: 1rem;
}
}
/* Phone widths (<=520px): seven icon tabs no longer fit next to a usable search
field, so the tabs move into the hamburger menu (the template repeats them
there) and the search becomes a full-width pill: logo + page name | search |
menu. 521px is the first width where the icon nav and the 200px search floor
coexist, which is where the cut belongs. */
@media (max-width: 520px) {
.main-nav {
display: none;
}
.page-title {
display: inline-block;
}
.hamburger-dropdown .hamburger-nav-item {
display: flex;
}
.hamburger-dropdown .hamburger-nav-divider {
display: block;
}
/* The menu now holds 7 tabs + 5 actions: let it scroll on short screens. */
.hamburger-dropdown {
max-height: calc(100vh - 72px);
overflow-y: auto;
}
.header-search {
flex: 1 1 auto;
width: auto;
min-width: 0;
max-width: none;
margin: 0 4px;
}
.header-search .search-container {
width: 100%;
border-radius: 999px;
}
.header-search input {
width: 100%;
min-width: 0;
padding: 0.4rem 0.75rem !important;
padding-left: 2rem !important;
padding-right: 4.25rem !important;
/* 16px stops mobile browsers zooming the page on focus. */
font-size: 1rem;
}
}
/* The smallest phones need the page name's width for the search field. */
@media (max-width: 380px) {
.page-title {
display: none;
}
}
/* Position relative for hamburger menu positioning */
.header-right {
position: relative;
}