From be75ad930ea44a5a0c75cbb2d631cd1721f9525f Mon Sep 17 00:00:00 2001 From: Will Miao Date: Fri, 1 May 2026 21:32:46 +0800 Subject: [PATCH] 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 --- locales/de.json | 3 +- locales/en.json | 3 +- locales/es.json | 3 +- locales/fr.json | 3 +- locales/he.json | 3 +- locales/ja.json | 3 +- locales/ko.json | 3 +- locales/ru.json | 3 +- locales/zh-CN.json | 3 +- locales/zh-TW.json | 3 +- static/css/components/card.css | 26 ++- static/css/components/header.css | 303 +++++++++++++++++++++++++++-- static/js/components/Header.js | 120 ++++++++++++ static/js/utils/VirtualScroller.js | 91 +++++---- templates/components/header.html | 117 +++++++---- 15 files changed, 571 insertions(+), 116 deletions(-) diff --git a/locales/de.json b/locales/de.json index eed87750..5e1aa2c9 100644 --- a/locales/de.json +++ b/locales/de.json @@ -15,7 +15,8 @@ "settings": "Einstellungen", "help": "Hilfe", "add": "Hinzufügen", - "close": "Schließen" + "close": "Schließen", + "menu": "Menü" }, "status": { "loading": "Wird geladen...", diff --git a/locales/en.json b/locales/en.json index 59104a86..dd125f02 100644 --- a/locales/en.json +++ b/locales/en.json @@ -15,7 +15,8 @@ "settings": "Settings", "help": "Help", "add": "Add", - "close": "Close" + "close": "Close", + "menu": "Menu" }, "status": { "loading": "Loading...", diff --git a/locales/es.json b/locales/es.json index 992d5815..a22f95d6 100644 --- a/locales/es.json +++ b/locales/es.json @@ -15,7 +15,8 @@ "settings": "Configuración", "help": "Ayuda", "add": "Añadir", - "close": "Cerrar" + "close": "Cerrar", + "menu": "Menú" }, "status": { "loading": "Cargando...", diff --git a/locales/fr.json b/locales/fr.json index 1b183229..f3fe26e8 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -15,7 +15,8 @@ "settings": "Paramètres", "help": "Aide", "add": "Ajouter", - "close": "Fermer" + "close": "Fermer", + "menu": "Menu" }, "status": { "loading": "Chargement...", diff --git a/locales/he.json b/locales/he.json index 50481c75..85692c19 100644 --- a/locales/he.json +++ b/locales/he.json @@ -15,7 +15,8 @@ "settings": "הגדרות", "help": "עזרה", "add": "הוספה", - "close": "סגור" + "close": "סגור", + "menu": "תפריט" }, "status": { "loading": "טוען...", diff --git a/locales/ja.json b/locales/ja.json index 2c5b352b..2ff5535a 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -15,7 +15,8 @@ "settings": "設定", "help": "ヘルプ", "add": "追加", - "close": "閉じる" + "close": "閉じる", + "menu": "メニュー" }, "status": { "loading": "読み込み中...", diff --git a/locales/ko.json b/locales/ko.json index 8b028618..99a60249 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -15,7 +15,8 @@ "settings": "설정", "help": "도움말", "add": "추가", - "close": "닫기" + "close": "닫기", + "menu": "메뉴" }, "status": { "loading": "로딩 중...", diff --git a/locales/ru.json b/locales/ru.json index 59246f34..c3fe71d8 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -15,7 +15,8 @@ "settings": "Настройки", "help": "Справка", "add": "Добавить", - "close": "Закрыть" + "close": "Закрыть", + "menu": "Меню" }, "status": { "loading": "Загрузка...", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 428d2aab..955b8264 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -15,7 +15,8 @@ "settings": "设置", "help": "帮助", "add": "添加", - "close": "关闭" + "close": "关闭", + "menu": "菜单" }, "status": { "loading": "加载中...", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index dc8746d0..52a53a41 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -15,7 +15,8 @@ "settings": "設定", "help": "說明", "add": "新增", - "close": "關閉" + "close": "關閉", + "menu": "選單" }, "status": { "loading": "載入中...", diff --git a/static/css/components/card.css b/static/css/components/card.css index cbfc54db..a7492f61 100644 --- a/static/css/components/card.css +++ b/static/css/components/card.css @@ -22,6 +22,7 @@ transition: transform 160ms ease-out; aspect-ratio: 896/1152; /* Preserve aspect ratio */ max-width: 260px; /* Base size */ + min-width: 200px; /* Prevent cards from becoming too narrow */ width: 100%; margin: 0 auto; cursor: pointer; @@ -370,7 +371,16 @@ 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) { .card-grid { grid-template-columns: minmax(260px, 1fr); /* Adjusted minimum size for mobile */ @@ -378,6 +388,7 @@ .model-card { 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; box-sizing: border-box; transition: transform 160ms ease-out; - margin: 0; /* Remove margins, positioning is handled by VirtualScroller */ - width: 100%; /* Allow width to be set by the VirtualScroller */ + margin: 0; + width: 100%; +} + +/* Allow cards to grow beyond 260px in virtual scroll mode */ +.virtual-scroll-item.model-card { + max-width: none; } .virtual-scroll-item:hover { @@ -576,11 +592,11 @@ body.hide-card-version .civitai-version { .card-grid.virtual-scroll { display: block; 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 */ height: auto; 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 */ overflow-x: hidden; /* Prevent horizontal overflow */ } diff --git a/static/css/components/header.css b/static/css/components/header.css index 2626dc9b..19032213 100644 --- a/static/css/components/header.css +++ b/static/css/components/header.css @@ -22,6 +22,22 @@ 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 */ @media (min-width: 2150px) { .header-container { @@ -77,6 +93,7 @@ align-items: center; gap: 0.5rem; font-size: 0.9rem; + white-space: nowrap; } .nav-item:hover, @@ -97,13 +114,100 @@ color: white; } -/* Header search */ +/* Header search - Centered with VS Code command palette style */ .header-search { flex: 1; - max-width: 400px; + display: flex; + justify-content: center; + max-width: 600px; + margin: 0 auto; 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 */ .header-search.disabled { opacity: 0.5; @@ -247,44 +351,207 @@ opacity: 1; } -/* Mobile adjustments */ -@media (max-width: 768px) { - .app-title { - display: none; - /* Hide text title on mobile */ +/* 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: 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 { - gap: 4px; + gap: 6px; } - .header-controls>div { - width: 28px; - height: 28px; + .header-controls > div { + width: 30px; + 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 { max-width: none; - margin: 0 0.5rem; + margin: 0; + flex: 1; + min-width: 200px; } .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) { .header-container { padding: 0 8px; + gap: 0.4rem; } .main-nav { - display: none; - /* Hide navigation on very small screens */ + display: flex; + gap: 0.15rem; + margin-right: 0; } - .header-search { - flex: 1; + .nav-item { + 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; +} diff --git a/static/js/components/Header.js b/static/js/components/Header.js index 05736803..c9e8ef3a 100644 --- a/static/js/components/Header.js +++ b/static/js/components/Header.js @@ -129,6 +129,126 @@ export class HeaderManager { // Hide search functionality on Statistics page 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() { diff --git a/static/js/utils/VirtualScroller.js b/static/js/utils/VirtualScroller.js index 59630f57..1ba45466 100644 --- a/static/js/utils/VirtualScroller.js +++ b/static/js/utils/VirtualScroller.js @@ -104,69 +104,74 @@ export class VirtualScroller { // Get display density setting const displayDensity = state.global.settings?.display_density || 'default'; - // Set exact column counts and grid widths to match CSS container widths - let maxColumns, maxGridWidth; + // Base gap between cards + 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 (displayDensity === 'default') { - maxColumns = 8; + preferredMaxColumns = 8; } else if (displayDensity === 'medium') { - maxColumns = 9; + preferredMaxColumns = 10; } else { // compact - maxColumns = 10; + preferredMaxColumns = 12; } - maxGridWidth = 2400; // Match exact CSS container width for 4K } else if (window.innerWidth >= 2150) { // 2K/1440p if (displayDensity === 'default') { - maxColumns = 6; + preferredMaxColumns = 6; } else if (displayDensity === 'medium') { - maxColumns = 7; + preferredMaxColumns = 8; } else { // compact - maxColumns = 8; + preferredMaxColumns = 10; } - maxGridWidth = 1800; // Match exact CSS container width for 2K - } else { - // 1080p + } else { // 1080p and smaller if (displayDensity === 'default') { - maxColumns = 5; + preferredMaxColumns = 5; } else if (displayDensity === 'medium') { - maxColumns = 6; + preferredMaxColumns = 6; } 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 - // Formula: (maxGridWidth - (columns-1)*gap) / columns - const baseCardWidth = (maxGridWidth - ((maxColumns - 1) * this.columnGap)) / maxColumns; + // Use the smaller of: max columns that fit, or preferred max + // This ensures cards are never smaller than minCardWidth + this.columnsCount = Math.min(maxColumns, preferredMaxColumns); - // Use the smaller of available content width or max grid width - const actualGridWidth = Math.min(availableContentWidth, maxGridWidth); + // Calculate card width to perfectly fill available space + // 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 - 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 + // Calculate height based on aspect ratio (896/1152) this.itemHeight = this.itemWidth / this.itemAspectRatio; - // Calculate the left offset to center the grid within the content area - this.leftOffset = Math.max(0, (availableContentWidth - actualGridWidth) / 2); + // Edge-to-edge layout: no offset, grid fills container + 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.width = `${actualGridWidth}px`; // Add or remove density classes for style adjustments 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.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; } diff --git a/templates/components/header.html b/templates/components/header.html index 0725c923..f7a51240 100644 --- a/templates/components/header.html +++ b/templates/components/header.html @@ -1,50 +1,53 @@
-
- - - {{ t('header.appTitle') }} - + +
+ + {% set current_path = request.path %} + {% if current_path.startswith('/loras/recipes') %} + {% set current_page = 'recipes' %} + {% elif current_path.startswith('/checkpoints') %} + {% set current_page = 'checkpoints' %} + {% elif current_path.startswith('/embeddings') %} + {% set current_page = 'embeddings' %} + {% elif current_path.startswith('/statistics') %} + {% set current_page = 'statistics' %} + {% else %} + {% set current_page = 'loras' %} + {% endif %} +
- {% set current_path = request.path %} - {% if current_path.startswith('/loras/recipes') %} - {% set current_page = 'recipes' %} - {% elif current_path.startswith('/checkpoints') %} - {% set current_page = 'checkpoints' %} - {% elif current_path.startswith('/embeddings') %} - {% set current_page = 'embeddings' %} - {% elif current_path.startswith('/statistics') %} - {% set current_page = 'statistics' %} - {% else %} - {% set current_page = 'loras' %} - {% 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' %} - - -
-
- -
+ +
+
@@ -85,6 +88,34 @@
+ + + +
+ + + + + + +