Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
<!doctype html>
2025-08-28 16:44:58 -07:00
< html lang = "en" data-theme = "{% if default_theme == 'light' %}light-blend{% elif default_theme == 'dark' %}dark{% else %}light-blend{% endif %}" >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< head >
< meta charset = "utf-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1" / >
2025-09-30 15:49:08 -07:00
< meta http-equiv = "Content-Security-Policy" content = "upgrade-insecure-requests" / >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< title > MTG Deckbuilder< / title >
< script src = "https://unpkg.com/htmx.org@1.9.12" onerror = "var s=document.createElement('script');s.src='/static/vendor/htmx-1.9.12.min.js';document.head.appendChild(s);" > < / script >
2025-08-28 16:44:58 -07:00
< script >
(function(){
// Pre-CSS theme bootstrapping to avoid flash/mismatch on first paint
try{
var root = document.documentElement;
var KEY = 'mtg:theme';
var SERVER_DEFAULT = '{{ default_theme }}';
var params = new URLSearchParams(window.location.search || '');
var urlTheme = (params.get('theme') || '').toLowerCase();
var stored = localStorage.getItem(KEY);
function resolveSystem(){
var prefersDark = window.matchMedia & & window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? 'dark' : 'light-blend';
}
function mapTheme(v){
var x = (v || 'system').toLowerCase();
if (x === 'system') return resolveSystem();
if (x === 'light') return 'light-blend';
return x;
}
var initial = urlTheme || ((stored & & stored.trim()) ? stored : (SERVER_DEFAULT || 'system'));
root.setAttribute('data-theme', mapTheme(initial));
}catch(_){ }
})();
< / script >
2025-10-07 15:56:57 -07:00
< script >
window.__telemetryEndpoint = '/telemetry/events';
< / script >
2025-09-11 14:54:35 -07:00
< link rel = "stylesheet" href = "/static/styles.css?v=20250911-1" / >
2025-10-28 08:21:52 -07:00
< link rel = "stylesheet" href = "/static/shared-components.css?v=20251021-1" / >
2025-10-17 18:40:15 -07:00
< style >
/* Disable all transitions until page is loaded to prevent sidebar flash */
.no-transition,
.no-transition *,
.no-transition *::before,
.no-transition *::after {
transition: none !important;
animation: none !important;
}
< / style >
2025-08-28 14:57:22 -07:00
<!-- Performance hints -->
< link rel = "preconnect" href = "https://api.scryfall.com" crossorigin >
< link rel = "dns-prefetch" href = "https://api.scryfall.com" >
2025-08-26 20:00:07 -07:00
<!-- Favicon -->
< link rel = "icon" type = "image/png" href = "/static/favicon.png" / >
< link rel = "shortcut icon" href = "/favicon.ico" / >
< link rel = "apple-touch-icon" href = "/static/favicon.png" / >
2025-08-28 16:44:58 -07:00
{% if enable_pwa %}
< link rel = "manifest" href = "/static/manifest.webmanifest" / >
{% endif %}
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< / head >
2025-10-17 18:40:15 -07:00
< body class = "no-transition" data-diag = "{% if show_diagnostics %}1{% else %}0{% endif %}" data-virt = "{% if virtualize %}1{% else %}0{% endif %}" >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< header class = "top-banner" >
< div class = "top-inner" >
2025-10-28 08:21:52 -07:00
< div class = "flex-row banner-left" >
< button type = "button" id = "nav-toggle" class = "btn" aria-controls = "sidebar" aria-expanded = "true" title = "Show/Hide navigation" style = "background: transparent; color: var(--surface-banner-text); border:1px solid var(--border); flex-shrink: 0;" >
2025-09-02 16:03:12 -07:00
☰ Menu
< / button >
2025-10-28 08:21:52 -07:00
< h1 style = "margin:0; white-space: nowrap;" > MTG Deckbuilder< / h1 >
Web UI polish: thumbnail-hover preview, white thumbnail selection, Themes bullet list; global Scryfall image retry (thumbs+previews) with fallbacks and cache-bust; standardized data-card-name. Deck Summary alignment overhaul (count//name/owned grid, tabular numerals, inset highlight, tooltips, starts under header). Added diagnostics (health + logs pages, error pages, request-id propagation), global HTMX error toasts, and docs updates. Update DOCKER guide and add run-web scripts. Update CHANGELOG and release notes template.
2025-08-27 11:21:46 -07:00
< / div >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< / div >
< / header >
< div class = "layout" >
2025-09-02 16:03:12 -07:00
< aside id = "sidebar" class = "sidebar" aria-label = "Primary navigation" >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< div class = "brand" >
< div class = "mana-dots" aria-hidden = "true" >
< span class = "dot green" > < / span >
< span class = "dot blue" > < / span >
< span class = "dot red" > < / span >
< span class = "dot white" > < / span >
< span class = "dot black" > < / span >
< / div >
< / div >
2025-09-23 09:19:23 -07:00
< nav class = "nav" id = "primary-nav" >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< a href = "/" > Home< / a >
< a href = "/build" > Build< / a >
< a href = "/configs" > Build from JSON< / a >
{% if show_setup %}< a href = "/setup" > Setup/Tag< / a > {% endif %}
2025-08-26 16:25:34 -07:00
< a href = "/owned" > Owned Library< / a >
2025-10-16 19:02:33 -07:00
< a href = "/cards" > All Cards< / a >
2025-09-30 15:49:08 -07:00
{% if show_commanders %}< a href = "/commanders" > Commanders< / a > {% endif %}
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< a href = "/decks" > Finished Decks< / a >
2025-09-23 09:19:23 -07:00
< a href = "/themes/" > Themes< / a >
2025-09-25 15:14:15 -07:00
{% if random_ui %}< a href = "/random" > Random< / a > {% endif %}
Web UI polish: thumbnail-hover preview, white thumbnail selection, Themes bullet list; global Scryfall image retry (thumbs+previews) with fallbacks and cache-bust; standardized data-card-name. Deck Summary alignment overhaul (count//name/owned grid, tabular numerals, inset highlight, tooltips, starts under header). Added diagnostics (health + logs pages, error pages, request-id propagation), global HTMX error toasts, and docs updates. Update DOCKER guide and add run-web scripts. Update CHANGELOG and release notes template.
2025-08-27 11:21:46 -07:00
{% if show_diagnostics %}< a href = "/diagnostics" > Diagnostics< / a > {% endif %}
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
{% if show_logs %}< a href = "/logs" > Logs< / a > {% endif %}
< / nav >
2025-09-11 14:54:35 -07:00
{% if enable_themes %}
< div class = "sidebar-theme" role = "group" aria-label = "Theme" >
< label class = "sidebar-theme-label" for = "theme-select" > Theme< / label >
< div class = "sidebar-theme-row" >
< select id = "theme-select" aria-label = "Theme selector" >
< option value = "system" > System< / option >
< option value = "light" > Light< / option >
< option value = "dark" > Dark< / option >
< option value = "high-contrast" > High contrast< / option >
< option value = "cb-friendly" > Color-blind< / option >
< / select >
< button type = "button" id = "theme-reset" class = "btn btn-ghost" title = "Reset theme preference" > Reset< / button >
< / div >
< / div >
{% endif %}
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< / aside >
Web UI polish: thumbnail-hover preview, white thumbnail selection, Themes bullet list; global Scryfall image retry (thumbs+previews) with fallbacks and cache-bust; standardized data-card-name. Deck Summary alignment overhaul (count//name/owned grid, tabular numerals, inset highlight, tooltips, starts under header). Added diagnostics (health + logs pages, error pages, request-id propagation), global HTMX error toasts, and docs updates. Update DOCKER guide and add run-web scripts. Update CHANGELOG and release notes template.
2025-08-27 11:21:46 -07:00
< main class = "content" data-error-surface >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
{% block content %}{% endblock %}
< / main >
< / div >
2025-08-26 11:34:42 -07:00
< footer class = "site-footer" role = "contentinfo" >
Card images and data provided by
< a href = "https://scryfall.com" target = "_blank" rel = "noopener" > Scryfall< / a > .
This website is not produced by, endorsed by, supported by, or affiliated with Scryfall or Wizards of the Coast.
< / footer >
2025-10-28 08:21:52 -07:00
<!-- Card hover, theme badges, and DFC toggle styles moved to tailwind.css 2025 - 10 - 21 -->
2025-09-23 09:19:23 -07:00
< style >
.nav a.active { font-weight:600; position:relative; }
.nav a.active::after { content:''; position:absolute; left:0; bottom:2px; width:100%; height:2px; background:var(--accent, #38bdf8); border-radius:2px; }
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< / style >
< script >
(function(){
2025-09-02 16:03:12 -07:00
// Sidebar toggle and persistence
try{
var BODY = document.body;
var SIDEBAR = document.getElementById('sidebar');
var TOGGLE = document.getElementById('nav-toggle');
var KEY = 'mtg:navCollapsed';
2025-10-17 18:40:15 -07:00
2025-09-02 16:03:12 -07:00
function apply(collapsed){
if (collapsed){
BODY.classList.add('nav-collapsed');
TOGGLE & & TOGGLE.setAttribute('aria-expanded', 'false');
SIDEBAR & & SIDEBAR.setAttribute('aria-hidden', 'true');
} else {
BODY.classList.remove('nav-collapsed');
TOGGLE & & TOGGLE.setAttribute('aria-expanded', 'true');
SIDEBAR & & SIDEBAR.setAttribute('aria-hidden', 'false');
}
}
// Initial state: respect saved pref, else collapse on small screens
var saved = localStorage.getItem(KEY);
var initialCollapsed = (saved === '1') || (saved === null & & (window.innerWidth || 0) < 900 ) ;
apply(initialCollapsed);
2025-10-17 18:40:15 -07:00
// Re-enable transitions after page is fully loaded
// Use longer delay for pages with heavy content (like card browser)
var enableTransitions = function(){
BODY.classList.remove('no-transition');
};
if (document.readyState === 'complete') {
// Already loaded
setTimeout(enableTransitions, 150);
} else {
window.addEventListener('load', function(){
setTimeout(enableTransitions, 150);
});
}
2025-09-02 16:03:12 -07:00
if (TOGGLE){
TOGGLE.addEventListener('click', function(){
var isCollapsed = BODY.classList.contains('nav-collapsed');
apply(!isCollapsed);
try{ localStorage.setItem(KEY, (!isCollapsed) ? '1' : '0'); }catch(_){ }
});
}
// Keep ARIA in sync on resize for first-load default when no pref yet
window.addEventListener('resize', function(){
// Do not override if user has an explicit preference saved
if (localStorage.getItem(KEY) !== null) return;
apply((window.innerWidth || 0) < 900 ) ;
});
}catch(_){ }
2025-10-17 18:40:15 -07:00
// Suppress sidebar transitions during HTMX partial updates (not full page loads)
document.addEventListener('htmx:beforeRequest', function(evt){
// Only suppress for small partial updates (identified by specific IDs)
var target = evt.detail & & evt.detail.target;
if (target & & target.id) {
var targetId = target.id;
// List of partial update containers that should suppress sidebar transitions
var partialContainers = ['similar-cards-container', 'card-list', 'theme-list'];
if (partialContainers.indexOf(targetId) !== -1 || targetId.indexOf('-container') !== -1) {
document.body.classList.add('htmx-settling');
}
}
});
document.addEventListener('htmx:afterSettle', function(){
document.body.classList.remove('htmx-settling');
});
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
// Setup/Tagging status poller
var statusEl;
function ensureStatusEl(){
if (!statusEl) statusEl = document.getElementById('banner-status');
return statusEl;
}
function renderSetupStatus(data){
feat(editorial): Phase D synergy commander enrichment, augmentation, lint & docs\n\nAdds Phase D editorial tooling: synergy-based commander selection with 3/2/1 pattern, duplicate filtering, annotated synergy_commanders, promotion to minimum examples, and augmentation heuristics (e.g. Counters Matter/Proliferate injection). Includes new scripts (generate_theme_editorial_suggestions, lint, validate, catalog build/apply), updates orchestrator & web routes, expands CI workflow, and documents usage & non-determinism policies. Updates lint rules, type definitions, and docker configs.
2025-09-18 10:59:20 -07:00
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;
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
}
feat(editorial): Phase D synergy commander enrichment, augmentation, lint & docs\n\nAdds Phase D editorial tooling: synergy-based commander selection with 3/2/1 pattern, duplicate filtering, annotated synergy_commanders, promotion to minimum examples, and augmentation heuristics (e.g. Counters Matter/Proliferate injection). Includes new scripts (generate_theme_editorial_suggestions, lint, validate, catalog build/apply), updates orchestrator & web routes, expands CI workflow, and documents usage & non-determinism policies. Updates lint rules, type definitions, and docker configs.
2025-09-18 10:59:20 -07:00
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');
}
}
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
function pollStatus(){
try {
fetch('/status/setup', { cache: 'no-store' })
.then(function(r){ return r.json(); })
.then(renderSetupStatus)
.catch(function(){ /* noop */ });
} catch(e) {}
}
2025-10-08 20:59:51 -07:00
// Poll every 10 seconds instead of 3 to reduce server load (only for header indicator)
setInterval(pollStatus, 10000);
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
pollStatus();
2025-10-29 10:44:29 -07:00
// Expose normalizeCardName for cardImages module
window.__normalizeCardName = function(raw){
2025-09-24 13:57:23 -07:00
if(!raw) return raw;
2025-10-29 10:44:29 -07:00
var m = /(.*?)(\s*-\s*Synergy\s*\(.*\))$/i.exec(raw);
if(m){ return m[1].trim(); }
2025-09-24 13:57:23 -07:00
return raw;
2025-10-29 10:44:29 -07:00
};
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
})();
< / script >
2025-09-23 09:19:23 -07:00
< script >
// Overlay flip button + persistence + accessibility for double-faced cards
(function(){
var FACE_ATTR = 'data-current-face';
var LS_PREFIX = 'mtg:face:';
var DEBOUNCE_MS = 120; // prevent rapid flip spamming / extra fetches
var lastFlip = 0;
2025-09-29 21:32:08 -07:00
var normalize = (window.__normalizeCardName) ? window.__normalizeCardName : function(raw){
if(!raw) return raw;
var m = /(.*?)(\s*-\s*Synergy\s*\(.*\))$/i.exec(raw);
if(m){ return m[1].trim(); }
return raw;
};
window.__normalizeCardName = normalize;
function getCardData(card, attr){
if(!card) return '';
var val = card.getAttribute(attr);
if(val) return val;
var node = card.querySelector & & card.querySelector('['+attr+']');
return node ? node.getAttribute(attr) : '';
}
2025-09-23 09:19:23 -07:00
function hasTwoFaces(card){
if(!card) return false;
2025-10-28 08:21:52 -07:00
// Check if card has a layout attribute - this is the source of truth
var layout = card.getAttribute('data-layout') || '';
if(layout) {
// Only these layouts are actual flippable double-faced cards
var flippableLayouts = ['modal_dfc', 'transform', 'reversible_card', 'flip', 'meld'];
return flippableLayouts.indexOf(layout) > -1;
}
// Fallback: If no layout data, check if name has // (backwards compatibility)
// This shouldn't happen if templates properly pass data-layout
2025-09-29 21:32:08 -07:00
var name = normalize(getCardData(card, 'data-card-name')) + ' ' + normalize(getCardData(card, 'data-original-name'));
2025-09-23 09:19:23 -07:00
return name.indexOf('//') > -1;
}
2025-10-28 08:21:52 -07:00
window.__dfcHasTwoFaces = hasTwoFaces; // Expose globally for popup hover panel
2025-09-23 09:19:23 -07:00
function keyFor(card){
2025-09-29 21:32:08 -07:00
var nm = normalize(getCardData(card, 'data-card-name') || getCardData(card, 'data-original-name') || '').toLowerCase();
2025-09-23 09:19:23 -07:00
return LS_PREFIX + nm;
}
function applyStoredFace(card){
try {
var k = keyFor(card);
var val = localStorage.getItem(k);
if(val === 'front' || val === 'back') card.setAttribute(FACE_ATTR, val);
} catch(_){}
}
function storeFace(card, face){
try { localStorage.setItem(keyFor(card), face); } catch(_){}
}
function announce(face, card){
var live = document.getElementById('dfc-live');
if(!live){
live = document.createElement('div');
live.id = 'dfc-live'; live.className='sr-only'; live.setAttribute('aria-live','polite');
document.body.appendChild(live);
}
2025-09-29 21:32:08 -07:00
var nm = normalize(getCardData(card, 'data-card-name')||'').split('//')[0].trim();
2025-09-23 09:19:23 -07:00
live.textContent = 'Showing ' + (face==='front'?'front face':'back face') + ' of ' + nm;
}
function updateButton(btn, face){
btn.setAttribute('data-face', face);
btn.setAttribute('aria-label', face==='front' ? 'Flip to back face' : 'Flip to front face');
btn.innerHTML = '< span class = "icon" aria-hidden = "true" style = "font-size:18px;" > ⥮< / span > ';
}
2025-09-29 21:32:08 -07:00
window.__dfcUpdateButton = updateButton;
2025-09-23 09:19:23 -07:00
function ensureButton(card){
if(!hasTwoFaces(card)) return;
if(card.querySelector('.dfc-toggle')) return;
card.classList.add('dfc-host');
2025-09-29 21:32:08 -07:00
var resolvedName = getCardData(card, 'data-card-name');
var resolvedOriginal = getCardData(card, 'data-original-name');
if(resolvedName & & !card.hasAttribute('data-card-name')) card.setAttribute('data-card-name', resolvedName);
if(resolvedOriginal & & !card.hasAttribute('data-original-name')) card.setAttribute('data-original-name', resolvedOriginal);
2025-09-23 09:19:23 -07:00
applyStoredFace(card);
var face = card.getAttribute(FACE_ATTR) || 'front';
var btn = document.createElement('button');
btn.type='button';
2025-10-28 08:21:52 -07:00
// Mobile: flip in popup only (flex below md). Desktop: flip in thumbnails only (hidden at md+)
var inPopup = card.closest & & card.closest('#hover-card-panel');
btn.className = inPopup ? 'dfc-toggle flex md:hidden' : 'dfc-toggle hidden md:flex';
2025-09-23 09:19:23 -07:00
btn.setAttribute('aria-pressed','false');
btn.setAttribute('tabindex','0');
btn.addEventListener('click', function(ev){ ev.stopPropagation(); flip(card, btn); });
btn.addEventListener('keydown', function(ev){ if(ev.key==='Enter' || ev.key===' ' || ev.key==='f' || ev.key==='F'){ ev.preventDefault(); flip(card, btn); }});
updateButton(btn, face);
2025-09-29 21:32:08 -07:00
if(card.classList.contains('list-row')){
btn.classList.add('dfc-toggle-inline');
var slot = card.querySelector('.flip-slot');
if(slot){
slot.innerHTML='';
slot.appendChild(btn);
slot.removeAttribute('aria-hidden');
} else {
var anchor = card.querySelector('.dfc-anchor');
if(anchor){ anchor.insertAdjacentElement('afterend', btn); }
else if(card.lastElementChild){ card.insertBefore(btn, card.lastElementChild); }
else { card.appendChild(btn); }
}
} else {
card.insertBefore(btn, card.firstChild);
}
2025-09-23 09:19:23 -07:00
}
2025-10-28 08:21:52 -07:00
window.__dfcEnsureButton = ensureButton; // Expose for hover panel use
2025-09-23 09:19:23 -07:00
function flip(card, btn){
var now = Date.now();
if(now - lastFlip < DEBOUNCE_MS ) return ;
lastFlip = now;
var cur = card.getAttribute(FACE_ATTR) || 'front';
var next = cur === 'front' ? 'back' : 'front';
card.setAttribute(FACE_ATTR, next);
storeFace(card, next);
if(btn) updateButton(btn, next);
// visual cue
card.style.outline='2px solid var(--accent)'; setTimeout(function(){ card.style.outline=''; }, 160);
announce(next, card);
// retrigger hover update under pointer if applicable
if(window.__hoverShowCard){ window.__hoverShowCard(card); }
2025-09-29 21:32:08 -07:00
if(window.__dfcNotifyHover){ try{ window.__dfcNotifyHover(card, next); }catch(_){ } }
2025-09-23 09:19:23 -07:00
}
2025-09-29 21:32:08 -07:00
window.__dfcFlipCard = function(card){ if(!card) return; flip(card, card.querySelector('.dfc-toggle')); };
window.__dfcGetFace = function(card){ if(!card) return 'front'; return card.getAttribute(FACE_ATTR) || 'front'; };
2025-09-23 09:19:23 -07:00
function scan(){
2025-10-06 09:17:59 -07:00
document.querySelectorAll('.card-sample, .commander-cell, .commander-thumb, .commander-card, .card-tile, .candidate-tile, .stack-card, .card-preview, .owned-row, .list-row').forEach(ensureButton);
2025-09-23 09:19:23 -07:00
}
document.addEventListener('pointermove', function(e){ window.__lastPointerEvent = e; }, { passive:true });
document.addEventListener('DOMContentLoaded', scan);
document.addEventListener('htmx:afterSwap', scan);
// Expose for debugging
window.__dfcScan = scan;
// MutationObserver to re-inject buttons if card tiles are replaced (e.g., HTMX swaps, dynamic filtering)
var moDebounce = null;
var observer = new MutationObserver(function(muts){
if(moDebounce) cancelAnimationFrame(moDebounce);
moDebounce = requestAnimationFrame(function(){ scan(); });
});
try { observer.observe(document.body, { childList:true, subtree:true }); } catch(_){ }
})();
< / script >
< script >
(function(){
try {
var path = window.location.pathname || '/';
var nav = document.getElementById('primary-nav'); if(!nav) return;
var links = nav.querySelectorAll('a');
var best = null; var bestLen = -1;
links.forEach(function(a){
var 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; bestLen = href.length; }
}
});
if(best) best.classList.add('active');
} catch(_) {}
})();
< / script >
2025-10-28 16:17:55 -07:00
< script src = "/static/js/components.js?v=20251028-1" > < / script >
< script src = "/static/js/app.js?v=20250826-4" > < / script >
2025-10-29 10:44:29 -07:00
< script src = "/static/js/cardImages.js?v=20251029-1" > < / script >
< script src = "/static/js/cardHover.js?v=20251028-1" > < / script >
2025-08-28 16:44:58 -07:00
{% if enable_themes %}
< script >
(function(){
try{
var sel = document.getElementById('theme-select');
var resetBtn = document.getElementById('theme-reset');
var root = document.documentElement;
var KEY = 'mtg:theme';
var SERVER_DEFAULT = '{{ default_theme }}';
function mapLight(v){ return v === 'light' ? 'light-blend' : v; }
function resolveSystem(){
var prefersDark = window.matchMedia & & window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? 'dark' : 'light-blend';
}
function normalizeUiValue(v){
var x = (v||'system').toLowerCase();
if (x === 'light-blend' || x === 'light-slate' || x === 'light-parchment') return 'light';
return x;
}
function apply(val){
var 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
var params = new URLSearchParams(window.location.search || '');
var 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 { var 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
var stored = localStorage.getItem(KEY);
var initial = urlTheme || ((stored & & stored.trim()) ? stored : (SERVER_DEFAULT || 'system'));
apply(initial);
if (sel){
sel.value = normalizeUiValue(initial);
sel.addEventListener('change', function(){
var v = sel.value || 'system';
localStorage.setItem(KEY, v);
apply(v);
});
}
if (resetBtn){
resetBtn.addEventListener('click', function(){
try{ localStorage.removeItem(KEY); }catch(_){ }
var v = SERVER_DEFAULT || 'system';
apply(v);
if (sel) sel.value = normalizeUiValue(v);
});
}
// React to system changes when set to system
if (window.matchMedia){
var mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener & & mq.addEventListener('change', function(){
var cur = localStorage.getItem(KEY) || (SERVER_DEFAULT || 'system');
if (cur === 'system') apply('system');
});
}
}catch(_){ }
})();
< / script >
{% endif %}
{% if not enable_themes %}
< script >
(function(){
try{
// Apply THEME env even when the selector is disabled. Resolve 'system' to OS preference.
var root = document.documentElement;
var SERVER_DEFAULT = '{{ default_theme }}';
function resolveSystem(){
var prefersDark = window.matchMedia & & window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? 'dark' : 'light-blend';
}
var 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){
var mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener & & mq.addEventListener('change', function(){ root.setAttribute('data-theme', resolveSystem()); });
}
}catch(_){ }
})();
< / script >
{% endif %}
{% if enable_pwa %}
< script >
(function(){
try{
if ('serviceWorker' in navigator){
2025-09-24 13:57:23 -07:00
var ver = '{{ catalog_hash|default("dev") }}';
var url = '/static/sw.js?v=' + encodeURIComponent(ver);
navigator.serviceWorker.register(url).then(function(reg){
window.__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 {
var 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(_){ }
});
2025-08-28 16:44:58 -07:00
}).catch(function(){ window.__pwaStatus = { registered: false }; });
}
}catch(_){ }
})();
< / script >
{% endif %}
2025-08-26 20:00:07 -07:00
< script >
// Show pending toast after full page reloads when actions replace the whole document
(function(){
try{
var raw = sessionStorage.getItem('mtg:toastAfterReload');
if (raw){
sessionStorage.removeItem('mtg:toastAfterReload');
var data = JSON.parse(raw);
if (data & & data.msg){ window.toast & & window.toast(data.msg, data.type||''); }
}
}catch(_){ }
})();
< / script >
2025-09-23 09:19:23 -07:00
< script >
2025-10-29 10:44:29 -07:00
<!-- Hover card panel system moved to TypeScript: code/web/static/ts/cardHover.ts -->
2025-09-23 09:19:23 -07:00
< / script >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< / body >
< / html >