Files
Will Miao be75ad930e 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
2026-05-01 21:34:31 +08:00

236 lines
11 KiB
HTML

<header class="app-header">
<div class="header-container">
<!-- Left section: Logo + Navigation -->
<div class="header-left">
<div class="header-branding">
<a href="/loras" class="logo-link">
<img src="/loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo">
<span class="app-title">{{ t('header.appTitle') }}</span>
</a>
</div>
{% 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 %}
<nav class="main-nav">
<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>
</a>
<a href="/loras/recipes" class="nav-item{% if current_path.startswith('/loras/recipes') %} active{% endif %}"
id="recipesNavItem">
<i class="fas fa-book-open"></i> <span>{{ t('header.navigation.recipes') }}</span>
</a>
<a href="/checkpoints" class="nav-item{% if current_path.startswith('/checkpoints') %} active{% endif %}"
id="checkpointsNavItem">
<i class="fas fa-check-circle"></i> <span>{{ t('header.navigation.checkpoints') }}</span>
</a>
<a href="/embeddings" class="nav-item{% if current_path.startswith('/embeddings') %} active{% endif %}"
id="embeddingsNavItem">
<i class="fas fa-code"></i> <span>{{ t('header.navigation.embeddings') }}</span>
</a>
<a href="/statistics" class="nav-item{% if current_path.startswith('/statistics') %} active{% endif %}"
id="statisticsNavItem">
<i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
</a>
</nav>
</div>
<!-- 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="search-container">
<input type="text" id="searchInput" placeholder="{{ t(search_placeholder_key) }}" {% if search_disabled %}
disabled{% endif %} />
<i class="fas fa-search search-icon"></i>
<button class="search-options-toggle" id="searchOptionsToggle" title="{{ t('header.search.options') }}" {% if
search_disabled %} disabled aria-disabled="true" {% endif %}>
<i class="fas fa-sliders-h"></i>
</button>
<button class="search-filter-toggle" id="filterButton" title="{{ t('header.filter.title') }}" {% if
search_disabled %} disabled aria-disabled="true" {% endif %}>
<i class="fas fa-filter"></i>
<span class="filter-badge" id="activeFiltersCount" style="display: none">0</span>
</button>
</div>
</div>
<!-- Right section: Controls -->
<div class="header-right">
<div class="header-controls" id="headerControls">
<div class="theme-toggle" title="{{ t('header.theme.toggle') }}">
<i class="fas fa-moon dark-icon"></i>
<i class="fas fa-sun light-icon"></i>
<i class="fas fa-adjust auto-icon"></i>
</div>
<div class="settings-toggle" title="{{ t('common.actions.settings') }}">
<i class="fas fa-cog"></i>
</div>
<div class="help-toggle" id="helpToggleBtn" title="{{ t('common.actions.help') }}">
<i class="fas fa-question-circle"></i>
<span class="update-badge"></span>
</div>
<div class="update-toggle" id="updateToggleBtn" title="{{ t('header.actions.notifications') }}">
<i class="fas fa-bell"></i>
<span class="update-badge"></span>
</div>
<div class="support-toggle" id="supportToggleBtn" title="{{ t('header.actions.support') }}">
<i class="fas fa-heart"></i>
</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>
</header>
<!-- Add search options panel with context-aware options -->
<div id="searchOptionsPanel" class="search-options-panel hidden">
<div class="options-header">
<h3>{{ t('header.search.options') }}</h3>
<button class="close-options-btn" id="closeSearchOptions">
<i class="fas fa-times"></i>
</button>
</div>
<div class="options-section">
<h4>{{ t('header.search.searchIn') }}</h4>
<div class="search-option-tags">
{% if request.path == '/loras/recipes' %}
<div class="search-option-tag active" data-option="title">{{ t('header.search.filters.title') }}</div>
<div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div>
<div class="search-option-tag active" data-option="loraName">{{ t('header.search.filters.loraName') }}</div>
<div class="search-option-tag active" data-option="loraModel">{{ t('header.search.filters.loraModel') }}</div>
<div class="search-option-tag active" data-option="prompt">{{ t('header.search.filters.prompt') }}</div>
{% elif request.path == '/checkpoints' %}
<div class="search-option-tag active" data-option="filename">{{ t('header.search.filters.filename') }}</div>
<div class="search-option-tag active" data-option="modelname">{{ t('header.search.filters.modelname') }}</div>
<div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div>
<div class="search-option-tag" data-option="creator">{{ t('header.search.filters.creator') }}</div>
{% elif request.path == '/embeddings' %}
<div class="search-option-tag active" data-option="filename">{{ t('header.search.filters.filename') }}</div>
<div class="search-option-tag active" data-option="modelname">{{ t('header.search.filters.modelname') }}</div>
<div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div>
<div class="search-option-tag" data-option="creator">{{ t('header.search.filters.creator') }}</div>
{% else %}
<!-- Default options for LoRAs page -->
<div class="search-option-tag active" data-option="filename">{{ t('header.search.filters.filename') }}</div>
<div class="search-option-tag active" data-option="modelname">{{ t('header.search.filters.modelname') }}</div>
<div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div>
<div class="search-option-tag" data-option="creator">{{ t('header.search.filters.creator') }}</div>
{% endif %}
</div>
</div>
</div>
<!-- Add filter panel -->
<div id="filterPanel" class="filter-panel hidden">
<div class="filter-header">
<h3>{{ t('header.filter.title') }}</h3>
<button class="close-filter-btn" onclick="filterManager.closeFilterPanel()">
<i class="fas fa-times"></i>
</button>
</div>
<!-- Presets Section -->
<div class="filter-section presets-section">
<h4>{{ t('header.filter.presets') }}</h4>
<div class="filter-presets" id="filterPresets">
</div>
</div>
<div class="filter-section">
<h4>{{ t('header.filter.baseModel') }}</h4>
<input
type="text"
id="baseModelSearchInput"
class="filter-search-input"
placeholder="{{ t('header.filter.baseModelSearchPlaceholder') }}"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
spellcheck="false"
>
<div class="filter-tags" id="baseModelTags">
<!-- Tags will be dynamically inserted here -->
</div>
<div id="baseModelEmptyState" class="filter-empty-state" hidden>
{{ t('header.filter.noBaseModelMatches') }}
</div>
</div>
<div class="filter-section">
<div class="filter-section-header">
<h4>{{ t('header.filter.modelTags') }}</h4>
<div class="tag-logic-toggle" id="tagLogicToggle">
<button class="tag-logic-option" data-value="any" title="{{ t('header.filter.tagLogicAny') }}">{{ t('header.filter.any') }}</button>
<button class="tag-logic-option" data-value="all" title="{{ t('header.filter.tagLogicAll') }}">{{ t('header.filter.all') }}</button>
</div>
</div>
<div class="filter-tags" id="modelTagsFilter">
<!-- Top tags will be dynamically inserted here -->
<div class="tags-loading">{{ t('common.status.loading') }}</div>
</div>
</div>
{% if current_page == 'loras' or current_page == 'checkpoints' %}
<div class="filter-section">
<h4>{{ t('header.filter.modelTypes') }}</h4>
<div class="filter-tags" id="modelTypeTags">
<div class="tags-loading">{{ t('common.status.loading') }}</div>
</div>
</div>
{% endif %}
{% if current_page != 'recipes' %}
<div class="filter-section">
<h4>{{ t('header.filter.license') }}</h4>
<div class="filter-tags">
<div class="filter-tag license-tag" data-license="noCredit">
{{ t('header.filter.noCreditRequired') }}
</div>
<div class="filter-tag license-tag" data-license="allowSelling">
{{ t('header.filter.allowSellingGeneratedContent') }}
</div>
</div>
</div>
{% endif %}
<div class="filter-actions">
<button class="clear-filters-btn" onclick="filterManager.clearFilters()">
{{ t('header.filter.clearAll') }}
</button>
</div>
</div>