mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-02-04 23:51:50 +01:00
refactor(web): finished JavaScript consolidation, tested JavaScript items, refined themes and color palettes, tested all themes and palettes, ensured all interactive lements use theme-aware css
This commit is contained in:
parent
9379732eec
commit
3c45a31aa3
19 changed files with 498 additions and 632 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = '<strong>Setup/Tagging:</strong> ' + msg + ' <a href="/setup/running" style="margin-left:.5rem;">View progress</a>';
|
||||
el.classList.add('busy');
|
||||
} else if (data && data.phase === 'done') {
|
||||
el.innerHTML = '';
|
||||
el.classList.remove('busy');
|
||||
} else if (data && data.phase === 'error') {
|
||||
el.innerHTML = '<span class="error">Setup error.</span>';
|
||||
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;
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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 = '' +
|
||||
'<div class="hcp-header" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;gap:6px;">' +
|
||||
'<div class="hcp-name" style="font-weight:600;font-size:16px;flex:1;padding-right:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"> </div>' +
|
||||
|
|
@ -126,11 +126,11 @@ interface PointerEventLike {
|
|||
'</div>' +
|
||||
'<div class="hcp-body">' +
|
||||
'<div class="hcp-img-wrap" style="text-align:center;display:flex;flex-direction:column;gap:12px;">' +
|
||||
'<img class="hcp-img" alt="Card image" style="max-width:320px;width:100%;height:auto;border-radius:10px;border:1px solid #475569;background:#0b0d12;opacity:1;" />' +
|
||||
'<img class="hcp-img" alt="Card image" style="max-width:320px;width:100%;height:auto;border-radius:10px;border:1px solid var(--border);background:var(--bg);opacity:1;" />' +
|
||||
'</div>' +
|
||||
'<div class="hcp-right" style="display:flex;flex-direction:column;min-width:0;">' +
|
||||
'<div style="display:flex;align-items:center;gap:6px;margin:0 0 4px;flex-wrap:wrap;">' +
|
||||
'<div class="hcp-role" style="display:inline-block;padding:3px 8px;font-size:11px;letter-spacing:.65px;border:1px solid #475569;border-radius:12px;background:#243044;text-transform:uppercase;"> </div>' +
|
||||
'<div class="hcp-role" style="display:inline-block;padding:3px 8px;font-size:11px;letter-spacing:.65px;border:1px solid var(--border);border-radius:12px;background:var(--bg);text-transform:uppercase;"> </div>' +
|
||||
'<div class="hcp-overlaps" style="flex:1;min-height:14px;"></div>' +
|
||||
'</div>' +
|
||||
'<ul class="hcp-taglist" aria-label="Themes"></ul>' +
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue