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
{% extends "base.html" %}
{% block banner_subtitle %}Finished Decks{% endblock %}
{% block content %}
2025-08-26 11:34:42 -07:00
< h2 id = "decks-heading" > Finished Decks< / h2 >
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
< p class = "muted" > These are exported decklists from previous runs. Open a deck to view the final summary, download CSV/TXT, and inspect card types and curve.< / p >
{% if error %}
< div class = "error" > {{ error }}< / div >
{% endif %}
2025-08-26 11:34:42 -07:00
< div style = "margin:.75rem 0; display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;" >
< input type = "text" id = "deck-filter" placeholder = "Filter decks…" style = "max-width:280px;" aria-controls = "deck-list" / >
< select id = "deck-sort" aria-label = "Sort decks" >
< option value = "newest" > Newest< / option >
< option value = "oldest" > Oldest< / option >
< option value = "name-asc" > Commander A– Z< / option >
< option value = "name-desc" > Commander Z– A< / option >
< / select >
2025-08-26 16:25:34 -07:00
< select id = "deck-theme" aria-label = "Theme" >
< option value = "" > All Themes< / option >
< / select >
2025-08-26 11:34:42 -07:00
< label for = "deck-txt-only" style = "display:flex; align-items:center; gap:.25rem;" >
< input type = "checkbox" id = "deck-txt-only" / > TXT only
< / label >
< button id = "deck-clear" type = "button" title = "Clear filters" > Clear< / button >
< button id = "deck-share" type = "button" title = "Copy a shareable link" > Share< / button >
2025-08-26 16:25:34 -07:00
< button id = "deck-reset-all" type = "button" title = "Reset filter, sort, and theme" > Reset all< / button >
2025-08-26 11:34:42 -07:00
< button id = "deck-help" type = "button" title = "Keyboard shortcuts and tips" aria-haspopup = "dialog" aria-controls = "deck-help-modal" > Help< / button >
< span id = "deck-count" class = "muted" aria-live = "polite" > < / span >
< span id = "deck-live" class = "sr-only" aria-live = "polite" role = "status" > < / span >
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 >
2025-08-26 16:25:34 -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
{% if items %}
2025-08-26 11:34:42 -07:00
< div id = "deck-list" role = "list" aria-labelledby = "decks-heading" style = "list-style:none; padding:0; margin:0; display:block;" >
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
{% for it in items %}
2025-08-26 11:34:42 -07:00
< div class = "panel" role = "listitem" tabindex = "0" data-name = "{{ it.name }}" data-commander = "{{ it.commander }}" data-tags = "{{ (it.tags|join(' ')) if it.tags else '' }}" data-tags-pipe = "{{ (it.tags|join('|')) if it.tags else '' }}" data-mtime = "{{ it.mtime if it.mtime is defined else 0 }}" data-txt = "{{ '1' if it.txt_path else '0' }}" style = "margin:0 0 .5rem 0;" >
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 style = "display:flex; justify-content:space-between; align-items:center; gap:.5rem;" >
< div >
< div >
< strong data-card-name = "{{ it.commander }}" > {{ it.commander }}< / strong >
< / div >
{% if it.tags and it.tags|length %}
< div class = "muted" style = "font-size:12px;" > Themes: {{ it.tags|join(', ') }}< / div >
{% endif %}
2025-08-26 11:34:42 -07:00
< div class = "muted" style = "font-size:12px;" >
{% if it.mtime is defined %}
< span title = "Modified" > {{ it.mtime | int }}< / span >
{% endif %}
< / 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 >
2025-08-26 11:34:42 -07:00
< div style = "display:flex; gap:.35rem; align-items:center;" >
< form action = "/files" method = "get" style = "display:inline; margin:0;" >
< input type = "hidden" name = "path" value = "{{ it.path }}" / >
< button type = "submit" title = "Download CSV" aria-label = "Download CSV for {{ it.commander }}" > CSV< / button >
< / form >
{% if it.txt_path %}
< form action = "/files" method = "get" style = "display:inline; margin:0;" >
< input type = "hidden" name = "path" value = "{{ it.txt_path }}" / >
< button type = "submit" title = "Download TXT" aria-label = "Download TXT for {{ it.commander }}" > TXT< / button >
< / form >
{% 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
< form action = "/decks/view" method = "get" style = "display:inline; margin:0;" >
< input type = "hidden" name = "name" value = "{{ it.name }}" / >
2025-08-26 11:34:42 -07:00
< button type = "submit" aria-label = "Open deck {{ it.commander }}" > Open< / button >
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
< / form >
< / div >
< / div >
< / div >
{% endfor %}
< / div >
2025-08-26 11:34:42 -07:00
< div id = "deck-empty" class = "muted" style = "display:none; margin-top:.5rem;" > No decks match your filters.< / div >
<!-- Help modal -->
< div id = "deck-help-modal" class = "modal" role = "dialog" aria-modal = "true" aria-labelledby = "deck-help-title" hidden >
< div class = "modal-backdrop" id = "deck-help-backdrop" > < / div >
< div class = "modal-content" role = "document" >
< div class = "modal-header" >
< h3 id = "deck-help-title" style = "margin:0;" > Keyboard and tips< / h3 >
< button type = "button" id = "deck-help-close" aria-label = "Close help" > × < / button >
< / div >
< div class = "modal-body" >
< ul style = "margin:.25rem 0 0 1rem;" >
< li > < kbd > /< / kbd > focuses the filter< / li >
< li > < kbd > Enter< / kbd > /< kbd > Space< / kbd > opens a focused deck; < kbd > Ctrl< / kbd > /< kbd > Shift< / kbd > +< kbd > Enter< / kbd > opens in a new tab< / li >
< li > < kbd > Arrow ↑/↓< / kbd > , < kbd > Home< / kbd > , < kbd > End< / kbd > navigate rows< / li >
< li > < kbd > Esc< / kbd > clears the filter (when focused)< / li >
2025-08-26 16:25:34 -07:00
< li > < kbd > R< / kbd > resets all filters, sort, and theme< / li >
2025-08-26 11:34:42 -07:00
< li > Use “TXT only” to show only decks that have a TXT export< / li >
< li > Share copies a link with your current filters< / li >
< / ul >
< / div >
< / div >
< / 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
{% else %}
< div class = "muted" > No exports yet. Run a build to create one.< / div >
{% endif %}
< script >
(function(){
var input = document.getElementById('deck-filter');
2025-08-26 11:34:42 -07:00
var sortSel = document.getElementById('deck-sort');
2025-08-26 16:25:34 -07:00
var themeSel = document.getElementById('deck-theme');
2025-08-26 11:34:42 -07:00
var clearBtn = document.getElementById('deck-clear');
var list = document.getElementById('deck-list');
var countEl = document.getElementById('deck-count');
var shareBtn = document.getElementById('deck-share');
var resetAllBtn = document.getElementById('deck-reset-all');
var liveEl = document.getElementById('deck-live');
var emptyEl = document.getElementById('deck-empty');
var helpBtn = document.getElementById('deck-help');
var helpModal = document.getElementById('deck-help-modal');
var helpClose = document.getElementById('deck-help-close');
var helpBackdrop = document.getElementById('deck-help-backdrop');
var txtOnlyCb = document.getElementById('deck-txt-only');
if (!list) return;
2025-08-26 16:25:34 -07:00
// Panels and themes discovery from data-tags-pipe
2025-08-26 11:34:42 -07:00
var panels = Array.prototype.slice.call(list.querySelectorAll('.panel'));
function refreshPanels(){ panels = Array.prototype.slice.call(list.querySelectorAll('.panel')); }
2025-08-26 16:25:34 -07:00
var themeSet = new Set();
panels.forEach(function(p){
var raw = p.dataset.tagsPipe || '';
raw.split('|').forEach(function(t){ t = (t||'').trim(); if (t) themeSet.add(t); });
2025-08-26 11:34:42 -07:00
});
2025-08-26 16:25:34 -07:00
// Populate theme dropdown
if (themeSel) {
// Preserve current selection if any
var prev = themeSel.value || '';
// Reset to default option
themeSel.innerHTML = '< option value = "" > All Themes< / option > ';
Array.from(themeSet).sort(function(a,b){ return a.localeCompare(b); }).forEach(function(t){
var opt = document.createElement('option');
opt.value = t; opt.textContent = t; themeSel.appendChild(opt);
});
if (prev) {
// Re-apply previous selection if it exists
var has = Array.prototype.some.call(themeSel.options, function(o){ return o.value === prev; });
if (has) themeSel.value = prev;
}
}
2025-08-26 11:34:42 -07:00
// URL hash < - > state sync helpers
function parseHash(){
try {
var h = (location.hash || '').replace(/^#/, '');
if (!h) return null;
var qp = new URLSearchParams(h);
var q = qp.get('q') || '';
var sort = qp.get('sort') || '';
2025-08-26 16:25:34 -07:00
var tag = qp.get('tag') || '';
2025-08-26 11:34:42 -07:00
var tagsStr = qp.get('tags') || '';
2025-08-26 16:25:34 -07:00
var tags = tagsStr ? tagsStr.split(',').filter(Boolean).map(function(s){ return decodeURIComponent(s); }) : [];
if (!tag & & tags.length) { tag = tags[0]; }
2025-08-26 11:34:42 -07:00
var txt = qp.get('txt');
var txtOnly = (txt === '1' || txt === 'true');
2025-08-26 16:25:34 -07:00
return { q: q, sort: sort, tag: tag, txt: txtOnly };
2025-08-26 11:34:42 -07:00
} catch(_) { return null; }
}
function updateHashFromState(){
try {
var q = (input & & input.value) ? input.value.trim() : '';
var sort = (sortSel & & sortSel.value) ? sortSel.value : 'newest';
2025-08-26 16:25:34 -07:00
var tag = (themeSel & & themeSel.value) ? themeSel.value : '';
2025-08-26 11:34:42 -07:00
var qp = new URLSearchParams();
if (q) qp.set('q', q);
if (sort & & sort !== 'newest') qp.set('sort', sort);
2025-08-26 16:25:34 -07:00
if (tag) qp.set('tag', tag);
2025-08-26 11:34:42 -07:00
if (txtOnlyCb & & txtOnlyCb.checked) qp.set('txt', '1');
var newHash = qp.toString();
var base = location.pathname + location.search;
var current = (location.hash || '').replace(/^#/, '');
if (current !== newHash) {
history.replaceState(null, '', base + (newHash ? ('#' + newHash) : ''));
}
} catch(_){ }
}
function applyStateFromHash(){
var s = parseHash();
if (!s) return false;
var changed = false;
if (typeof s.q === 'string' & & input & & input.value !== s.q) { input.value = s.q; changed = true; }
if (s.sort & & sortSel & & sortSel.value !== s.sort) { sortSel.value = s.sort; changed = true; }
2025-08-26 16:25:34 -07:00
if (typeof s.tag === 'string' & & themeSel) {
// If the tag isn't present in options, add it for back-compat
var exists = Array.prototype.some.call(themeSel.options, function(o){ return o.value === s.tag; });
if (s.tag & & !exists) { var opt = document.createElement('option'); opt.value = s.tag; opt.textContent = s.tag; themeSel.appendChild(opt); }
themeSel.value = s.tag; changed = true;
}
2025-08-26 11:34:42 -07:00
if (typeof s.txt === 'boolean' & & txtOnlyCb) { txtOnlyCb.checked = s.txt; changed = true; }
applyAll();
return changed;
}
function updateCount(){
if (!countEl) return;
var total = panels.length;
var visible = panels.filter(function(p){ return p.style.display !== 'none'; }).length;
countEl.textContent = visible + ' of ' + total + ' decks';
if (emptyEl) emptyEl.style.display = (visible === 0 ? '' : 'none');
try {
if (liveEl) {
if (visible === 0) liveEl.textContent = 'No decks match your filters';
else liveEl.textContent = 'Showing ' + visible + ' of ' + total + ' decks';
}
} catch(_){ }
return { total: total, visible: visible };
}
function applyFilter(){
var q = (input & & input.value || '').toLowerCase();
2025-08-26 16:25:34 -07:00
var selTag = (themeSel & & themeSel.value) ? themeSel.value : '';
2025-08-26 11:34:42 -07:00
panels.forEach(function(row){
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
var hay = (row.dataset.name + ' ' + row.dataset.commander + ' ' + (row.dataset.tags||'')).toLowerCase();
2025-08-26 11:34:42 -07:00
var textMatch = hay.indexOf(q) >= 0;
var tagsPipe = row.dataset.tagsPipe || '';
var tags = tagsPipe ? tagsPipe.split('|').filter(Boolean) : [];
2025-08-26 16:25:34 -07:00
var tagMatch = selTag ? (tags.indexOf(selTag) !== -1) : true;
2025-08-26 11:34:42 -07:00
var txtOk = true;
try { if (txtOnlyCb & & txtOnlyCb.checked) { txtOk = (row.dataset.txt === '1'); } } catch(_){ }
row.style.display = (textMatch & & tagMatch & & txtOk) ? '' : 'none';
});
}
function highlightMatches(){
var q = (input & & input.value || '').trim();
var ql = q.toLowerCase();
panels.forEach(function(row){
var strong = row.querySelector('strong[data-card-name]');
if (!strong) return;
var raw = strong.getAttribute('data-card-name') || strong.textContent || '';
if (!q) { strong.textContent = raw; return; }
var low = raw.toLowerCase();
var i = low.indexOf(ql);
if (i >= 0) {
strong.innerHTML = raw.substring(0, i) + '< mark > ' + raw.substring(i, i+q.length) + '< / mark > ' + raw.substring(i+q.length);
} else {
strong.textContent = raw;
}
// Also highlight in Themes: ... line if present
try {
var themeEl = Array.prototype.slice.call(row.querySelectorAll('.muted')).find(function(el){
var t = (el.textContent || '').trim().toLowerCase();
return t.startsWith('themes:');
});
if (themeEl) {
if (!themeEl.dataset.raw) { themeEl.dataset.raw = themeEl.textContent || ''; }
var base = themeEl.dataset.raw;
if (!q) { themeEl.textContent = base; }
else {
var prefix = 'Themes: ';
var rest = base.startsWith(prefix) ? base.substring(prefix.length) : base;
var li = rest.toLowerCase().indexOf(ql);
if (li >= 0) {
themeEl.innerHTML = prefix + rest.substring(0, li) + '< mark > ' + rest.substring(li, li+q.length) + '< / mark > ' + rest.substring(li+q.length);
} else {
themeEl.textContent = base;
}
}
}
} catch(_){ }
});
}
function applySort(){
var mode = (sortSel & & sortSel.value) || 'newest';
var rows = panels.slice();
rows.sort(function(a,b){
if (mode === 'newest' || mode === 'oldest'){
var am = parseFloat(a.dataset.mtime || '0');
var bm = parseFloat(b.dataset.mtime || '0');
return (mode === 'newest') ? (bm - am) : (am - bm);
} else if (mode === 'name-asc' || mode === 'name-desc'){
var ac = (a.dataset.commander || '').toLowerCase();
var bc = (b.dataset.commander || '').toLowerCase();
var cmp = ac.localeCompare(bc);
return (mode === 'name-asc') ? cmp : -cmp;
}
return 0;
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
});
2025-08-26 11:34:42 -07:00
// Re-append in new order
rows.forEach(function(r){ list.appendChild(r); });
refreshPanels();
}
function applyAll(){
applyFilter();
applySort();
highlightMatches();
var counts = updateCount();
// If focus is on a hidden panel, move to first visible
try {
var active = document.activeElement;
if (active & & list.contains(active)) {
var p = active.closest('.panel');
if (p & & p.style.display === 'none') {
var firstVis = Array.prototype.slice.call(list.querySelectorAll('.panel')).find(function(el){ return el.style.display !== 'none'; });
if (firstVis) firstVis.focus();
}
}
} catch(_){ }
// Persist state
try {
if (input) localStorage.setItem('decks-filter', input.value || '');
if (sortSel) localStorage.setItem('decks-sort', sortSel.value || 'newest');
2025-08-26 16:25:34 -07:00
if (themeSel) localStorage.setItem('decks-theme', themeSel.value || '');
2025-08-26 11:34:42 -07:00
if (txtOnlyCb) localStorage.setItem('decks-txt', txtOnlyCb.checked ? '1' : '0');
} catch(_){ }
// Update URL hash for shareable state
updateHashFromState();
}
// Debounce helper
function debounce(fn, delay){
var timer = null;
return function(){
var ctx = this, args = arguments;
if (timer) clearTimeout(timer);
timer = setTimeout(function(){ fn.apply(ctx, args); }, delay);
};
}
var debouncedApply = debounce(applyAll, 150);
if (input) input.addEventListener('input', debouncedApply);
if (sortSel) sortSel.addEventListener('change', applyAll);
2025-08-26 16:25:34 -07:00
if (themeSel) themeSel.addEventListener('change', applyAll);
2025-08-26 11:34:42 -07:00
if (txtOnlyCb) txtOnlyCb.addEventListener('change', applyAll);
if (clearBtn) clearBtn.addEventListener('click', function(){
if (input) input.value = '';
2025-08-26 16:25:34 -07:00
if (themeSel) themeSel.value = '';
2025-08-26 11:34:42 -07:00
if (sortSel) sortSel.value = 'newest';
if (txtOnlyCb) txtOnlyCb.checked = false;
applyAll();
});
if (resetAllBtn) resetAllBtn.addEventListener('click', function(){
// Clear UI state
try {
if (input) input.value = '';
if (sortSel) sortSel.value = 'newest';
if (txtOnlyCb) txtOnlyCb.checked = false;
2025-08-26 16:25:34 -07:00
if (themeSel) themeSel.value = '';
2025-08-26 11:34:42 -07:00
// Clear persistence
localStorage.removeItem('decks-filter');
localStorage.removeItem('decks-sort');
2025-08-26 16:25:34 -07:00
localStorage.removeItem('decks-theme');
2025-08-26 11:34:42 -07:00
localStorage.removeItem('decks-txt');
// Clear URL hash
var base = location.pathname + location.search;
history.replaceState(null, '', base);
} catch(_){ }
applyAll();
2025-08-26 16:25:34 -07:00
if (liveEl) liveEl.textContent = 'Filters, sort, and theme reset';
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
});
2025-08-26 11:34:42 -07:00
if (shareBtn) shareBtn.addEventListener('click', function(){
try {
// Ensure hash reflects current UI state
updateHashFromState();
var url = window.location.href;
if (navigator.clipboard & & navigator.clipboard.writeText) {
navigator.clipboard.writeText(url);
} else {
var t = document.createElement('input');
t.value = url; document.body.appendChild(t); t.select(); try { document.execCommand('copy'); } catch(_){} document.body.removeChild(t);
}
var prev = shareBtn.textContent;
shareBtn.textContent = 'Copied';
setTimeout(function(){ shareBtn.textContent = prev; }, 1200);
if (liveEl) liveEl.textContent = 'Link copied to clipboard';
} catch(_){ }
});
// Initial state: prefer URL hash, fall back to localStorage
var hadHash = false;
try { hadHash = !!((location.hash || '').replace(/^#/, '')); } catch(_){ }
if (hadHash) {
if (!applyStateFromHash()) { applyAll(); }
} else {
// Load persisted state
try {
var savedFilter = localStorage.getItem('decks-filter') || '';
if (input) input.value = savedFilter;
var savedSort = localStorage.getItem('decks-sort') || 'newest';
if (sortSel) sortSel.value = savedSort;
2025-08-26 16:25:34 -07:00
var savedTheme = localStorage.getItem('decks-theme') || '';
if (themeSel & & savedTheme) {
var exists = Array.prototype.some.call(themeSel.options, function(o){ return o.value === savedTheme; });
if (!exists) { var opt = document.createElement('option'); opt.value = savedTheme; opt.textContent = savedTheme; themeSel.appendChild(opt); }
themeSel.value = savedTheme;
}
// Back-compat: if no savedTheme, try first of old saved tags
if (themeSel & & !savedTheme) {
try {
var oldTags = JSON.parse(localStorage.getItem('decks-tags') || '[]');
if (Array.isArray(oldTags) & & oldTags.length > 0) {
var ot = oldTags[0];
var ex2 = Array.prototype.some.call(themeSel.options, function(o){ return o.value === ot; });
if (!ex2) { var o2 = document.createElement('option'); o2.value = ot; o2.textContent = ot; themeSel.appendChild(o2); }
themeSel.value = ot;
}
} catch(_e){}
}
2025-08-26 11:34:42 -07:00
if (txtOnlyCb) txtOnlyCb.checked = (localStorage.getItem('decks-txt') === '1');
} catch(_){ }
applyAll();
}
// React to external hash changes
window.addEventListener('hashchange', function(){ applyStateFromHash(); });
// Open deck: keyboard and mouse helpers on panels
function getPanelUrl(p){
try {
var name = p.getAttribute('data-name') || '';
if (name) return '/decks/view?name=' + encodeURIComponent(name);
var form = p.querySelector('form[action="/decks/view"]');
if (form) {
var nameInput = form.querySelector('input[name="name"]');
if (nameInput & & nameInput.value) return '/decks/view?name=' + encodeURIComponent(nameInput.value);
}
} catch(_){ }
return '/decks/view';
}
function openPanel(p, newTab){
if (!p) return;
if (newTab) { window.open(getPanelUrl(p), '_blank'); return; }
var openForm = p.querySelector('form[action="/decks/view"]');
if (openForm) {
if (window.htmx) { window.htmx.trigger(openForm, 'submit'); }
else if (openForm.submit) { openForm.submit(); }
} else { window.location.href = getPanelUrl(p); }
}
list.addEventListener('dblclick', function(e){
var p = e.target.closest('.panel');
if (!p) return;
// Ignore when double-clicking interactive controls
if (e.target.closest('button, a, input, select, textarea, label, form')) return;
openPanel(p);
});
list.addEventListener('keydown', function(e){
var p = e.target.closest('.panel[tabindex]');
if (!p) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
var newTab = !!(e.ctrlKey || e.metaKey || e.shiftKey);
openPanel(p, newTab);
}
});
// Arrow key navigation between visible panels
document.addEventListener('keydown', function(e){
if (e.key !== 'ArrowDown' & & e.key !== 'ArrowUp' & & e.key !== 'Home' & & e.key !== 'End') return;
var active = document.activeElement;
if (!active || !list.contains(active)) return;
var vis = Array.prototype.slice.call(list.querySelectorAll('.panel')).filter(function(p){ return p.style.display !== 'none'; });
if (!vis.length) return;
var idx = vis.indexOf(active.closest('.panel'));
if (idx === -1) return;
e.preventDefault();
var target = null;
if (e.key === 'ArrowDown') target = vis[Math.min(idx + 1, vis.length - 1)];
else if (e.key === 'ArrowUp') target = vis[Math.max(idx - 1, 0)];
else if (e.key === 'Home') target = vis[0];
else if (e.key === 'End') target = vis[vis.length - 1];
if (target) { try { target.focus(); } catch(_){ } }
});
// ESC clears filter when focused in the filter input
if (input) {
input.addEventListener('keydown', function(e){
if (e.key === 'Escape' & & input.value) {
input.value = '';
debouncedApply();
} else if (e.key === 'Enter') {
// Open first visible deck when pressing Enter in filter
var firstVis = Array.prototype.slice.call(list.querySelectorAll('.panel')).find(function(el){ return el.style.display !== 'none'; });
if (firstVis) { e.preventDefault(); openPanel(firstVis, !!(e.ctrlKey||e.metaKey||e.shiftKey)); }
}
});
}
// Quick focus: '/' focuses filter when not typing elsewhere
document.addEventListener('keydown', function(e){
if (e.key !== '/') return;
var tag = (e.target & & e.target.tagName) ? e.target.tagName.toLowerCase() : '';
var isEditable = (tag === 'input' || tag === 'textarea' || tag === 'select' || (e.target & & e.target.isContentEditable));
if (isEditable) return;
if (e.ctrlKey || e.altKey || e.metaKey) return;
e.preventDefault();
if (input) { input.focus(); try { input.select(); } catch(_){} }
});
// Global shortcut: 'R' to reset all (when not typing)
document.addEventListener('keydown', function(e){
if ((e.key === 'r' || e.key === 'R') & & !(e.ctrlKey || e.altKey || e.metaKey)) {
var tag = (e.target & & e.target.tagName) ? e.target.tagName.toLowerCase() : '';
var isEditable = (tag === 'input' || tag === 'textarea' || tag === 'select' || (e.target & & e.target.isContentEditable));
if (isEditable) return;
if (resetAllBtn) { e.preventDefault(); resetAllBtn.click(); }
}
});
// Help modal wiring
(function(){
if (!helpBtn || !helpModal) return;
var prevFocus = null;
function openHelp(){
prevFocus = document.activeElement;
helpModal.hidden = false;
try { document.body.dataset.prevOverflow = document.body.style.overflow || ''; document.body.style.overflow = 'hidden'; } catch(_){ }
var close = helpClose || helpModal.querySelector('button');
if (close) try { close.focus(); } catch(_){ }
}
function closeHelp(){
helpModal.hidden = true;
try { document.body.style.overflow = document.body.dataset.prevOverflow || ''; } catch(_){ }
if (prevFocus) try { prevFocus.focus(); } catch(_){ }
}
helpBtn.addEventListener('click', openHelp);
if (helpClose) helpClose.addEventListener('click', closeHelp);
if (helpBackdrop) helpBackdrop.addEventListener('click', closeHelp);
document.addEventListener('keydown', function(e){ if (e.key === 'Escape' & & !helpModal.hidden) { e.preventDefault(); closeHelp(); } });
document.addEventListener('keydown', function(e){
if ((e.key === '?' || (e.shiftKey & & e.key === '/')) & & !(e.ctrlKey||e.metaKey||e.altKey)){
var tag = (e.target & & e.target.tagName) ? e.target.tagName.toLowerCase() : '';
var isEditable = (tag === 'input' || tag === 'textarea' || tag === 'select' || (e.target & & e.target.isContentEditable));
if (isEditable) return;
e.preventDefault();
if (helpModal.hidden) openHelp(); else closeHelp();
}
});
})();
// Enhance mtime display to human-readable date
try {
panels.forEach(function(p){
var m = parseFloat(p.dataset.mtime || '0');
if (!m) return;
var el = p.querySelector('[title="Modified"]');
if (el) {
try { el.textContent = new Date(m * 1000).toLocaleString(); } catch(_){}
}
});
} catch(_){ }
// (copy button removed)
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-08-26 11:34:42 -07:00
< style >
.sr-only{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
mark { background: rgba(251, 191, 36, .35); color: inherit; padding:0 .1rem; border-radius:2px; }
#deck-list[role="list"] .panel[role="listitem"] { outline: none; }
#deck-list[role="list"] .panel[role="listitem"]:focus { box-shadow: 0 0 0 2px #3b82f6 inset; }
< / style >
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
{% endblock %}