feat(layout): implement responsive edge-to-edge card grid with density-aware column calculation

- Add dynamic column calculation based on container width and min card width
- Prevent tiny cards on narrow windows by respecting density-based minimums:
  - Default: 240px, Medium: 200px, Compact: 170px
- Fix edge-to-edge layout with proper CSS selector (.virtual-scroll-item.model-card)
- Add hamburger menu for mobile/small screens with proper translations
- Update all locale files with 'common.actions.menu' key

Fixes: Cards becoming too small/overlapping on narrow window widths (e.g., 1156px)
Changes: 15 files, +569/-114 lines
This commit is contained in:
Will Miao
2026-05-01 21:32:46 +08:00
parent 763c4f4dad
commit be75ad930e
15 changed files with 571 additions and 116 deletions

View File

@@ -15,7 +15,8 @@
"settings": "Einstellungen", "settings": "Einstellungen",
"help": "Hilfe", "help": "Hilfe",
"add": "Hinzufügen", "add": "Hinzufügen",
"close": "Schließen" "close": "Schließen",
"menu": "Menü"
}, },
"status": { "status": {
"loading": "Wird geladen...", "loading": "Wird geladen...",

View File

@@ -15,7 +15,8 @@
"settings": "Settings", "settings": "Settings",
"help": "Help", "help": "Help",
"add": "Add", "add": "Add",
"close": "Close" "close": "Close",
"menu": "Menu"
}, },
"status": { "status": {
"loading": "Loading...", "loading": "Loading...",

View File

@@ -15,7 +15,8 @@
"settings": "Configuración", "settings": "Configuración",
"help": "Ayuda", "help": "Ayuda",
"add": "Añadir", "add": "Añadir",
"close": "Cerrar" "close": "Cerrar",
"menu": "Menú"
}, },
"status": { "status": {
"loading": "Cargando...", "loading": "Cargando...",

View File

@@ -15,7 +15,8 @@
"settings": "Paramètres", "settings": "Paramètres",
"help": "Aide", "help": "Aide",
"add": "Ajouter", "add": "Ajouter",
"close": "Fermer" "close": "Fermer",
"menu": "Menu"
}, },
"status": { "status": {
"loading": "Chargement...", "loading": "Chargement...",

View File

@@ -15,7 +15,8 @@
"settings": "הגדרות", "settings": "הגדרות",
"help": "עזרה", "help": "עזרה",
"add": "הוספה", "add": "הוספה",
"close": "סגור" "close": "סגור",
"menu": "תפריט"
}, },
"status": { "status": {
"loading": "טוען...", "loading": "טוען...",

View File

@@ -15,7 +15,8 @@
"settings": "設定", "settings": "設定",
"help": "ヘルプ", "help": "ヘルプ",
"add": "追加", "add": "追加",
"close": "閉じる" "close": "閉じる",
"menu": "メニュー"
}, },
"status": { "status": {
"loading": "読み込み中...", "loading": "読み込み中...",

View File

@@ -15,7 +15,8 @@
"settings": "설정", "settings": "설정",
"help": "도움말", "help": "도움말",
"add": "추가", "add": "추가",
"close": "닫기" "close": "닫기",
"menu": "메뉴"
}, },
"status": { "status": {
"loading": "로딩 중...", "loading": "로딩 중...",

View File

@@ -15,7 +15,8 @@
"settings": "Настройки", "settings": "Настройки",
"help": "Справка", "help": "Справка",
"add": "Добавить", "add": "Добавить",
"close": "Закрыть" "close": "Закрыть",
"menu": "Меню"
}, },
"status": { "status": {
"loading": "Загрузка...", "loading": "Загрузка...",

View File

@@ -15,7 +15,8 @@
"settings": "设置", "settings": "设置",
"help": "帮助", "help": "帮助",
"add": "添加", "add": "添加",
"close": "关闭" "close": "关闭",
"menu": "菜单"
}, },
"status": { "status": {
"loading": "加载中...", "loading": "加载中...",

View File

@@ -15,7 +15,8 @@
"settings": "設定", "settings": "設定",
"help": "說明", "help": "說明",
"add": "新增", "add": "新增",
"close": "關閉" "close": "關閉",
"menu": "選單"
}, },
"status": { "status": {
"loading": "載入中...", "loading": "載入中...",

View File

@@ -22,6 +22,7 @@
transition: transform 160ms ease-out; transition: transform 160ms ease-out;
aspect-ratio: 896/1152; /* Preserve aspect ratio */ aspect-ratio: 896/1152; /* Preserve aspect ratio */
max-width: 260px; /* Base size */ max-width: 260px; /* Base size */
min-width: 200px; /* Prevent cards from becoming too narrow */
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
cursor: pointer; cursor: pointer;
@@ -370,7 +371,16 @@
text-shadow: 0 0 5px rgba(255, 193, 7, 0.5); text-shadow: 0 0 5px rgba(255, 193, 7, 0.5);
} }
/* 响应式设计 */ @media (max-width: 1200px) {
.card-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
.model-card {
max-width: 240px;
min-width: 180px;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.card-grid { .card-grid {
grid-template-columns: minmax(260px, 1fr); /* Adjusted minimum size for mobile */ grid-template-columns: minmax(260px, 1fr); /* Adjusted minimum size for mobile */
@@ -378,6 +388,7 @@
.model-card { .model-card {
max-width: 100%; /* Allow cards to fill available space on mobile */ max-width: 100%; /* Allow cards to fill available space on mobile */
min-width: 200px;
} }
} }
@@ -563,8 +574,13 @@ body.hide-card-version .civitai-version {
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;
transition: transform 160ms ease-out; transition: transform 160ms ease-out;
margin: 0; /* Remove margins, positioning is handled by VirtualScroller */ margin: 0;
width: 100%; /* Allow width to be set by the VirtualScroller */ width: 100%;
}
/* Allow cards to grow beyond 260px in virtual scroll mode */
.virtual-scroll-item.model-card {
max-width: none;
} }
.virtual-scroll-item:hover { .virtual-scroll-item:hover {
@@ -576,11 +592,11 @@ body.hide-card-version .civitai-version {
.card-grid.virtual-scroll { .card-grid.virtual-scroll {
display: block; display: block;
position: relative; position: relative;
margin: 0 auto; margin: 0; /* Remove auto margins - positioning handled by VirtualScroller leftOffset */
padding: 4px 0; /* Add top/bottom padding equivalent to card padding */ padding: 4px 0; /* Add top/bottom padding equivalent to card padding */
height: auto; height: auto;
width: 100%; width: 100%;
max-width: 1400px; /* Keep the max-width from original grid */ max-width: none; /* Remove max-width constraint - handled by VirtualScroller */
box-sizing: border-box; /* Include padding in width calculation */ box-sizing: border-box; /* Include padding in width calculation */
overflow-x: hidden; /* Prevent horizontal overflow */ overflow-x: hidden; /* Prevent horizontal overflow */
} }

View File

@@ -22,6 +22,22 @@
gap: 1rem; 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;
}
/* Responsive header container for larger screens */ /* Responsive header container for larger screens */
@media (min-width: 2150px) { @media (min-width: 2150px) {
.header-container { .header-container {
@@ -77,6 +93,7 @@
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
font-size: 0.9rem; font-size: 0.9rem;
white-space: nowrap;
} }
.nav-item:hover, .nav-item:hover,
@@ -97,13 +114,100 @@
color: white; color: white;
} }
/* Header search */ /* Header search - Centered with VS Code command palette style */
.header-search { .header-search {
flex: 1; flex: 1;
max-width: 400px; display: flex;
justify-content: center;
max-width: 600px;
margin: 0 auto;
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
/* VS Code command palette style search container */
.header-search .search-container {
width: 100%;
max-width: 600px;
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: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.header-search .search-container:focus-within {
border-color: var(--lora-accent);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 0 0 1px var(--lora-accent);
transform: translateY(-1px);
}
.header-search input {
flex: 1;
width: 100%;
padding: 0.5rem 0.75rem;
padding-left: 2.25rem !important;
padding-right: 5rem !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: all 0.2s ease;
}
.header-search .search-options-toggle {
right: 2.25rem;
}
.header-search .search-options-toggle:hover,
.header-search .search-filter-toggle:hover {
background: var(--lora-surface-hover, oklch(95% 0.02 256));
color: var(--lora-accent);
}
.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 */ /* Disabled state for header search */
.header-search.disabled { .header-search.disabled {
opacity: 0.5; opacity: 0.5;
@@ -247,44 +351,207 @@
opacity: 1; opacity: 1;
} }
/* Mobile adjustments */ /* Hamburger menu button - hidden by default */
@media (max-width: 768px) { .hamburger-menu-btn {
.app-title {
display: none; display: none;
/* Hide text title on mobile */ 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: all 0.2s ease;
flex-shrink: 0;
}
.hamburger-menu-btn:hover {
background: var(--lora-accent);
color: white;
}
/* 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: 0 4px 16px rgba(0, 0, 0, 0.15);
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: all 0.2s ease;
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;
}
/* Responsive: Early optimization at 1200px - reduce gaps and padding */
@media (max-width: 1200px) {
.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 { .header-controls {
gap: 4px; gap: 6px;
} }
.header-controls > div { .header-controls > div {
width: 28px; width: 30px;
height: 28px; height: 30px;
}
}
/* Responsive: Hide nav icons at 1100px to save space */
@media (max-width: 1100px) {
.nav-item {
gap: 0;
padding: 0.25rem 0.4rem;
}
.nav-item i {
display: none;
}
.header-search {
max-width: 450px;
}
}
@media (max-width: 950px) {
.app-title {
display: none !important;
}
.header-container {
padding: 0 10px;
gap: 0.5rem;
}
.header-controls {
display: none !important;
}
.hamburger-menu-btn {
display: flex !important;
}
.hamburger-dropdown {
display: none;
}
.hamburger-dropdown.active {
display: flex;
} }
.header-search { .header-search {
max-width: none; max-width: none;
margin: 0 0.5rem; margin: 0;
flex: 1;
min-width: 200px;
} }
.main-nav { .main-nav {
margin-right: 0.5rem; gap: 0.25rem;
margin-right: 0;
}
.nav-item {
padding: 0.25rem 0.35rem;
font-size: 0.8rem;
} }
} }
/* For very small screens */ /* 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);
}
}
/* For very small screens - switch nav to icons only */
@media (max-width: 600px) { @media (max-width: 600px) {
.header-container { .header-container {
padding: 0 8px; padding: 0 8px;
gap: 0.4rem;
} }
.main-nav { .main-nav {
display: none; display: flex;
/* Hide navigation on very small screens */ gap: 0.15rem;
margin-right: 0;
} }
.header-search { .nav-item {
flex: 1; padding: 0.25rem;
font-size: 0.75rem;
}
.nav-item span {
display: none;
}
.nav-item i {
display: block;
font-size: 1rem;
} }
} }
/* Position relative for hamburger menu positioning */
.header-right {
position: relative;
}

View File

@@ -129,6 +129,126 @@ export class HeaderManager {
// Hide search functionality on Statistics page // Hide search functionality on Statistics page
this.updateHeaderForPage(); this.updateHeaderForPage();
// Initialize hamburger menu for mobile
this.initializeHamburgerMenu();
}
initializeHamburgerMenu() {
const hamburgerBtn = document.getElementById('hamburgerMenuBtn');
const hamburgerDropdown = document.getElementById('hamburgerDropdown');
if (!hamburgerBtn || !hamburgerDropdown) return;
// Toggle dropdown on hamburger button click
hamburgerBtn.addEventListener('click', (e) => {
e.stopPropagation();
hamburgerDropdown.classList.toggle('active');
const icon = hamburgerBtn.querySelector('i');
if (hamburgerDropdown.classList.contains('active')) {
icon.classList.remove('fa-bars');
icon.classList.add('fa-times');
} else {
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
}
});
// Handle dropdown item clicks
const dropdownItems = hamburgerDropdown.querySelectorAll('.dropdown-item');
dropdownItems.forEach(item => {
item.addEventListener('click', (e) => {
const action = item.dataset.action;
this.handleHamburgerAction(action);
hamburgerDropdown.classList.remove('active');
const icon = hamburgerBtn.querySelector('i');
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!hamburgerDropdown.contains(e.target) && !hamburgerBtn.contains(e.target)) {
hamburgerDropdown.classList.remove('active');
const icon = hamburgerBtn.querySelector('i');
if (icon) {
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
}
}
});
// Update theme icon in hamburger menu based on current theme
this.updateHamburgerThemeIcon();
}
handleHamburgerAction(action) {
switch (action) {
case 'theme':
if (typeof toggleTheme === 'function') {
const newTheme = toggleTheme();
// Update theme toggle in header if it exists
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
themeToggle.classList.remove('theme-light', 'theme-dark', 'theme-auto');
themeToggle.classList.add(`theme-${newTheme}`);
this.updateThemeTooltip(themeToggle, newTheme);
}
this.updateHamburgerThemeIcon();
}
break;
case 'settings':
if (window.settingsManager) {
window.settingsManager.toggleSettings();
}
break;
case 'help':
const helpToggle = document.getElementById('helpToggleBtn');
if (helpToggle) {
helpToggle.click();
}
break;
case 'notifications':
updateService.toggleUpdateModal();
break;
case 'support':
if (window.modalManager) {
window.modalManager.toggleModal('supportModal');
renderSupporters().catch(error => {
console.error('Error loading supporters:', error);
});
}
break;
}
}
updateHamburgerThemeIcon() {
const themeItem = document.querySelector('.dropdown-item[data-action="theme"]');
if (!themeItem) return;
const currentTheme = getStorageItem('theme') || 'auto';
const icon = themeItem.querySelector('i');
const text = themeItem.querySelector('span');
if (icon) {
icon.classList.remove('fa-moon', 'fa-sun', 'fa-adjust');
if (currentTheme === 'light') {
icon.classList.add('fa-sun');
} else if (currentTheme === 'dark') {
icon.classList.add('fa-moon');
} else {
icon.classList.add('fa-adjust');
}
}
// Update text based on current theme
if (text) {
const key = currentTheme === 'light' ? 'header.theme.switchToDark' :
currentTheme === 'dark' ? 'header.theme.switchToLight' :
'header.theme.toggle';
updateElementAttribute(themeItem, 'aria-label', key, {}, '');
}
} }
updateHeaderForPage() { updateHeaderForPage() {

View File

@@ -104,69 +104,74 @@ export class VirtualScroller {
// Get display density setting // Get display density setting
const displayDensity = state.global.settings?.display_density || 'default'; const displayDensity = state.global.settings?.display_density || 'default';
// Set exact column counts and grid widths to match CSS container widths // Base gap between cards
let maxColumns, maxGridWidth; const baseGap = 12;
this.columnGap = baseGap;
// Match exact column counts and CSS container width values based on density // Define minimum card width based on density setting to ensure usability
// Cards smaller than this become hard to interact with and view
const minCardWidths = {
'default': 240, // Default: comfortable minimum
'medium': 200, // Medium: slightly smaller
'compact': 170 // Compact: smallest usable size
};
const minCardWidth = minCardWidths[displayDensity] || 240;
// Calculate maximum possible columns that fit in available width
// Formula: maxColumns = floor((availableWidth + gap) / (minCardWidth + gap))
const maxPossibleColumns = Math.floor((availableContentWidth + this.columnGap) / (minCardWidth + this.columnGap));
// Ensure at least 1 column
const maxColumns = Math.max(1, maxPossibleColumns);
// Define preferred maximum columns based on display density and screen size
// These are upper limits to prevent too many columns on ultra-wide screens
let preferredMaxColumns;
if (window.innerWidth >= 3000) { // 4K if (window.innerWidth >= 3000) { // 4K
if (displayDensity === 'default') { if (displayDensity === 'default') {
maxColumns = 8; preferredMaxColumns = 8;
} else if (displayDensity === 'medium') { } else if (displayDensity === 'medium') {
maxColumns = 9; preferredMaxColumns = 10;
} else { // compact } else { // compact
maxColumns = 10; preferredMaxColumns = 12;
} }
maxGridWidth = 2400; // Match exact CSS container width for 4K
} else if (window.innerWidth >= 2150) { // 2K/1440p } else if (window.innerWidth >= 2150) { // 2K/1440p
if (displayDensity === 'default') { if (displayDensity === 'default') {
maxColumns = 6; preferredMaxColumns = 6;
} else if (displayDensity === 'medium') { } else if (displayDensity === 'medium') {
maxColumns = 7; preferredMaxColumns = 8;
} else { // compact } else { // compact
maxColumns = 8; preferredMaxColumns = 10;
} }
maxGridWidth = 1800; // Match exact CSS container width for 2K } else { // 1080p and smaller
} else {
// 1080p
if (displayDensity === 'default') { if (displayDensity === 'default') {
maxColumns = 5; preferredMaxColumns = 5;
} else if (displayDensity === 'medium') { } else if (displayDensity === 'medium') {
maxColumns = 6; preferredMaxColumns = 6;
} else { // compact } else { // compact
maxColumns = 7; preferredMaxColumns = 8;
} }
maxGridWidth = 1400; // Match exact CSS container width for 1080p
} }
// Calculate baseCardWidth based on desired column count and available space // Use the smaller of: max columns that fit, or preferred max
// Formula: (maxGridWidth - (columns-1)*gap) / columns // This ensures cards are never smaller than minCardWidth
const baseCardWidth = (maxGridWidth - ((maxColumns - 1) * this.columnGap)) / maxColumns; this.columnsCount = Math.min(maxColumns, preferredMaxColumns);
// Use the smaller of available content width or max grid width // Calculate card width to perfectly fill available space
const actualGridWidth = Math.min(availableContentWidth, maxGridWidth); // Formula: (availableWidth - totalGap) / columns
const totalGap = (this.columnsCount - 1) * this.columnGap;
this.itemWidth = (availableContentWidth - totalGap) / this.columnsCount;
// Set exact column count based on screen size and mode // Calculate height based on aspect ratio (896/1152)
this.columnsCount = maxColumns;
// When available width is smaller than maxGridWidth, recalculate columns
if (availableContentWidth < maxGridWidth) {
// Calculate how many columns can fit in the available space
this.columnsCount = Math.max(1, Math.floor(
(availableContentWidth + this.columnGap) / (baseCardWidth + this.columnGap)
));
}
// Calculate actual item width
this.itemWidth = (actualGridWidth - (this.columnsCount - 1) * this.columnGap) / this.columnsCount;
// Calculate height based on aspect ratio
this.itemHeight = this.itemWidth / this.itemAspectRatio; this.itemHeight = this.itemWidth / this.itemAspectRatio;
// Calculate the left offset to center the grid within the content area // Edge-to-edge layout: no offset, grid fills container
this.leftOffset = Math.max(0, (availableContentWidth - actualGridWidth) / 2); this.leftOffset = 0;
const actualGridWidth = this.itemWidth * this.columnsCount + totalGap;
// Update grid element max-width to match available width // Update grid element to fill available width
this.gridElement.style.maxWidth = `${actualGridWidth}px`; this.gridElement.style.maxWidth = `${actualGridWidth}px`;
this.gridElement.style.width = `${actualGridWidth}px`;
// Add or remove density classes for style adjustments // Add or remove density classes for style adjustments
this.gridElement.classList.remove('default-density', 'medium-density', 'compact-density'); this.gridElement.classList.remove('default-density', 'medium-density', 'compact-density');
@@ -478,6 +483,12 @@ export class VirtualScroller {
element.style.width = `${this.itemWidth}px`; element.style.width = `${this.itemWidth}px`;
element.style.height = `${this.itemHeight}px`; element.style.height = `${this.itemHeight}px`;
// Remove max-width constraint from model-card to allow dynamic sizing
const modelCard = element.querySelector('.model-card');
if (modelCard) {
modelCard.style.maxWidth = 'none';
}
return element; return element;
} }

View File

@@ -1,5 +1,7 @@
<header class="app-header"> <header class="app-header">
<div class="header-container"> <div class="header-container">
<!-- Left section: Logo + Navigation -->
<div class="header-left">
<div class="header-branding"> <div class="header-branding">
<a href="/loras" class="logo-link"> <a href="/loras" class="logo-link">
<img src="/loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo"> <img src="/loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo">
@@ -18,10 +20,6 @@
{% else %} {% else %}
{% set current_page = 'loras' %} {% set current_page = 'loras' %}
{% endif %} {% endif %}
{% set search_disabled = current_page == 'statistics' %}
{% set search_placeholder_key = 'header.search.notAvailable' if search_disabled else 'header.search.placeholders.' ~
current_page %}
{% set header_search_class = 'header-search disabled' if search_disabled else 'header-search' %}
<nav class="main-nav"> <nav class="main-nav">
<a href="/loras" class="nav-item{% if current_path == '/loras' %} active{% endif %}" id="lorasNavItem"> <a href="/loras" class="nav-item{% if current_path == '/loras' %} active{% endif %}" id="lorasNavItem">
<i class="fas fa-layer-group"></i> <span>{{ t('header.navigation.loras') }}</span> <i class="fas fa-layer-group"></i> <span>{{ t('header.navigation.loras') }}</span>
@@ -43,8 +41,13 @@
<i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span> <i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
</a> </a>
</nav> </nav>
</div>
<!-- Context-aware search container --> <!-- Center section: Search -->
{% set search_disabled = current_page == 'statistics' %}
{% set search_placeholder_key = 'header.search.notAvailable' if search_disabled else 'header.search.placeholders.' ~
current_page %}
{% set header_search_class = 'header-search disabled' if search_disabled else 'header-search' %}
<div class="{{ header_search_class }}" id="headerSearch"> <div class="{{ header_search_class }}" id="headerSearch">
<div class="search-container"> <div class="search-container">
<input type="text" id="searchInput" placeholder="{{ t(search_placeholder_key) }}" {% if search_disabled %} <input type="text" id="searchInput" placeholder="{{ t(search_placeholder_key) }}" {% if search_disabled %}
@@ -62,9 +65,9 @@
</div> </div>
</div> </div>
<div class="header-actions"> <!-- Right section: Controls -->
<!-- Integrated corner controls --> <div class="header-right">
<div class="header-controls"> <div class="header-controls" id="headerControls">
<div class="theme-toggle" title="{{ t('header.theme.toggle') }}"> <div class="theme-toggle" title="{{ t('header.theme.toggle') }}">
<i class="fas fa-moon dark-icon"></i> <i class="fas fa-moon dark-icon"></i>
<i class="fas fa-sun light-icon"></i> <i class="fas fa-sun light-icon"></i>
@@ -85,6 +88,34 @@
<i class="fas fa-heart"></i> <i class="fas fa-heart"></i>
</div> </div>
</div> </div>
<!-- Hamburger menu button (visible on mobile) -->
<button class="hamburger-menu-btn" id="hamburgerMenuBtn" title="{{ t('common.actions.menu') }}">
<i class="fas fa-bars"></i>
</button>
<!-- Hamburger dropdown menu -->
<div class="hamburger-dropdown" id="hamburgerDropdown">
<div class="dropdown-item theme-toggle-item" data-action="theme">
<i class="fas fa-moon"></i>
<span>{{ t('header.theme.toggle') }}</span>
</div>
<div class="dropdown-item" data-action="settings">
<i class="fas fa-cog"></i>
<span>{{ t('common.actions.settings') }}</span>
</div>
<div class="dropdown-item" data-action="help">
<i class="fas fa-question-circle"></i>
<span>{{ t('common.actions.help') }}</span>
</div>
<div class="dropdown-item" data-action="notifications">
<i class="fas fa-bell"></i>
<span>{{ t('header.actions.notifications') }}</span>
</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item" data-action="support">
<i class="fas fa-heart"></i>
<span>{{ t('header.actions.support') }}</span>
</div>
</div>
</div> </div>
</div> </div>
</header> </header>