mtg_python_deckbuilder/code/web/templates/themes/picker.html

413 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html' %}
{% block content %}
<h2 style="position:relative;">Theme Catalog <small id="theme-stale-indicator" style="display:none; font-size:12px; color:#b45309;">(Refreshing…)</small></h2>
<div id="theme-picker" class="theme-picker" hx-get="/themes/fragment/list?limit=20&offset=0" hx-trigger="load" hx-target="#theme-results" hx-swap="innerHTML" role="region" aria-label="Theme picker">
<div class="theme-picker-controls">
<input type="text" id="theme-search" placeholder="Search themes or synergies" aria-label="Search"
hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="debouncedinput change" name="q"
data-hx-debounce="260" data-hx-debounce-events="input,keyup" data-hx-debounce-flush="blur" data-hx-debounce-group="theme-search" />
<select id="theme-archetype" name="archetype" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change">
<option value="">All Archetypes</option>
{% if archetypes %}{% for a in archetypes %}<option value="{{ a }}">{{ a }}</option>{% endfor %}{% endif %}
</select>
<select id="theme-bucket" name="bucket" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change">
<option value="">All Popularity</option>
<option>Very Common</option>
<option>Common</option>
<option>Uncommon</option>
<option>Niche</option>
<option>Rare</option>
</select>
<select id="theme-limit" name="limit" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change" title="Themes per page">
<option value="20" selected>20</option>
<option value="30">30</option>
<option value="50">50</option>
<option value="75">75</option>
<option value="100">100</option>
</select>
<label title="Show full synergy list (diagnostics only)"><input type="checkbox" id="synergy-full" name="synergy_mode" value="full" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change"/> Full Synergies</label>
<label title="Search input responsiveness experiment">
Mode:
<select id="search-mode">
<option value="throttle" selected>Throttle 250ms</option>
<option value="debounce">Debounce 250ms</option>
</select>
</label>
<div class="color-filters" role="group" aria-label="Filter by primary/secondary color">
{% for c in ['W','U','B','R','G'] %}
<label><input type="checkbox" name="colors" value="{{ c }}"
hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change"/> {{ c }}</label>
{% endfor %}
</div>
{% if theme_picker_diagnostics %}
<label title="Show diagnostics-only badges"><input type="checkbox" id="diag-toggle" name="diagnostics" value="1" hx-get="/themes/fragment/list" hx-target="#theme-results" hx-trigger="change"/> Diagnostics</label>
{% endif %}
</div>
<div id="filter-chips" class="filter-chips" aria-label="Active filters" style="margin-bottom:.25rem;"></div>
<div id="theme-results" class="theme-results preload-hide" aria-live="polite" aria-busy="true" role="listbox" aria-label="Loading themes">
<div class="skeleton-table" aria-hidden="true">
{% for i in range(6) %}
<div class="skeleton-row skeleton-align">
<div class="sk-cell sk-col sk-theme"></div>
<div class="sk-cell sk-col sk-arch"></div>
<div class="sk-cell sk-col sk-pop"></div>
<div class="sk-cell sk-col sk-colors"></div>
<div class="sk-cell sk-col sk-cnt"></div>
<div class="sk-cell sk-col sk-synergies"></div>
</div>
{% endfor %}
</div>
</div>
<template id="theme-row-template"></template>
<div class="legend" style="margin-top:1rem; font-size:12px; line-height:1.3;">
<strong>Legend:</strong>
<span class="theme-badge badge-enforced" title="Enforced synergy (whitelist governance)">ENF</span>
<span class="theme-badge badge-curated" title="Curated synergy (hand authored)">CUR</span>
<span class="theme-badge badge-inferred" title="Inferred synergy (analytics)">INF</span>
<span class="theme-badge badge-pop-vc" title="Popularity: Very Common">VC</span>
<span class="theme-badge badge-pop-c" title="Popularity: Common">C</span>
<span class="theme-badge badge-pop-u" title="Popularity: Uncommon">U</span>
<span class="theme-badge badge-pop-n" title="Popularity: Niche">N</span>
<span class="theme-badge badge-pop-r" title="Popularity: Rare">R</span>
{% if theme_picker_diagnostics %}
<span class="theme-badge badge-fallback" title="Generic fallback description"></span>
<span class="theme-badge badge-quality-draft" title="Editorial quality: draft">D</span>
<span class="theme-badge badge-quality-reviewed" title="Editorial quality: reviewed">R</span>
<span class="theme-badge badge-quality-final" title="Editorial quality: final">F</span>
{% endif %}
</div>
<div id="theme-preview-host"></div>
</div>
<style>
.theme-picker-controls { display:flex; flex-wrap:wrap; gap:.5rem; margin-bottom:.75rem; }
.theme-results table { width:100%; border-collapse: collapse; }
.theme-results th, .theme-results td { padding:.35rem .5rem; border-bottom:1px solid var(--border); font-size:13px; }
.theme-results tr:hover { background: var(--hover); cursor:pointer; }
:root { --focus:#6366f1; }
@media (prefers-contrast: more){ :root { --focus:#ff9800; } }
.theme-row.is-active { outline:2px solid var(--focus); outline-offset:-2px; background:var(--hover); }
/* Long theme name truncation */
.theme-results td:first-child { max-width:260px; }
.theme-results td:first-child span.trunc-name { display:inline-block; max-width:240px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; vertical-align:bottom; }
/* Badge wrapping heuristics */
.theme-synergies .theme-badge { max-width:120px; overflow:hidden; text-overflow:ellipsis; }
.theme-synergies { font-size:11px; opacity:.85; display:flex; flex-wrap:wrap; gap:4px; }
.theme-badge { display:inline-block; padding:2px 6px; border-radius:12px; font-size:10px; background: var(--panel-alt); border:1px solid var(--border); letter-spacing:.5px; }
.badge-fallback { background:#7f1d1d; color:#fff; }
.badge-quality-draft { background:#4338ca; color:#fff; }
.badge-quality-reviewed { background:#065f46; color:#fff; }
.badge-quality-final { background:#065f46; color:#fff; font-weight:600; }
.badge-pop-vc { background:#065f46; color:#fff; }
.badge-pop-c { background:#047857; color:#fff; }
.badge-pop-u { background:#0369a1; color:#fff; }
.badge-pop-n { background:#92400e; color:#fff; }
.badge-pop-r { background:#7f1d1d; color:#fff; }
.badge-curated { background:#4f46e5; color:#fff; }
.badge-enforced { background:#334155; color:#fff; }
.badge-inferred { background:#57534e; color:#fff; }
/* Preview modal */
.preview-modal { position:fixed; inset:0; background:rgba(0,0,0,0.55); display:flex; align-items:flex-start; justify-content:center; padding:4vh 2vw; z-index:9000; }
.preview-modal-content { background:var(--panel); padding:1rem; border-radius:8px; max-width:900px; width:100%; max-height:88vh; overflow:auto; box-shadow:0 4px 18px rgba(0,0,0,0.4); }
/* Skeleton */
.skeleton-table { display:flex; flex-direction:column; gap:6px; }
.skeleton-row { display:grid; grid-template-columns:22% 10% 10% 12% 12% 1fr; gap:8px; align-items:center; }
.sk-cell { height:14px; background:linear-gradient(90deg, var(--panel-alt) 25%, var(--hover) 50%, var(--panel-alt) 75%); background-size:200% 100%; animation: sk 1.2s ease-in-out infinite; border-radius:4px; opacity:.7; }
.skeleton-align .sk-col { height:14px; }
.sk-synergies { height:18px; }
/* New UX additions */
.filter-chips { display:flex; gap:6px; flex-wrap:wrap; margin-top:.35rem; }
.filter-chip { background:var(--panel-alt); border:1px solid var(--border); padding:2px 8px; border-radius:14px; font-size:11px; cursor:pointer; display:inline-flex; align-items:center; gap:4px; }
.filter-chip button { background:none; border:none; color:inherit; cursor:pointer; font-size:11px; padding:0; line-height:1; }
mark { background:#fde68a; color:inherit; padding:0 2px; border-radius:2px; }
@keyframes sk {0%{background-position:0 0;}100%{background-position:-200% 0;}}
</style>
<script>
(function(){
function serializeFilters(container){
var params = [];
var qs = container.querySelector('#theme-search');
if(qs && qs.value.trim()){ params.push('q='+encodeURIComponent(qs.value.trim())); }
var as = container.querySelector('#theme-archetype');
if(as && as.value) params.push('archetype='+encodeURIComponent(as.value));
var bs = container.querySelector('#theme-bucket');
if(bs && bs.value) params.push('bucket='+encodeURIComponent(bs.value));
var lim = container.querySelector('#theme-limit');
if(lim && lim.value) params.push('limit='+encodeURIComponent(lim.value));
var diag = container.querySelector('#diag-toggle');
if(diag && diag.checked) params.push('diagnostics=1');
var syFull = container.querySelector('#synergy-full');
if(syFull && syFull.checked) params.push('synergy_mode=full');
var colorChecks = container.querySelectorAll('input[name="colors"]:checked');
if(colorChecks.length){
var vals = Array.prototype.map.call(colorChecks, c=>c.value).join(',');
params.push('colors='+encodeURIComponent(vals));
}
return params.join('&');
}
var perfMarks={}; function mark(n){ perfMarks[n]=performance.now(); }
function fetchList(){
saveScroll();
var container = document.getElementById('theme-picker');
if(!container) return;
var target = document.getElementById('theme-results');
if(!target) return;
// Abort any in-flight request (resilience: rapid search)
if(window.__themeListAbort){ try { window.__themeListAbort.abort(); } catch(_e){} }
var controller = new AbortController();
window.__themeListAbort = controller;
target.setAttribute('aria-busy','true');
target.setAttribute('aria-label','Loading themes');
mark('list_render_start');
var base = serializeFilters(container);
if(base.indexOf('offset=') === -1){ base += (base ? '&' : '') + 'offset=0'; }
toggleRefreshBtn(true);
fetch('/themes/fragment/list?'+base, {cache:'no-store', signal: controller.signal})
.then(r=>r.text())
.then(html=>{
if(controller.signal.aborted) return;
target.innerHTML = html;
if(window.htmx && typeof window.htmx.process==='function'){
window.htmx.process(target);
}
target.removeAttribute('aria-busy');
target.classList.remove('preload-hide');
listRenderComplete();
toggleRefreshBtn(false);
})
.catch(err=>{ if(controller.signal.aborted) return; target.innerHTML = '<div class="error" role="alert">Failed loading themes. <button id="retry-fetch" class="btn btn-ghost">Retry</button></div>'; target.removeAttribute('aria-busy'); target.classList.remove('preload-hide'); attachRetry(); structuredLog('list_fetch_error'); announceResultCount(); toggleRefreshBtn(false); });
}
function attachRetry(){ var b=document.getElementById('retry-fetch'); if(!b) return; b.addEventListener('click', function(){ fetchList(); }); }
function listRenderComplete(){ mark('list_ready'); announceResultCount(); if(window.THEME_DIAG_ENABLED){ try { var dur=perfMarks.list_ready - perfMarks.list_render_start; if(navigator.sendBeacon){ navigator.sendBeacon('/themes/metrics/client', new Blob([JSON.stringify({events:[{name:'list_render', duration_ms:Math.round(dur)}]})], {type:'application/json'})); } } catch(_e){} } }
function injectPrefetchLinks(){
try {
var head=document.head; if(!head) return;
// Remove old dynamic prefetch links
Array.from(head.querySelectorAll('link[data-dynamic-prefetch]')).forEach(l=>l.remove());
// Choose top 5 rows (skip currently selected to bias exploration)
var rows = document.querySelectorAll('#theme-results tr.theme-row[data-theme-id]');
var current = new URL(window.location.href).searchParams.get('theme');
var picked=[]; rows.forEach(r=>{ var id=r.getAttribute('data-theme-id'); if(!id) return; if(id===current) return; if(picked.length<5) picked.push(id); });
picked.forEach(function(id){ var link=document.createElement('link'); link.rel='prefetch'; link.href='/themes/fragment/detail/'+id; link.as='fetch'; link.setAttribute('data-dynamic-prefetch','1'); head.appendChild(link); });
} catch(e) {}
}
function structuredLog(ev){ if(!window.THEME_DIAG_ENABLED) return; try { fetch('/themes/log',{method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({event:ev, ts:Date.now()})}); } catch(_e){} }
document.addEventListener('htmx:afterOnLoad', function(ev){
if(ev.target && ev.target.id==='theme-results'){
var rows = ev.target.querySelectorAll('tr.theme-row[data-theme-id]');
rows.forEach(function(r){
var id = r.getAttribute('data-theme-id');
if(!id) return;
r.addEventListener('click', function(){
var url = new URL(window.location.href);
url.searchParams.set('theme', id);
var filters = serializeFilters(document.getElementById('theme-picker'));
if(filters){ url.searchParams.set('filters', filters); }
history.pushState({ theme:id, filters: filters }, '', url.toString()); window._lastThemeFocusId=id;
}, { once:false });
var prefetchTimer;
r.addEventListener('mouseenter', function(){
clearTimeout(prefetchTimer);
prefetchTimer = setTimeout(function(){
fetch('/themes/fragment/detail/'+id+'?'+(document.getElementById('diag-toggle')?.checked?'diagnostics=1':''), {cache:'force-cache'}).then(function(resp){ structuredLog('prefetch_success'); return resp.text(); }).then(function(_html){ }).catch(function(){ structuredLog('prefetch_error'); });
}, 180);
});
r.addEventListener('mouseleave', function(){ clearTimeout(prefetchTimer); });
});
var current = new URL(window.location.href).searchParams.get('theme');
if(current && !document.getElementById('theme-detail')?.dataset?.loaded){
htmx.ajax('GET', '/themes/fragment/detail/'+current, '#theme-detail');
}
// Restore focus to previously active row if available
if(window._lastThemeFocusId){
var targetRow = ev.target.querySelector('tr.theme-row[data-theme-id="'+window._lastThemeFocusId+'"]');
if(targetRow){ targetRow.focus({preventScroll:false}); }
}
buildFilterChips();
injectPrefetchLinks();
restoreScroll();
enableKeyboardNav();
}
});
window.addEventListener('popstate', function(ev){
var state = ev.state || {};
if(state.filters){
var params = new URLSearchParams(state.filters);
var container = document.getElementById('theme-picker');
if(container){
if(params.get('q')) container.querySelector('#theme-search').value = decodeURIComponent(params.get('q'));
if(params.get('archetype')) container.querySelector('#theme-archetype').value = decodeURIComponent(params.get('archetype'));
if(params.get('bucket')) container.querySelector('#theme-bucket').value = decodeURIComponent(params.get('bucket'));
if(params.get('limit')) container.querySelector('#theme-limit').value = decodeURIComponent(params.get('limit'));
var colorStr = params.get('colors');
if(colorStr){
var set = new Set(colorStr.split(','));
container.querySelectorAll('input[name="colors"]').forEach(function(cb){ cb.checked = set.has(cb.value); });
}
if(params.get('diagnostics')==='1'){ var diag=container.querySelector('#diag-toggle'); if(diag){ diag.checked=true; } }
fetchList();
}
}
if(state.theme){
htmx.ajax('GET', '/themes/fragment/detail/'+state.theme, '#theme-detail');
}
});
window.addEventListener('load', function(){
var url = new URL(window.location.href);
var filters = url.searchParams.get('filters');
if(filters){
var params = new URLSearchParams(filters);
var container = document.getElementById('theme-picker');
if(container){
if(params.get('q')) container.querySelector('#theme-search').value = decodeURIComponent(params.get('q'));
if(params.get('archetype')) container.querySelector('#theme-archetype').value = decodeURIComponent(params.get('archetype'));
if(params.get('bucket')) container.querySelector('#theme-bucket').value = decodeURIComponent(params.get('bucket'));
if(params.get('limit')) container.querySelector('#theme-limit').value = decodeURIComponent(params.get('limit'));
var colorStr = params.get('colors');
if(colorStr){
var set = new Set(colorStr.split(','));
container.querySelectorAll('input[name="colors"]').forEach(function(cb){ cb.checked = set.has(cb.value); });
}
if(params.get('diagnostics')==='1'){ var diag=container.querySelector('#diag-toggle'); if(diag){ diag.checked=true; } }
}
}
var theme = url.searchParams.get('theme');
if(theme){ htmx.ajax('GET','/themes/fragment/detail/'+theme,'#theme-detail'); }
window.THEME_DIAG_ENABLED = !!document.getElementById('diag-toggle');
}, { once:true });
var lastScroll = 0;
function saveScroll(){ lastScroll = window.scrollY || document.documentElement.scrollTop; }
function restoreScroll(){ if(typeof lastScroll === 'number'){ window.scrollTo(0, lastScroll); } }
function buildFilterChips(){
var host = document.getElementById('filter-chips');
if(!host) return;
host.innerHTML='';
var container = document.getElementById('theme-picker');
if(!container) return;
var q = container.querySelector('#theme-search').value.trim();
var archetype = container.querySelector('#theme-archetype').value;
var bucket = container.querySelector('#theme-bucket').value;
var colors = Array.from(container.querySelectorAll('input[name="colors"]:checked')).map(c=>c.value).join(',');
var diag = container.querySelector('#diag-toggle')?.checked;
function addChip(label, key){
var chip = document.createElement('span');
chip.className='filter-chip';
chip.innerHTML = '<span>'+label+'</span><button aria-label="Remove '+key+'">×</button>';
chip.querySelector('button').addEventListener('click', function(){
if(key==='q'){ container.querySelector('#theme-search').value=''; }
if(key==='archetype'){ container.querySelector('#theme-archetype').value=''; }
if(key==='bucket'){ container.querySelector('#theme-bucket').value=''; }
if(key==='colors'){ container.querySelectorAll('input[name="colors"]').forEach(cb=>cb.checked=false); }
if(key==='diagnostics'){ var d=container.querySelector('#diag-toggle'); if(d) d.checked=false; }
fetchList();
});
host.appendChild(chip);
}
if(q) addChip('Search: '+q, 'q');
if(archetype) addChip('Archetype: '+archetype, 'archetype');
if(bucket) addChip('Popularity: '+bucket, 'bucket');
if(colors) addChip('Colors: '+colors, 'colors');
if(diag) addChip('Diagnostics', 'diagnostics');
var syFull = container.querySelector('#synergy-full')?.checked; if(syFull) addChip('Full Synergies','synergy_mode');
}
function enableKeyboardNav(){
var tbody = document.querySelector('#theme-results tbody');
if(!tbody) return;
var rows = Array.from(tbody.querySelectorAll('tr.theme-row'));
if(!rows.length) return;
var activeIndex = -1;
function setActive(i){ rows.forEach(r=>r.classList.remove('is-active')); if(i>=0 && rows[i]){ rows[i].classList.add('is-active'); rows[i].focus({preventScroll:true}); activeIndex=i; } }
document.addEventListener('keydown', function(e){
if(['ArrowDown','ArrowUp','Enter','Escape'].indexOf(e.key)===-1) return;
if(e.key==='ArrowDown'){ e.preventDefault(); setActive(Math.min(activeIndex+1, rows.length-1)); }
else if(e.key==='ArrowUp'){ e.preventDefault(); setActive(Math.max(activeIndex-1, 0)); }
else if(e.key==='Enter'){ if(activeIndex>=0){ rows[activeIndex].click(); } }
else if(e.key==='Escape'){ setActive(-1); var detail=document.getElementById('theme-detail'); if(detail){ detail.innerHTML='Selection cleared.'; detail.setAttribute('aria-live','polite'); } }
}, { once:false });
document.addEventListener('keydown', function(e){
if(e.key==='Enter' && e.shiftKey){ var cb=document.getElementById('synergy-full'); if(cb){ cb.checked=!cb.checked; fetchList(); } }
});
}
var searchInput = document.getElementById('theme-search');
var searchModeSel = document.getElementById('search-mode');
var lastExec = 0; var pendingTimer = null; var BASE_DELAY = 250;
function performSearch(){ fetchList(); }
function throttledHandler(){ var now=Date.now(); if(now - lastExec > BASE_DELAY){ lastExec = now; performSearch(); } }
function debouncedHandler(){ clearTimeout(pendingTimer); pendingTimer = setTimeout(performSearch, BASE_DELAY); }
function attachSearchHandler(){
if(!searchInput) return;
searchInput.removeEventListener('keyup', throttledHandler);
searchInput.removeEventListener('keyup', debouncedHandler);
if(searchModeSel && searchModeSel.value==='debounce'){
searchInput.addEventListener('keyup', debouncedHandler);
} else {
searchInput.addEventListener('keyup', throttledHandler);
}
}
if(searchModeSel){ searchModeSel.addEventListener('change', attachSearchHandler); }
attachSearchHandler();
function toggleRefreshBtn(dis){ var btn=document.getElementById('catalog-refresh-btn'); if(btn){ if(dis){ btn.setAttribute('disabled','disabled'); btn.setAttribute('aria-busy','true'); } else { btn.removeAttribute('disabled'); btn.removeAttribute('aria-busy'); } } }
function checkStatus(){
fetch('/themes/status',{cache:'no-store'}).then(r=>r.json()).then(js=>{
if(js.stale){
var ind=document.getElementById('theme-stale-indicator'); if(ind){ ind.style.display='inline'; }
toggleRefreshBtn(true);
fetch('/themes/refresh',{method:'POST'}).then(()=>{
var attempts=0; var max=20; var iv=setInterval(()=>{
fetch('/themes/status',{cache:'no-store'}).then(r=>r.json()).then(s2=>{
if(!s2.stale){ clearInterval(iv); if(ind) ind.style.display='none'; fetchList(); }
}).catch(()=>{});
attempts++; if(attempts>max){ clearInterval(iv); }
},1500);
}).finally(()=>{ toggleRefreshBtn(false); });
}
}).catch(()=>{});
}
function announceResultCount(){ var tbody=document.querySelector('#theme-results tbody'); if(!tbody) return; var count=tbody.querySelectorAll('tr.theme-row').length; var host=document.getElementById('theme-results'); if(host){ host.setAttribute('aria-label', count+' themes'); } }
window.addEventListener('load', checkStatus, {once:true});
})();
// Preview modal retry/backoff & logging
(function(){
document.addEventListener('click', function(e){ var btn=e.target.closest('button[data-preview-btn]'); if(!btn) return; var theme=btn.getAttribute('data-theme-id'); if(!theme) return; setTimeout(function(){ attach(theme); }, 40); });
function attach(theme){
var modal=document.getElementById('theme-preview-modal'); if(!modal) return;
var cacheKey='preview:'+theme;
var tStart = performance.now();
// Stale-While-Revalidate: show cached HTML immediately if present
try {
var cached=sessionStorage.getItem(cacheKey);
if(cached){ modal.innerHTML=cached; modal.setAttribute('data-swr','stale'); }
} catch(_e){}
var attempts=0,max=3,back=350; function run(){ attempts++; fetch('/themes/fragment/preview/'+theme,{cache:'no-store'}).then(function(r){ if(r.status===200) return r.text(); if(r.status===304) return ''; throw new Error('bad'); }).then(function(html){ if(!html) return; modal.innerHTML=html; structuredLog('preview_fetch_success'); try { sessionStorage.setItem(cacheKey, html); } catch(_e){} try { recordPreviewLatency(performance.now()-tStart); } catch(_e){} }).catch(function(){ structuredLog('preview_fetch_error'); try { recordPreviewLatency(performance.now()-tStart, true); } catch(_e){} if(attempts<max){ setTimeout(run, back); back*=2; } else { modal.innerHTML='<div class="preview-modal-content"><div role="alert" style="font-size:13px;">Failed to load preview.<br/><button id="retry-preview" class="btn">Retry</button></div></div>'; var rp=document.getElementById('retry-preview'); if(rp){ rp.addEventListener('click', function(){ attempts=0; back=350; run(); }); } } }); }
if(/Loading/.test(modal.textContent||'') || modal.getAttribute('data-swr')==='stale') run();
// Inject export buttons (single insertion guard)
setTimeout(function(){
try {
if(!modal.querySelector('.preview-export-bar') && modal.querySelector('.preview-header')){
var bar = document.createElement('div');
bar.className='preview-export-bar';
bar.style.cssText='margin:.5rem 0 .25rem; display:flex; gap:.5rem; flex-wrap:wrap; align-items:center; font-size:11px;';
bar.innerHTML = '<button class="btn btn-ghost" style="font-size:11px;padding:2px 8px;" data-exp-json>Export JSON</button>'+
'<button class="btn btn-ghost" style="font-size:11px;padding:2px 8px;" data-exp-csv>Export CSV</button>'+
'<span style="opacity:.6;">(Respects curated toggle)</span>';
var header = modal.querySelector('.preview-header');
header.parentNode.insertBefore(bar, header.nextSibling);
function curatedOnly(){ try { return localStorage.getItem('mtg:preview.curatedOnly')==='1'; } catch(_){ return false; } }
bar.querySelector('[data-exp-json]').addEventListener('click', function(){ window.open('/themes/preview/'+encodeURIComponent(theme)+'/export.json?curated_only='+(curatedOnly()?'1':'0'),'_blank'); });
bar.querySelector('[data-exp-csv]').addEventListener('click', function(){ window.open('/themes/preview/'+encodeURIComponent(theme)+'/export.csv?curated_only='+(curatedOnly()?'1':'0'),'_blank'); });
}
} catch(_e){}
}, 120);
}
})();
</script>
<style>
#theme-results.preload-hide { visibility:hidden; }
#catalog-refresh-btn[disabled]{ opacity:.55; cursor:progress; }
</style>
<script>
// Batch preview latency beacons every 20 events
(function(){
var latSamples=[]; var errSamples=0; var BATCH=20; window.recordPreviewLatency=function(ms, isErr){ try { latSamples.push(ms); if(isErr) errSamples++; if(latSamples.length>=BATCH && window.THEME_DIAG_ENABLED){ var avg=Math.round(latSamples.reduce((a,b)=>a+b,0)/latSamples.length); var payload={events:[{name:'preview_load_batch', count:latSamples.length, avg_ms:avg, err:errSamples}]}; if(navigator.sendBeacon){ navigator.sendBeacon('/themes/metrics/client', new Blob([JSON.stringify(payload)],{type:'application/json'})); } latSamples=[]; errSamples=0; } } catch(_e){} };
})();
</script>
{% endblock %}