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",
|
||||
"help": "Hilfe",
|
||||
"add": "Hinzufügen",
|
||||
"close": "Schließen"
|
||||
"close": "Schließen",
|
||||
"menu": "Menü"
|
||||
},
|
||||
"status": {
|
||||
"loading": "Wird geladen...",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"settings": "Settings",
|
||||
"help": "Help",
|
||||
"add": "Add",
|
||||
"close": "Close"
|
||||
"close": "Close",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"status": {
|
||||
"loading": "Loading...",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"settings": "Configuración",
|
||||
"help": "Ayuda",
|
||||
"add": "Añadir",
|
||||
"close": "Cerrar"
|
||||
"close": "Cerrar",
|
||||
"menu": "Menú"
|
||||
},
|
||||
"status": {
|
||||
"loading": "Cargando...",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"settings": "Paramètres",
|
||||
"help": "Aide",
|
||||
"add": "Ajouter",
|
||||
"close": "Fermer"
|
||||
"close": "Fermer",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"status": {
|
||||
"loading": "Chargement...",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"settings": "הגדרות",
|
||||
"help": "עזרה",
|
||||
"add": "הוספה",
|
||||
"close": "סגור"
|
||||
"close": "סגור",
|
||||
"menu": "תפריט"
|
||||
},
|
||||
"status": {
|
||||
"loading": "טוען...",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"settings": "設定",
|
||||
"help": "ヘルプ",
|
||||
"add": "追加",
|
||||
"close": "閉じる"
|
||||
"close": "閉じる",
|
||||
"menu": "メニュー"
|
||||
},
|
||||
"status": {
|
||||
"loading": "読み込み中...",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"settings": "설정",
|
||||
"help": "도움말",
|
||||
"add": "추가",
|
||||
"close": "닫기"
|
||||
"close": "닫기",
|
||||
"menu": "메뉴"
|
||||
},
|
||||
"status": {
|
||||
"loading": "로딩 중...",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"settings": "Настройки",
|
||||
"help": "Справка",
|
||||
"add": "Добавить",
|
||||
"close": "Закрыть"
|
||||
"close": "Закрыть",
|
||||
"menu": "Меню"
|
||||
},
|
||||
"status": {
|
||||
"loading": "Загрузка...",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"settings": "设置",
|
||||
"help": "帮助",
|
||||
"add": "添加",
|
||||
"close": "关闭"
|
||||
"close": "关闭",
|
||||
"menu": "菜单"
|
||||
},
|
||||
"status": {
|
||||
"loading": "加载中...",
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"settings": "設定",
|
||||
"help": "說明",
|
||||
"add": "新增",
|
||||
"close": "關閉"
|
||||
"close": "關閉",
|
||||
"menu": "選單"
|
||||
},
|
||||
"status": {
|
||||
"loading": "載入中...",
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +1,53 @@
|
||||
<header class="app-header">
|
||||
<div class="header-container">
|
||||
<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>
|
||||
<!-- 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>
|
||||
{% 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 %}
|
||||
|
||||
<!-- 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' %}
|
||||
<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="search-container">
|
||||
<input type="text" id="searchInput" placeholder="{{ t(search_placeholder_key) }}" {% if search_disabled %}
|
||||
@@ -62,9 +65,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<!-- Integrated corner controls -->
|
||||
<div class="header-controls">
|
||||
<!-- 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>
|
||||
@@ -85,6 +88,34 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user