diff --git a/CHANGELOG.md b/CHANGELOG.md index c774beb..7a0f47b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,12 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning - PostCSS build pipeline with autoprefixer - Reduced inline styles in templates (moved to shared CSS classes) - Organized CSS into functional sections with clear documentation - - **Light theme visual improvements**: Warm earth tone palette with better button/panel contrast +- **Theme Visual Improvements**: Enhanced readability and consistency across all theme modes + - Light mode: Darker text for improved readability, warm earth tone color palette + - Dark mode: Refined contrast for better visual hierarchy + - High-contrast mode: Optimized for maximum accessibility + - Consistent hover states across all interactive elements + - Improved visibility of form inputs and controls - **JavaScript Modernization**: Updated to modern JavaScript patterns - Converted `var` declarations to `const`/`let` - Added TypeScript type annotations for better IDE support and error catching diff --git a/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md index 14f356d..b4f9239 100644 --- a/RELEASE_NOTES_TEMPLATE.md +++ b/RELEASE_NOTES_TEMPLATE.md @@ -34,7 +34,12 @@ Web UI improvements with Tailwind CSS migration, TypeScript conversion, componen - PostCSS build pipeline with autoprefixer - Reduced inline styles in templates (moved to shared CSS classes) - Organized CSS into functional sections with clear documentation - - **Light theme visual improvements**: Warm earth tone palette with better button/panel contrast +- **Theme Visual Improvements**: Enhanced readability and consistency across all theme modes + - Light mode: Darker text for improved readability, warm earth tone color palette + - Dark mode: Refined contrast for better visual hierarchy + - High-contrast mode: Optimized for maximum accessibility + - Consistent hover states across all interactive elements + - Improved visibility of form inputs and controls - **JavaScript Modernization**: Updated to modern JavaScript patterns - Converted `var` declarations to `const`/`let` - Added TypeScript type annotations for better IDE support and error catching diff --git a/code/web/static/shared-components.css b/code/web/static/shared-components.css index 628dff0..986f565 100644 --- a/code/web/static/shared-components.css +++ b/code/web/static/shared-components.css @@ -116,7 +116,7 @@ position: relative; max-width: 720px; width: clamp(320px, 90vw, 720px); - background: #0f1115; + background: var(--panel); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); @@ -197,12 +197,15 @@ font-family: monospace; font-size: 12px; border-left: 3px solid #4ade80; - color: #1f2937; - background: #ffffff; + border-right: 1px solid var(--border); + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + color: var(--text); + background: var(--bg); } .include-textarea::placeholder { - color: #9ca3af; + color: var(--muted); opacity: 0.7; } @@ -226,12 +229,15 @@ font-family: monospace; font-size: 12px; border-left: 3px solid #ef4444; - color: #1f2937; - background: #ffffff; + border-right: 1px solid var(--border); + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + color: var(--text); + background: var(--bg); } .exclude-textarea::placeholder { - color: #9ca3af; + color: var(--muted); opacity: 0.7; } @@ -410,7 +416,7 @@ .build-controls { position: sticky; z-index: 5; - background: linear-gradient(180deg, rgba(15,17,21,.95), rgba(15,17,21,.85)); + background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 0.5rem; @@ -459,8 +465,8 @@ .ownership-badge { display: inline-block; border: 1px solid var(--border); - background: rgba(17,24,39,.9); - color: #e5e7eb; + background: var(--panel); + color: var(--text); border-radius: 12px; font-size: 12px; line-height: 18px; @@ -474,7 +480,7 @@ .build-log { margin-top: 0.5rem; white-space: pre-wrap; - background: #0f1115; + background: var(--panel); border: 1px solid var(--border); padding: 1rem; border-radius: 8px; @@ -575,8 +581,14 @@ padding: 0.5rem; border: 1px solid var(--border); border-radius: 8px; - background: #12161c; + background: var(--panel); font-weight: 600; + transition: background-color 0.15s ease; +} + +.combo-summary:hover { + background: color-mix(in srgb, var(--bg) 70%, var(--text) 30%); + border-color: var(--text); } /* Mana analytics row grid */ @@ -592,7 +604,7 @@ border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem; - background: #0f1115; + background: var(--panel); } /* Mana panel heading */ diff --git a/code/web/static/styles.css b/code/web/static/styles.css index 0dbbdd6..d0593a6 100644 --- a/code/web/static/styles.css +++ b/code/web/static/styles.css @@ -1116,10 +1116,10 @@ video { /* warm beige background (keep existing) */ --panel: #ebe5d8; /* lighter warm cream - more contrast with bg, subtle panels */ - --text: #1a1410; - /* dark brown for readability */ - --muted: #6b655d; - /* warm muted brown (keep existing) */ + --text: #0d0a08; + /* very dark brown/near-black for strong readability */ + --muted: #5a544c; + /* darker muted brown for better contrast */ --border: #bfb5a3; /* darker warm-gray border for better definition */ /* Navbar/banner: darker warm brown for hierarchy */ @@ -1652,6 +1652,42 @@ select,input[type="text"],input[type="number"]{ padding:.35rem .4rem; } +/* Range slider styling */ + +input[type="range"]{ + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; + height: 8px; + background: var(--bg); + border-radius: 4px; + outline: none; + border: 1px solid var(--border); +} + +input[type="range"]::-webkit-slider-thumb{ + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--blue-main); + border-radius: 50%; + cursor: pointer; + border: 2px solid var(--panel); + box-shadow: 0 2px 4px rgba(0,0,0,.2); +} + +input[type="range"]::-moz-range-thumb{ + width: 20px; + height: 20px; + background: var(--blue-main); + border-radius: 50%; + cursor: pointer; + border: 2px solid var(--panel); + box-shadow: 0 2px 4px rgba(0,0,0,.2); +} + fieldset{ border:1px solid var(--border); border-radius:8px; @@ -1740,8 +1776,8 @@ small, .muted{ } .toast{ - background: rgba(17,24,39,.95); - color:#e5e7eb; + background: var(--panel); + color:var(--text); border:1px solid var(--border); border-radius:10px; padding:.5rem .65rem; @@ -2084,8 +2120,8 @@ small, .muted{ position:absolute; top:6px; left:6px; - background:rgba(17,24,39,.9); - color:#e5e7eb; + background:var(--panel); + color:var(--text); border:1px solid var(--border); border-radius:12px; font-size:12px; @@ -2211,7 +2247,7 @@ small, .muted{ width:20px; height:20px; border-radius:50%; - background:#1f2937; + background:var(--bg); font-size:12px; } @@ -2225,7 +2261,7 @@ small, .muted{ position: sticky; top: calc(var(--banner-offset, 48px) + 6px); z-index: 100; - background: linear-gradient(180deg, rgba(15,17,21,.98), rgba(15,17,21,.92)); + background: var(--panel); backdrop-filter: blur(8px); border: 1px solid var(--border); border-radius: 10px; @@ -2712,8 +2748,8 @@ img.lqip.loaded { } .analytics-accordion summary:hover { - background: #1f2937; - border-color: #374151; + background: color-mix(in srgb, var(--bg) 70%, var(--text) 30%); + border-color: var(--text); } .analytics-accordion summary:active { @@ -3509,7 +3545,7 @@ img.lqip.loaded { .modal-content { position: relative; - background: #0f1115; + background: var(--panel); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); diff --git a/code/web/static/tailwind.css b/code/web/static/tailwind.css index 1e1abae..f8d085c 100644 --- a/code/web/static/tailwind.css +++ b/code/web/static/tailwind.css @@ -41,8 +41,8 @@ [data-theme="light-blend"]{ --bg: #e8e2d0; /* warm beige background (keep existing) */ --panel: #ebe5d8; /* lighter warm cream - more contrast with bg, subtle panels */ - --text: #1a1410; /* dark brown for readability */ - --muted: #6b655d; /* warm muted brown (keep existing) */ + --text: #0d0a08; /* very dark brown/near-black for strong readability */ + --muted: #5a544c; /* darker muted brown for better contrast */ --border: #bfb5a3; /* darker warm-gray border for better definition */ /* Navbar/banner: darker warm brown for hierarchy */ --surface-banner: #9b8f7a; /* warm medium brown - darker than panels, lighter than dark theme */ @@ -214,6 +214,37 @@ label{ display:inline-flex; flex-direction:column; gap:.25rem; margin-right:.75r .mana-G{ background:#10b981; border-color:#047857; } .mana-C{ background:#d3d3d3; border-color:#9ca3af; } select,input[type="text"],input[type="number"]{ background: var(--panel); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:.35rem .4rem; } +/* Range slider styling */ +input[type="range"]{ + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 8px; + background: var(--bg); + border-radius: 4px; + outline: none; + border: 1px solid var(--border); +} +input[type="range"]::-webkit-slider-thumb{ + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + background: var(--blue-main); + border-radius: 50%; + cursor: pointer; + border: 2px solid var(--panel); + box-shadow: 0 2px 4px rgba(0,0,0,.2); +} +input[type="range"]::-moz-range-thumb{ + width: 20px; + height: 20px; + background: var(--blue-main); + border-radius: 50%; + cursor: pointer; + border: 2px solid var(--panel); + box-shadow: 0 2px 4px rgba(0,0,0,.2); +} fieldset{ border:1px solid var(--border); border-radius:8px; padding:.75rem; margin:.75rem 0; } small, .muted{ color: var(--muted); } .partner-preview{ border:1px solid var(--border); border-radius:8px; background: var(--panel); padding:.75rem; margin-bottom:.5rem; } @@ -231,7 +262,7 @@ small, .muted{ color: var(--muted); } /* Toasts */ .toast-host{ position: fixed; right: 12px; bottom: 12px; display: flex; flex-direction: column; gap: 8px; z-index: 9999; } -.toast{ background: rgba(17,24,39,.95); color:#e5e7eb; border:1px solid var(--border); border-radius:10px; padding:.5rem .65rem; box-shadow: 0 8px 24px rgba(0,0,0,.35); transition: transform .2s ease, opacity .2s ease; } +.toast{ background: var(--panel); color:var(--text); border:1px solid var(--border); border-radius:10px; padding:.5rem .65rem; box-shadow: 0 8px 24px rgba(0,0,0,.35); transition: transform .2s ease, opacity .2s ease; } .toast.hide{ opacity:0; transform: translateY(6px); } .toast.success{ border-color: rgba(22,163,74,.4); } .toast.error{ border-color: rgba(239,68,68,.45); } @@ -403,8 +434,8 @@ small, .muted{ color: var(--muted); } position:absolute; top:6px; left:6px; - background:rgba(17,24,39,.9); - color:#e5e7eb; + background:var(--panel); + color:var(--text); border:1px solid var(--border); border-radius:12px; font-size:12px; @@ -448,7 +479,7 @@ small, .muted{ color: var(--muted); } .stage-nav .stage-link { display:flex; align-items:center; gap:.4rem; background: var(--panel); border:1px solid var(--border); color:var(--text); border-radius:999px; padding:.25rem .6rem; cursor:pointer; } .stage-nav .stage-item.done .stage-link { opacity:.75; } .stage-nav .stage-item.current .stage-link { box-shadow: 0 0 0 2px rgba(96,165,250,.4) inset; border-color:#3b82f6; } -.stage-nav .idx { display:inline-grid; place-items:center; width:20px; height:20px; border-radius:50%; background:#1f2937; font-size:12px; } +.stage-nav .idx { display:inline-grid; place-items:center; width:20px; height:20px; border-radius:50%; background:var(--bg); font-size:12px; } .stage-nav .name { font-size:12px; } /* Build controls sticky box tweaks */ @@ -456,7 +487,7 @@ small, .muted{ color: var(--muted); } position: sticky; top: calc(var(--banner-offset, 48px) + 6px); z-index: 100; - background: linear-gradient(180deg, rgba(15,17,21,.98), rgba(15,17,21,.92)); + background: var(--panel); backdrop-filter: blur(8px); border: 1px solid var(--border); border-radius: 10px; @@ -678,8 +709,8 @@ img.lqip.loaded { filter: blur(0); opacity: 1; } } .analytics-accordion summary:hover { - background: #1f2937; - border-color: #374151; + background: color-mix(in srgb, var(--bg) 70%, var(--text) 30%); + border-color: var(--text); } .analytics-accordion summary:active { @@ -1434,7 +1465,7 @@ img.lqip.loaded { filter: blur(0); opacity: 1; } .modal-content { position: relative; - background: #0f1115; + background: var(--panel); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); diff --git a/code/web/static/ts/app.ts b/code/web/static/ts/app.ts index 91a8138..3e276eb 100644 --- a/code/web/static/ts/app.ts +++ b/code/web/static/ts/app.ts @@ -1409,4 +1409,294 @@ interface SkeletonManager { // Initialize on load and after HTMX swaps document.addEventListener('DOMContentLoaded', function(){ initLazyAccordions(document.body); }); document.addEventListener('htmx:afterSwap', function(e){ initLazyAccordions(e.target); }); + + // ============================================================================= + // UTILITIES EXTRACTED FROM BASE.HTML INLINE SCRIPTS (Phase 3) + // ============================================================================= + + /** + * Poll setup status endpoint for progress updates + * Shows dynamic status message in #banner-status element + */ + function initSetupStatusPoller(): void { + let statusEl: HTMLElement | null = null; + + function ensureStatusEl(): HTMLElement | null { + if (!statusEl) statusEl = document.getElementById('banner-status'); + return statusEl; + } + + function renderSetupStatus(data: any): void { + const el = ensureStatusEl(); + if (!el) return; + + if (data && data.running) { + const msg = data.message || 'Preparing data...'; + const pct = (typeof data.percent === 'number') ? data.percent : null; + + // Suppress banner if we're effectively finished (>=99%) or message is purely theme catalog refreshed + let suppress = false; + if (pct !== null && pct >= 99) suppress = true; + const lm = (msg || '').toLowerCase(); + if (lm.indexOf('theme catalog refreshed') >= 0) suppress = true; + + if (suppress) { + if (el.innerHTML) { + el.innerHTML = ''; + el.classList.remove('busy'); + } + return; + } + + el.innerHTML = 'Setup/Tagging: ' + msg + ' View progress'; + el.classList.add('busy'); + } else if (data && data.phase === 'done') { + el.innerHTML = ''; + el.classList.remove('busy'); + } else if (data && data.phase === 'error') { + el.innerHTML = 'Setup error.'; + setTimeout(function(){ + el.innerHTML = ''; + el.classList.remove('busy'); + }, 5000); + } else { + if (!el.innerHTML.trim()) el.innerHTML = ''; + el.classList.remove('busy'); + } + } + + function pollStatus(): void { + try { + fetch('/status/setup', { cache: 'no-store' }) + .then(function(r){ return r.json(); }) + .then(renderSetupStatus) + .catch(function(){ /* noop */ }); + } catch(_){} + } + + // Poll every 10 seconds to reduce server load (only for header indicator) + setInterval(pollStatus, 10000); + pollStatus(); // Initial poll + } + + /** + * Highlight active navigation link based on current path + * Matches exact or prefix paths, prioritizing longer matches + */ + function initActiveNavHighlighter(): void { + try { + const path = window.location.pathname || '/'; + const nav = document.getElementById('primary-nav'); + if (!nav) return; + + const links = nav.querySelectorAll('a'); + let best: HTMLAnchorElement | null = null; + let bestLen = -1; + + links.forEach(function(a){ + const href = a.getAttribute('href') || ''; + if (!href) return; + // Exact match or prefix match (ignoring trailing slash) + if (path === href || path === href + '/' || (href !== '/' && path.startsWith(href))){ + if (href.length > bestLen){ + best = a as HTMLAnchorElement; + bestLen = href.length; + } + } + }); + + if (best) best.classList.add('active'); + } catch(_){} + } + + /** + * Initialize theme selector dropdown and persistence + * Handles localStorage, URL overrides, and system preference tracking + */ + function initThemeSelector(enableThemes: boolean, defaultTheme: string): void { + if (!enableThemes) return; + + try { + const sel = document.getElementById('theme-select') as HTMLSelectElement | null; + const resetBtn = document.getElementById('theme-reset'); + const root = document.documentElement; + const KEY = 'mtg:theme'; + const SERVER_DEFAULT = defaultTheme; + + function mapLight(v: string): string { + return v === 'light' ? 'light-blend' : v; + } + + function resolveSystem(): string { + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + return prefersDark ? 'dark' : 'light-blend'; + } + + function normalizeUiValue(v: string): string { + const x = (v || 'system').toLowerCase(); + if (x === 'light-blend' || x === 'light-slate' || x === 'light-parchment') return 'light'; + return x; + } + + function apply(val: string): void { + let v = (val || 'system').toLowerCase(); + if (v === 'system') v = resolveSystem(); + v = mapLight(v); + root.setAttribute('data-theme', v); + } + + // Optional URL override: ?theme=system|light|dark|high-contrast|cb-friendly + const params = new URLSearchParams(window.location.search || ''); + const urlTheme = (params.get('theme') || '').toLowerCase(); + if (urlTheme) { + // Persist the UI value, not the mapped CSS token + localStorage.setItem(KEY, normalizeUiValue(urlTheme)); + // Clean the URL so reloads don't keep overriding + try { + const u = new URL(window.location.href); + u.searchParams.delete('theme'); + window.history.replaceState({}, document.title, u.toString()); + } catch(_){} + } + + // Determine initial selection: URL -> localStorage -> server default -> system + const stored = localStorage.getItem(KEY); + const initial = urlTheme || ((stored && stored.trim()) ? stored : (SERVER_DEFAULT || 'system')); + apply(initial); + + if (sel) { + sel.value = normalizeUiValue(initial); + sel.addEventListener('change', function(){ + const v = sel.value || 'system'; + localStorage.setItem(KEY, v); + apply(v); + }); + } + + if (resetBtn) { + resetBtn.addEventListener('click', function(){ + try { localStorage.removeItem(KEY); } catch(_){} + const v = SERVER_DEFAULT || 'system'; + apply(v); + if (sel) sel.value = normalizeUiValue(v); + }); + } + + // React to system changes when set to system + if (window.matchMedia) { + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + mq.addEventListener && mq.addEventListener('change', function(){ + const cur = localStorage.getItem(KEY) || (SERVER_DEFAULT || 'system'); + if (cur === 'system') apply('system'); + }); + } + } catch(_){} + } + + /** + * Apply theme from environment variable when selector is disabled + * Resolves 'system' to OS preference + */ + function initThemeEnvOnly(enableThemes: boolean, defaultTheme: string): void { + if (enableThemes) return; // Only run when themes are disabled + + try { + const root = document.documentElement; + const SERVER_DEFAULT = defaultTheme; + + function resolveSystem(): string { + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + return prefersDark ? 'dark' : 'light-blend'; + } + + let v = (SERVER_DEFAULT || 'system').toLowerCase(); + if (v === 'system') v = resolveSystem(); + if (v === 'light') v = 'light-blend'; + root.setAttribute('data-theme', v); + + // Track OS changes when using system + if ((SERVER_DEFAULT || 'system').toLowerCase() === 'system' && window.matchMedia) { + const mq = window.matchMedia('(prefers-color-scheme: dark)'); + mq.addEventListener && mq.addEventListener('change', function(){ + root.setAttribute('data-theme', resolveSystem()); + }); + } + } catch(_){} + } + + /** + * Register PWA service worker and handle updates + * Automatically reloads when new version is available + */ + function initServiceWorker(enablePwa: boolean, catalogHash: string): void { + if (!enablePwa) return; + + try { + if ('serviceWorker' in navigator) { + const ver = catalogHash || 'dev'; + const url = '/static/sw.js?v=' + encodeURIComponent(ver); + + navigator.serviceWorker.register(url).then(function(reg){ + (window as any).__pwaStatus = { registered: true, scope: reg.scope, version: ver }; + + // Listen for updates (new worker installing) + if (reg.waiting) { + reg.waiting.postMessage({ type: 'SKIP_WAITING' }); + } + + reg.addEventListener('updatefound', function(){ + try { + const nw = reg.installing; + if (!nw) return; + + nw.addEventListener('statechange', function(){ + if (nw.state === 'installed' && navigator.serviceWorker.controller) { + // New version available; reload silently for freshness + try { + sessionStorage.setItem('mtg:swUpdated', '1'); + } catch(_){} + window.location.reload(); + } + }); + } catch(_){} + }); + }).catch(function(){ + (window as any).__pwaStatus = { registered: false }; + }); + } + } catch(_){} + } + + /** + * Show toast after page reload + * Used when actions replace the whole document + */ + function initToastAfterReload(): void { + try { + const raw = sessionStorage.getItem('mtg:toastAfterReload'); + if (raw) { + sessionStorage.removeItem('mtg:toastAfterReload'); + const data = JSON.parse(raw); + if (data && data.msg) { + window.toast && window.toast(data.msg, data.type || ''); + } + } + } catch(_){} + } + + // Initialize all utilities on DOMContentLoaded + document.addEventListener('DOMContentLoaded', function(){ + initSetupStatusPoller(); + initActiveNavHighlighter(); + initToastAfterReload(); + + // Theme and PWA initialization require server-injected values + // These will be called from base.html inline scripts that pass the values + // window.__initThemeSelector, window.__initThemeEnvOnly, window.__initServiceWorker + }); + + // Expose functions globally for inline script calls (with server values) + (window as any).__initThemeSelector = initThemeSelector; + (window as any).__initThemeEnvOnly = initThemeEnvOnly; + (window as any).__initServiceWorker = initServiceWorker; })(); diff --git a/code/web/static/ts/cardHover.ts b/code/web/static/ts/cardHover.ts index 260e783..15f0836 100644 --- a/code/web/static/ts/cardHover.ts +++ b/code/web/static/ts/cardHover.ts @@ -117,7 +117,7 @@ interface PointerEventLike { panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'Card detail hover panel'); panel.setAttribute('aria-hidden', 'true'); - panel.style.cssText = 'display:none;position:fixed;z-index:9999;width:560px;max-width:98vw;background:#1f2937;border:1px solid #374151;border-radius:12px;padding:18px;box-shadow:0 16px 42px rgba(0,0,0,.75);color:#f3f4f6;font-size:14px;line-height:1.45;pointer-events:none;'; + panel.style.cssText = 'display:none;position:fixed;z-index:9999;width:560px;max-width:98vw;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:18px;box-shadow:0 16px 42px rgba(0,0,0,.75);color:var(--text);font-size:14px;line-height:1.45;pointer-events:none;'; panel.innerHTML = '' + '
' + '
 
