2025-09-23 09:19:23 -07:00
{% 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"
2025-10-07 11:35:43 -07:00
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" />
2025-09-23 09:19:23 -07:00
< 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())
2025-09-30 08:26:11 -07:00
.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);
})
2025-09-23 09:19:23 -07:00
.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 %}