mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 08:26:45 -03:00
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:
@@ -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...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -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...",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "הגדרות",
|
"settings": "הגדרות",
|
||||||
"help": "עזרה",
|
"help": "עזרה",
|
||||||
"add": "הוספה",
|
"add": "הוספה",
|
||||||
"close": "סגור"
|
"close": "סגור",
|
||||||
|
"menu": "תפריט"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "טוען...",
|
"loading": "טוען...",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"help": "ヘルプ",
|
"help": "ヘルプ",
|
||||||
"add": "追加",
|
"add": "追加",
|
||||||
"close": "閉じる"
|
"close": "閉じる",
|
||||||
|
"menu": "メニュー"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "설정",
|
"settings": "설정",
|
||||||
"help": "도움말",
|
"help": "도움말",
|
||||||
"add": "추가",
|
"add": "추가",
|
||||||
"close": "닫기"
|
"close": "닫기",
|
||||||
|
"menu": "메뉴"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "로딩 중...",
|
"loading": "로딩 중...",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"help": "Справка",
|
"help": "Справка",
|
||||||
"add": "Добавить",
|
"add": "Добавить",
|
||||||
"close": "Закрыть"
|
"close": "Закрыть",
|
||||||
|
"menu": "Меню"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"help": "帮助",
|
"help": "帮助",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"close": "关闭"
|
"close": "关闭",
|
||||||
|
"menu": "菜单"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"help": "說明",
|
"help": "說明",
|
||||||
"add": "新增",
|
"add": "新增",
|
||||||
"close": "關閉"
|
"close": "關閉",
|
||||||
|
"menu": "選單"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "載入中...",
|
"loading": "載入中...",
|
||||||
|
|||||||
@@ -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 */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
width: 32px;
|
||||||
/* Hide text title on mobile */
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,53 @@
|
|||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div class="header-branding">
|
<!-- Left section: Logo + Navigation -->
|
||||||
<a href="/loras" class="logo-link">
|
<div class="header-left">
|
||||||
<img src="/loras_static/images/favicon-32x32.png" alt="LoRA Manager" class="app-logo">
|
<div class="header-branding">
|
||||||
<span class="app-title">{{ t('header.appTitle') }}</span>
|
<a href="/loras" class="logo-link">
|
||||||
</a>
|
<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>
|
</div>
|
||||||
{% set current_path = request.path %}
|
|
||||||
{% if current_path.startswith('/loras/recipes') %}
|
<!-- Center section: Search -->
|
||||||
{% 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_disabled = current_page == 'statistics' %}
|
||||||
{% set search_placeholder_key = 'header.search.notAvailable' if search_disabled else 'header.search.placeholders.' ~
|
{% set search_placeholder_key = 'header.search.notAvailable' if search_disabled else 'header.search.placeholders.' ~
|
||||||
current_page %}
|
current_page %}
|
||||||
{% set header_search_class = 'header-search disabled' if search_disabled else 'header-search' %}
|
{% set header_search_class = 'header-search disabled' if search_disabled else 'header-search' %}
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Context-aware search container -->
|
|
||||||
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user