' + @@ -126,11 +126,11 @@ interface PointerEventLike { '
' + '
' + '
' + - 'Card image' + + 'Card image' + '
' + '
' + '
' + - '
 
' + + '
 
' + '
' + '
' + '' + diff --git a/code/web/templates/base.html b/code/web/templates/base.html index 3e27358..c17b53f 100644 --- a/code/web/templates/base.html +++ b/code/web/templates/base.html @@ -66,6 +66,7 @@

MTG Deckbuilder

+
@@ -195,51 +196,8 @@ document.body.classList.remove('htmx-settling'); }); - // Setup/Tagging status poller - var statusEl; - function ensureStatusEl(){ - if (!statusEl) statusEl = document.getElementById('banner-status'); - return statusEl; - } - function renderSetupStatus(data){ - var el = ensureStatusEl(); if (!el) return; - if (data && data.running) { - var msg = (data.message || 'Preparing data...'); - var pct = (typeof data.percent === 'number') ? data.percent : null; - // Suppress banner if we're effectively finished (>=99%) or message is purely theme catalog refreshed - var suppress = false; - if (pct !== null && pct >= 99) suppress = true; - var lm = (msg || '').toLowerCase(); - if (lm.indexOf('theme catalog refreshed') >= 0) suppress = true; - if (suppress) { - if (el.innerHTML) { el.innerHTML=''; el.classList.remove('busy'); } - return; - } - el.innerHTML = 'Setup/Tagging: ' + msg + ' View progress'; - el.classList.add('busy'); - } else if (data && data.phase === 'done') { - el.innerHTML = ''; - el.classList.remove('busy'); - } else if (data && data.phase === 'error') { - el.innerHTML = 'Setup error.'; - setTimeout(function(){ el.innerHTML = ''; el.classList.remove('busy'); }, 5000); - } else { - if (!el.innerHTML.trim()) el.innerHTML = ''; - el.classList.remove('busy'); - } - } - function pollStatus(){ - try { - fetch('/status/setup', { cache: 'no-store' }) - .then(function(r){ return r.json(); }) - .then(renderSetupStatus) - .catch(function(){ /* noop */ }); - } catch(e) {} - } - // Poll every 10 seconds instead of 3 to reduce server load (only for header indicator) - setInterval(pollStatus, 10000); - pollStatus(); - + // Setup/Tagging status poller moved to app.ts (initSetupStatusPoller) + // Expose normalizeCardName for cardImages module window.__normalizeCardName = function(raw){ if(!raw) return raw; @@ -390,163 +348,45 @@ try { observer.observe(document.body, { childList:true, subtree:true }); } catch(_){ } })(); - + - + {% if enable_themes %} {% endif %} {% if not enable_themes %} {% endif %} {% if enable_pwa %} {% endif %} - - + + diff --git a/code/web/templates/browse/cards/index.html b/code/web/templates/browse/cards/index.html index ccc9e28..1a4c31a 100644 --- a/code/web/templates/browse/cards/index.html +++ b/code/web/templates/browse/cards/index.html @@ -9,40 +9,41 @@ left: 0; right: 0; z-index: 1000; - background: var(--card-bg, #1a1d24); - border: 1px solid var(--border, #374151); + background: var(--panel); + border: 1px solid var(--border); border-top: none; border-radius: 0 0 6px 6px; max-height: 300px; overflow-y: auto; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + color: var(--text); } .autocomplete-dropdown:empty { display: none; } .autocomplete-item { padding: .75rem; cursor: pointer; - border-bottom: 1px solid rgba(55, 65, 81, 0.5); + border-bottom: 1px solid var(--border); transition: background 0.15s; } .autocomplete-item:last-child { border-bottom: none; } .autocomplete-item:hover, .autocomplete-item:focus, .autocomplete-item.selected { - background: rgba(148, 163, 184, .15); + background: var(--bg); } .autocomplete-item.selected { - background: rgba(148, 163, 184, .25); - border-left: 3px solid var(--ring, #3b82f6); + background: var(--bg); + border-left: 3px solid var(--ring); padding-left: calc(.75rem - 3px); } .autocomplete-empty { padding: .75rem; text-align: center; - color: var(--muted, #9ca3af); + color: var(--muted); font-size: .85rem; } .autocomplete-error { padding: .75rem; text-align: center; - color: #f87171; + color: var(--err); font-size: .85rem; } diff --git a/code/web/templates/build/_alternatives.html b/code/web/templates/build/_alternatives.html index 84bb51b..f2fb4f8 100644 --- a/code/web/templates/build/_alternatives.html +++ b/code/web/templates/build/_alternatives.html @@ -3,7 +3,7 @@ { 'name': display_name, 'name_lower': lower, 'owned': bool, 'tags': list[str] } ] #} -
+
Alternatives diff --git a/code/web/templates/build/_new_deck_additional_themes.html b/code/web/templates/build/_new_deck_additional_themes.html index 7c3dda4..c8180bf 100644 --- a/code/web/templates/build/_new_deck_additional_themes.html +++ b/code/web/templates/build/_new_deck_additional_themes.html @@ -35,7 +35,8 @@ style="display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;">
diff --git a/code/web/templates/commanders/index.html b/code/web/templates/commanders/index.html index 00244f3..83fbb30 100644 --- a/code/web/templates/commanders/index.html +++ b/code/web/templates/commanders/index.html @@ -198,15 +198,15 @@ /* Autocomplete dropdown styles */ .autocomplete-container { position:relative; width:100%; } - .autocomplete-dropdown { position:absolute; top:100%; left:0; right:0; z-index:1000; background:var(--panel); border:1px solid var(--border); border-radius:8px; margin-top:4px; max-height:280px; overflow-y:auto; box-shadow:0 4px 12px rgba(0,0,0,.25); display:none; } + .autocomplete-dropdown { position:absolute; top:100%; left:0; right:0; z-index:1000; background:var(--panel); border:1px solid var(--border); border-radius:8px; margin-top:4px; max-height:280px; overflow-y:auto; box-shadow:0 4px 12px rgba(0,0,0,.25); display:none; color:var(--text); } .autocomplete-dropdown:not(:empty) { display:block; } .autocomplete-item { padding:.5rem .75rem; cursor:pointer; border-bottom:1px solid var(--border); transition:background .15s ease; } .autocomplete-item:last-child { border-bottom:none; } - .autocomplete-item:hover, .autocomplete-item:focus, .autocomplete-item.selected { background:rgba(148,163,184,.15); } - .autocomplete-item.selected { background:rgba(148,163,184,.25); border-left:3px solid var(--ring); padding-left:calc(.75rem - 3px); } + .autocomplete-item:hover, .autocomplete-item:focus, .autocomplete-item.selected { background:var(--bg); } + .autocomplete-item.selected { background:var(--bg); border-left:3px solid var(--ring); padding-left:calc(.75rem - 3px); } .autocomplete-item .tag-count { color:var(--muted); font-size:.85rem; float:right; } .autocomplete-empty { padding:.75rem; text-align:center; color:var(--muted); font-size:.85rem; } - .autocomplete-error { padding:.75rem; text-align:center; color:#f87171; font-size:.85rem; } + .autocomplete-error { padding:.75rem; text-align:center; color:var(--err); font-size:.85rem; }