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 >
2026-03-20 09:03:20 -07:00
< link rel = "stylesheet" href = "/static/styles.css?v=20260319-3" / >
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 >
2025-10-29 15:45:40 -07:00
< div id = "banner-status" class = "banner-status" role = "status" aria-live = "polite" > < / 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.
2026-03-23 16:38:18 -07:00
{% set _pba = _price_cache_ts() %}{% if _pba %}< br > < span class = "muted" style = "font-size:.8em;" > Prices as of {{ _pba }} — for live pricing visit < a href = "https://scryfall.com" target = "_blank" rel = "noopener" > Scryfall< / a > .< / span > {% endif %}
2025-08-26 11:34:42 -07:00
< / 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');
});
2025-10-29 15:45:40 -07:00
// Setup/Tagging status poller moved to app.ts (initSetupStatusPoller)
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 >
2025-10-29 15:45:40 -07:00
<!-- Active nav highlighter moved to app.ts (initActiveNavHighlighter) -->
2025-10-28 16:17:55 -07:00
< script src = "/static/js/components.js?v=20251028-1" > < / script >
2025-10-29 15:45:40 -07:00
< script src = "/static/js/app.js?v=20251029-2" > < / 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 >
2025-10-29 15:45:40 -07:00
// Theme selector logic moved to app.ts (initThemeSelector)
// Call it with server-injected values on DOMContentLoaded
document.addEventListener('DOMContentLoaded', function(){
if (window.__initThemeSelector) {
window.__initThemeSelector({{ enable_themes|lower }}, '{{ default_theme }}');
}
});
2025-08-28 16:44:58 -07:00
< / script >
{% endif %}
{% if not enable_themes %}
< script >
2025-10-29 15:45:40 -07:00
// Theme env-only logic moved to app.ts (initThemeEnvOnly)
// Call it with server-injected values on DOMContentLoaded
document.addEventListener('DOMContentLoaded', function(){
if (window.__initThemeEnvOnly) {
window.__initThemeEnvOnly({{ enable_themes|lower }}, '{{ default_theme }}');
}
});
2025-08-28 16:44:58 -07:00
< / script >
{% endif %}
{% if enable_pwa %}
< script >
2025-10-29 15:45:40 -07:00
// Service worker registration moved to app.ts (initServiceWorker)
// Call it with server-injected values on DOMContentLoaded
document.addEventListener('DOMContentLoaded', function(){
if (window.__initServiceWorker) {
window.__initServiceWorker({{ enable_pwa|lower }}, '{{ catalog_hash|default("dev") }}');
}
});
2025-08-28 16:44:58 -07:00
< / script >
{% endif %}
2025-10-29 15:45:40 -07:00
<!-- Toast after reload, setup poller, nav highlighter moved to app.ts -->
<!-- Hover card panel system moved to cardHover.ts -->
2026-03-23 16:38:18 -07:00
<!-- Price tooltip: lightweight fetch on mouseenter for .card - name - price - hover -->
< script >
(function(){
var _priceCache = {};
var _tip = null;
function _showTip(el, text) {
if (!_tip) {
_tip = document.createElement('div');
_tip.className = 'card-price-tip';
document.body.appendChild(_tip);
}
_tip.textContent = text;
var r = el.getBoundingClientRect();
_tip.style.left = (r.left + r.width/2 + window.scrollX) + 'px';
_tip.style.top = (r.top + window.scrollY - 4) + 'px';
_tip.style.transform = 'translate(-50%, -100%)';
_tip.style.display = 'block';
}
function _hideTip() { if (_tip) _tip.style.display = 'none'; }
document.addEventListener('mouseover', function(e) {
var el = e.target & & e.target.closest & & e.target.closest('.card-name-price-hover');
if (!el) return;
var name = el.dataset.cardName || el.textContent.trim();
if (!name) return;
if (_priceCache[name] !== undefined) {
_showTip(el, _priceCache[name]);
return;
}
_showTip(el, 'Loading price...');
fetch('/api/price/' + encodeURIComponent(name))
.then(function(r){ return r.ok ? r.json() : null; })
.then(function(d){
var label = (d & & d.found & & d.price != null) ? ('$' + parseFloat(d.price).toFixed(2)) : 'Price unavailable';
_priceCache[name] = label;
_showTip(el, label);
})
.catch(function(){ _priceCache[name] = 'Price unavailable'; });
});
document.addEventListener('mouseout', function(e) {
var el = e.target & & e.target.closest & & e.target.closest('.card-name-price-hover');
if (el) _hideTip();
});
})();
< / script >
<!-- Budget price display: injects prices on card tiles and list rows, tracks running total -->
< script >
(function(){
var BASIC_LANDS = new Set([
'Plains','Island','Swamp','Mountain','Forest','Wastes',
'Snow-Covered Plains','Snow-Covered Island','Snow-Covered Swamp',
'Snow-Covered Mountain','Snow-Covered Forest'
]);
var _priceNum = {}; // card name -> float|null
var _deckPrices = {}; // accumulated across build stages: card name -> float
var _buildToken = null;
function _fetchNum(name) {
if (_priceNum.hasOwnProperty(name)) return Promise.resolve(_priceNum[name]);
return fetch('/api/price/' + encodeURIComponent(name))
.then(function(r){ return r.ok ? r.json() : null; })
.then(function(d){
var p = (d & & d.found & & d.price != null) ? parseFloat(d.price) : null;
_priceNum[name] = p; return p;
}).catch(function(){ _priceNum[name] = null; return null; });
}
function _getBuildToken() {
var el = document.querySelector('[data-build-id]');
return el ? el.getAttribute('data-build-id') : null;
}
function _cfg() { return window._budgetCfg || null; }
function initPriceDisplay() {
var tok = _getBuildToken();
if (tok !== null & & tok !== _buildToken) { _buildToken = tok; _deckPrices = {}; }
var cfg = _cfg();
var ceiling = cfg & & cfg.card_ceiling ? parseFloat(cfg.card_ceiling) : null;
var totalCap = cfg & & cfg.total ? parseFloat(cfg.total) : null;
function updateRunningTotal(prevTotal) {
var chip = document.getElementById('budget-running');
if (!chip) return;
var total = Object.values(_deckPrices).reduce(function(s,p){ return s + (p||0); }, 0);
chip.textContent = total.toFixed(2);
var chipWrap = chip.closest('.chip');
if (chipWrap & & totalCap !== null) chipWrap.classList.toggle('chip-warn', total > totalCap);
if (prevTotal !== undefined) {
var stepAdded = total - prevTotal;
var stepEl = document.getElementById('budget-step');
if (stepEl & & stepAdded > 0.005) {
stepEl.textContent = '+$' + stepAdded.toFixed(2) + ' this step';
stepEl.style.display = '';
}
}
}
var overlays = document.querySelectorAll('.card-price-overlay[data-price-for]');
var inlines = document.querySelectorAll('.card-price-inline[data-price-for]');
var toFetch = new Set();
overlays.forEach(function(el){ var n = el.dataset.priceFor; if (n & & !BASIC_LANDS.has(n)) toFetch.add(n); });
inlines.forEach(function(el){ var n = el.dataset.priceFor; if (n & & !BASIC_LANDS.has(n)) toFetch.add(n); });
// Always refresh the running total chip even when there's nothing new to fetch
updateRunningTotal();
if (!toFetch.size) return;
var prevTotal = Object.values(_deckPrices).reduce(function(s,p){ return s + (p||0); }, 0);
var promises = [];
toFetch.forEach(function(name){ promises.push(_fetchNum(name).then(function(p){ return {name:name,price:p}; })); });
Promise.all(promises).then(function(results){
var map = {};
var prevTotal2 = Object.values(_deckPrices).reduce(function(s,p){ return s + (p||0); }, 0);
results.forEach(function(r){ map[r.name] = r.price; if (r.price !== null) _deckPrices[r.name] = r.price; });
overlays.forEach(function(el){
var name = el.dataset.priceFor;
if (!name || BASIC_LANDS.has(name)) { el.style.display='none'; return; }
var p = map[name];
el.textContent = p !== null ? ('$' + p.toFixed(2)) : '';
if (ceiling !== null & & p !== null & & p > ceiling) {
var tile = el.closest('.card-tile,.stack-card');
if (tile) tile.classList.add('over-budget');
}
});
inlines.forEach(function(el){
var name = el.dataset.priceFor;
if (!name || BASIC_LANDS.has(name)) { el.style.display='none'; return; }
var p = map[name];
el.textContent = p !== null ? ('$' + p.toFixed(2)) : '';
if (ceiling !== null & & p !== null & & p > ceiling) {
var row = el.closest('.list-row');
if (row) row.classList.add('over-budget');
}
});
// Update running total chip with per-step delta
updateRunningTotal(prevTotal2);
// Update summary budget bar
var bar = document.getElementById('budget-summary-bar');
if (bar) {
var allNames = new Set();
var sumTotal = 0;
document.querySelectorAll('.card-price-overlay[data-price-for],.card-price-inline[data-price-for]').forEach(function(el){
var n = el.dataset.priceFor;
if (n & & !BASIC_LANDS.has(n) & & !allNames.has(n)) {
allNames.add(n);
sumTotal += (map[n] || 0);
}
});
if (totalCap !== null) {
var over = sumTotal > totalCap;
bar.textContent = 'Estimated deck cost: $' + sumTotal.toFixed(2) + ' / $' + totalCap.toFixed(2) + (over ? ' — over budget' : ' — under budget');
bar.className = over ? 'budget-price-bar over' : 'budget-price-bar under';
} else {
bar.textContent = 'Estimated deck cost: $' + sumTotal.toFixed(2) + ' (basic lands excluded)';
bar.className = 'budget-price-bar';
}
}
});
}
document.addEventListener('DOMContentLoaded', function(){ initPriceDisplay(); });
document.addEventListener('htmx:afterSwap', function(){ setTimeout(initPriceDisplay, 80); });
})();
< / 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 >