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
< section >
2025-08-26 20:00:07 -07:00
{% set step_index = 1 %}{% set step_total = 5 %}
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
< h3 > Step 1: Choose a Commander< / h3 >
2025-08-26 20:00:07 -07:00
{% include "build/_stage_navigator.html" %}
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
< form id = "cmdr-search-form" hx-post = "/build/step1" hx-target = "#wizard" hx-swap = "innerHTML" aria-label = "Commander search form" role = "search" >
< label for = "cmdr-search" > Search by name< / label >
< span class = "input-wrap" >
< input id = "cmdr-search" type = "text" name = "query" value = "{{ query or '' }}" autocomplete = "off" aria-describedby = "cmdr-help" aria-controls = "candidate-grid" placeholder = "Type a commander name…" / >
< button id = "cmdr-clear" type = "button" class = "clear-btn" title = "Clear search" aria-label = "Clear search" hidden > × < / button >
< / span >
< input id = "active-name" type = "hidden" name = "active" value = "{{ active or '' }}" / >
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
< button type = "submit" > Search< / button >
< label style = "margin-left:.5rem; font-weight:normal;" >
< input type = "checkbox" name = "auto" value = "1" { % if auto % } checked { % endif % } / > Auto-select top match (very confident)
< / label >
2025-08-26 11:34:42 -07:00
< span id = "search-spinner" class = "spinner" aria-hidden = "true" hidden style = "display:none;" > < / 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
< / form >
2025-08-26 11:34:42 -07:00
< div id = "cmdr-help" class = "muted" style = "margin:.35rem 0 .5rem 0; font-size:.9rem;" >
Tip: Press Enter to select the highlighted result, or use arrow keys to navigate. If your query is a full first word (e.g., "vivi"), exact first-word matches are prioritized.
< / div >
< div id = "selection-live" class = "sr-only" aria-live = "polite" role = "status" > < / div >
< div id = "results-live" class = "sr-only" aria-live = "polite" role = "status" > < / div >
< div id = "kbd-hint" class = "hint" hidden >
< span class = "hint-text" > Use
< span class = "keys" > < kbd > ↑< / kbd > < kbd > ↓< / kbd > < / span > to navigate, < kbd > Enter< / kbd > to select
< / span >
< button type = "button" class = "hint-close" title = "Dismiss keyboard hint" aria-label = "Dismiss" > × < / 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
< / div >
{% if candidates %}
2025-08-26 11:34:42 -07:00
< h4 style = "display:flex; align-items:center; gap:.5rem;" >
Top matches
< small class = "muted" aria-live = "polite" > {% if count is defined %}{{ count }} result{% if count != 1 %}s{% endif %}{% else %}{{ (candidates|length) if candidates else 0 }} results{% endif %}< / small >
< / h4 >
< div class = "candidate-grid" id = "candidate-grid" role = "list" >
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 name, score, colors in candidates %}
2025-08-26 11:34:42 -07:00
< div class = "candidate-tile{% if active and active == name %} active{% endif %}" data-card-name = "{{ name }}" role = "listitem" aria-selected = "{% if active and active == name %}true{% else %}false{% 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 hx-post = "/build/step1/confirm" hx-target = "#wizard" hx-swap = "innerHTML" >
< input type = "hidden" name = "name" value = "{{ name }}" / >
< button class = "img-btn" type = "submit" title = "Select {{ name }} (score {{ score }})" >
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
< img src = "https://api.scryfall.com/cards/named?fuzzy={{ name|urlencode }}&format=image&version=normal" data-card-name = "{{ name }}"
2025-08-26 11:34:42 -07:00
alt="{{ name }}" loading="lazy" decoding="async" />
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
< / button >
< / form >
< div class = "meta" >
< div class = "name" > < span class = "name-text" > {{ name }}< / span > < / div >
< div class = "score" >
match {{ score }}%
{% if colors %}
< span class = "colors" style = "margin-left:.25rem;" >
{% for c in colors %}
< span class = "chip chip-{{ c|lower }}" title = "{{ c }}" > {{ c }}< / span >
{% endfor %}
< / span >
{% endif %}
< / div >
< form hx-post = "/build/step1/inspect" hx-target = "#wizard" hx-swap = "innerHTML" style = "margin-top:.25rem;" >
< input type = "hidden" name = "name" value = "{{ name }}" / >
< button type = "submit" > Inspect< / button >
< / form >
< / div >
< / div >
{% endfor %}
< / div >
{% endif %}
2025-08-26 11:34:42 -07:00
{% if (query is defined and query and (not candidates or (candidates|length == 0))) and not inspect %}
< div id = "candidate-grid" class = "muted" style = "margin-top:.5rem;" aria-live = "polite" >
No results for “{{ query }}”. Try a shorter name or a different spelling.
< / 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
{% if inspect and inspect.ok %}
< div class = "two-col two-col-left-rail" >
< aside class = "card-preview card-sm" data-card-name = "{{ selected }}" >
< a href = "https://scryfall.com/search?q={{ selected|urlencode }}" target = "_blank" rel = "noopener" >
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
< img src = "https://api.scryfall.com/cards/named?fuzzy={{ selected|urlencode }}&format=image&version=normal" alt = "{{ selected }} card image" data-card-name = "{{ selected }}" / >
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 >
< / aside >
< div class = "grow" >
< h4 > Theme Tags< / h4 >
{% if tags and tags|length > 0 %}
< ul >
{% for t in tags %}
< li > {{ t }}< / li >
{% endfor %}
< / ul >
{% else %}
< p class = "muted" > No theme tags found for this commander.< / p >
{% endif %}
< div style = "margin-top:.75rem;" >
< form style = "display:inline" hx-post = "/build/step1/confirm" hx-target = "#wizard" hx-swap = "innerHTML" >
< input type = "hidden" name = "name" value = "{{ selected }}" / >
2025-08-26 20:00:07 -07:00
< button class = "btn-continue" data-action = "continue" > Use this commander< / 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 >
< form style = "display:inline" hx-post = "/build/step1" hx-target = "#wizard" hx-swap = "innerHTML" >
< input type = "hidden" name = "query" value = "" / >
2025-08-26 20:00:07 -07:00
< button class = "btn-back" data-action = "back" > Back to search< / 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 >
< form action = "/build" method = "get" style = "display:inline; margin-left:.5rem;" >
< button type = "submit" > Start over< / button >
< / form >
< / div >
< div hx-get = "/build/banner?step=Choose%20Commander&i=1&n=5" hx-trigger = "load" > < / div >
< / div >
< / div >
{% elif inspect and not inspect.ok %}
< div style = "color:#a00" > {{ inspect.error }}< / div >
{% endif %}
{% if error %}
< div style = "color:#a00" > {{ error }}< / div >
{% endif %}
< / section >
< script >
(function(){
var input = document.getElementById('cmdr-search');
var form = document.getElementById('cmdr-search-form');
var grid = document.getElementById('candidate-grid');
2025-08-26 11:34:42 -07:00
var spinner = document.getElementById('search-spinner');
var activeField = document.getElementById('active-name');
var selLive = document.getElementById('selection-live');
var resultsLive = document.getElementById('results-live');
var hint = document.getElementById('kbd-hint');
var defaultPlaceholder = (input & & input.placeholder) ? input.placeholder : 'Type a commander name…';
var clearBtn = document.getElementById('cmdr-clear');
var initialDescribedBy = (input & & input.getAttribute('aria-describedby')) || '';
// Persist auto-select preference
try {
var autoCb = document.querySelector('input[name="auto"][type="checkbox"]');
if (autoCb) {
var saved = localStorage.getItem('step1-auto');
if (saved === '1' || saved === '0') autoCb.checked = (saved === '1');
autoCb.addEventListener('change', function(){ localStorage.setItem('step1-auto', autoCb.checked ? '1' : '0'); });
}
} catch(_){ }
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 (!input || !form) return;
2025-08-26 11:34:42 -07:00
// Show keyboard hint only when candidates exist and user hasn't dismissed it
function showHintIfNeeded() {
try {
if (!hint) return;
var dismissed = localStorage.getItem('step1-hint-dismissed') === '1';
var hasTiles = !!(document.getElementById('candidate-grid') & & document.getElementById('candidate-grid').querySelector('.candidate-tile'));
var shouldShow = !(dismissed || !hasTiles);
hint.hidden = !shouldShow;
// Link hint to input a11y description only when visible
if (input) {
var base = initialDescribedBy.trim();
var parts = base ? base.split(/\s+/) : [];
var idx = parts.indexOf('kbd-hint');
if (shouldShow) {
if (idx === -1) parts.push('kbd-hint');
} else {
if (idx !== -1) parts.splice(idx, 1);
}
if (parts.length) input.setAttribute('aria-describedby', parts.join(' '));
else input.removeAttribute('aria-describedby');
}
} catch(_) { /* noop */ }
}
showHintIfNeeded();
// Close button for hint
try {
var closeBtn = hint ? hint.querySelector('.hint-close') : null;
if (closeBtn) {
closeBtn.addEventListener('click', function(){
try { localStorage.setItem('step1-hint-dismissed', '1'); } catch(_){}
if (hint) hint.hidden = true;
});
}
} catch(_){ }
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
// Debounce live search
var t = null;
function submit(){
if (!form) return;
// Trigger the HTMX post without clicking submit
if (window.htmx) { window.htmx.trigger(form, 'submit'); }
else { form.submit(); }
}
input.addEventListener('input', function(){
if (t) clearTimeout(t);
t = setTimeout(submit, 250);
2025-08-26 11:34:42 -07:00
try { if (clearBtn) clearBtn.hidden = !(input & & input.value & & input.value.length); } catch(_){ }
});
// Initialize clear visibility
try { if (clearBtn) clearBtn.hidden = !(input & & input.value & & input.value.length); } catch(_){ }
if (clearBtn) clearBtn.addEventListener('click', function(){
if (!input) return;
input.value = '';
try { clearBtn.hidden = true; } catch(_){ }
if (t) clearTimeout(t);
t = setTimeout(submit, 0);
try { input.focus(); } catch(_){}
});
// Focus the search box on load if nothing else is focused
try {
var ae = document.activeElement;
if (input & & (!ae || ae === document.body)) { input.focus(); input.select & & input.select(); }
} catch(_){}
// Quick focus: press "/" to focus the search input (unless already typing)
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(_){} }
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
});
// Keyboard navigation: up/down to move selection, Enter to choose/inspect
document.addEventListener('keydown', function(e){
2025-08-26 11:34:42 -07:00
// Dismiss hint on first keyboard navigation
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'Enter') {
try { localStorage.setItem('step1-hint-dismissed', '1'); } catch(_){}
if (hint) hint.hidden = true;
}
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 (!grid || !grid.children || grid.children.length === 0) return;
var tiles = Array.prototype.slice.call(grid.querySelectorAll('.candidate-tile'));
// Ensure something is selected by default
var idx = tiles.findIndex(function(el){ return el.classList.contains('active'); });
if (idx < 0 & & tiles . length > 0) {
tiles[0].classList.add('active');
2025-08-26 11:34:42 -07:00
try { if (activeField) activeField.value = tiles[0].dataset.cardName || ''; } catch(_){}
try { if (selLive) selLive.textContent = 'Selected ' + (tiles[0].dataset.cardName || ''); } catch(_){}
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
idx = 0;
}
// Determine columns via first row's offsetTop count
var cols = 1;
if (tiles.length > 1) {
var firstTop = tiles[0].offsetTop;
cols = tiles.findIndex(function(el, i){ return i>0 & & el.offsetTop !== firstTop; });
if (cols === -1 || cols === 0) cols = tiles.length; // single row fallback
}
function setActive(newIdx) {
// Clamp to bounds; wrapping handled by callers
newIdx = Math.max(0, Math.min(tiles.length - 1, newIdx));
2025-08-26 11:34:42 -07:00
tiles.forEach(function(el){ el.classList.remove('active'); el.setAttribute('aria-selected', 'false'); });
tiles[newIdx].classList.add('active');
tiles[newIdx].setAttribute('aria-selected', 'true');
try { if (activeField) activeField.value = tiles[newIdx].dataset.cardName || ''; } catch(_){}
try { if (selLive) selLive.textContent = 'Selected ' + (tiles[newIdx].dataset.cardName || ''); } catch(_){}
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
tiles[newIdx].scrollIntoView({ block: 'nearest', inline: 'nearest' });
return newIdx;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
var total = tiles.length;
var rows = Math.ceil(total / cols);
var row = Math.floor(idx / cols);
var col = idx % cols;
var newRow = row + 1;
if (newRow >= rows) newRow = 0; // wrap to first row
var newIdx = newRow * cols + col;
if (newIdx >= total) newIdx = total - 1; // clamp to last tile if last row shorter
idx = setActive(newIdx);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
var totalU = tiles.length;
var rowsU = Math.ceil(totalU / cols);
var rowU = Math.floor(idx / cols);
var colU = idx % cols;
var newRowU = rowU - 1;
if (newRowU < 0 ) newRowU = rowsU - 1 ; / / wrap to last row
var newIdxU = newRowU * cols + colU;
if (newIdxU >= totalU) newIdxU = totalU - 1;
idx = setActive(newIdxU);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
var totalR = tiles.length;
idx = setActive((idx + 1) % totalR);
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
var totalL = tiles.length;
idx = setActive((idx - 1 + totalL) % totalL);
} else if (e.key === 'Enter') {
e.preventDefault();
var active = tiles[idx];
if (!active) return;
var formSel = active.querySelector('form[hx-post="/build/step1/confirm"]');
if (formSel) {
if (window.htmx) { window.htmx.trigger(formSel, 'submit'); }
else if (formSel.submit) { formSel.submit(); }
else {
var btn = active.querySelector('button');
if (btn) btn.click();
}
}
2025-08-26 11:34:42 -07:00
} else if (e.key === 'Escape') {
// ESC clears the search field and triggers a refresh
if (input & & input.value) {
input.value = '';
if (t) clearTimeout(t);
t = setTimeout(submit, 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
// Persist current active on click selection movement too
if (grid) {
grid.addEventListener('click', function(e){
// Dismiss hint on interaction
try { localStorage.setItem('step1-hint-dismissed', '1'); } catch(_){}
if (hint) hint.hidden = true;
var tile = e.target.closest('.candidate-tile');
if (!tile) return;
grid.querySelectorAll('.candidate-tile').forEach(function(el){ el.classList.remove('active'); el.setAttribute('aria-selected', 'false'); });
tile.classList.add('active');
tile.setAttribute('aria-selected', 'true');
try { if (activeField) activeField.value = tile.dataset.cardName || ''; } catch(_){}
try { if (selLive) selLive.textContent = 'Selected ' + (tile.dataset.cardName || ''); } catch(_){}
});
}
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
// Highlight matched text
try {
var q = (input.value || '').trim().toLowerCase();
if (q & & grid) {
grid.querySelectorAll('.name-text').forEach(function(el){
var txt = el.textContent || '';
var low = txt.toLowerCase();
var i = low.indexOf(q);
if (i >= 0) {
el.innerHTML = txt.substring(0, i) + '< mark > ' + txt.substring(i, i+q.length) + '< / mark > ' + txt.substring(i+q.length);
}
});
}
} catch(_){}
2025-08-26 11:34:42 -07:00
// HTMX spinner binding for this form — only show if no results are currently displayed
if (window.htmx & & form) {
form.addEventListener('htmx:beforeRequest', function(){
var hasTiles = false;
try { hasTiles = !!(grid & & grid.querySelector('.candidate-tile')); } catch(_){}
if (spinner) spinner.hidden = hasTiles ? true : false;
if (!hasTiles & & input) input.placeholder = 'Searching…';
try { form.setAttribute('aria-busy', 'true'); } catch(_){ }
if (resultsLive) resultsLive.textContent = 'Searching…';
});
form.addEventListener('htmx:afterSwap', function(){
if (spinner) spinner.hidden = true; if (input) input.placeholder = defaultPlaceholder;
// After swap, if there are no candidate tiles, clear active selection and live text
try {
var grid2 = document.getElementById('candidate-grid');
var hasAny = !!(grid2 & & grid2.querySelector('.candidate-tile'));
if (!hasAny) {
if (activeField) activeField.value = '';
if (selLive) selLive.textContent = '';
}
// Re-evaluate hint visibility post-swap
showHintIfNeeded();
// Announce results count
try {
var qNow = (input & & input.value) ? input.value.trim() : '';
var cnt = 0;
if (grid2) cnt = grid2.querySelectorAll('.candidate-tile').length;
if (resultsLive) {
if (cnt > 0) resultsLive.textContent = cnt + (cnt === 1 ? ' result' : ' results');
else if (qNow) resultsLive.textContent = 'No results for "' + qNow + '"';
else resultsLive.textContent = '';
}
} catch(_){ }
try { form.removeAttribute('aria-busy'); } catch(_){ }
} catch(_){ }
});
form.addEventListener('htmx:responseError', function(){ if (spinner) spinner.hidden = true; if (input) input.placeholder = defaultPlaceholder; });
}
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 >
< style >
.candidate-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:.75rem; }
.candidate-tile { border:1px solid var(--border); border-radius:8px; background:#0f1115; padding:.5rem; }
.candidate-tile.active { outline:2px solid #3b82f6; }
.img-btn { display:block; width:100%; background:transparent; border:0; padding:0; cursor:pointer; }
.img-btn img { width:100%; height:auto; border-radius:6px; }
.chip { display:inline-block; padding:0 .35rem; border-radius:999px; font-size:.75rem; line-height:1.4; border:1px solid var(--border); background:#151821; margin-left:.15rem; }
.chip-w { background:#fdf4d6; color:#6b4f00; border-color:#e9d8a6; }
.chip-u { background:#dbeafe; color:#1e40af; border-color:#93c5fd; }
.chip-b { background:#e5e7eb; color:#111827; border-color:#9ca3af; }
.chip-g { background:#dcfce7; color:#065f46; border-color:#86efac; }
.chip-r { background:#fee2e2; color:#991b1b; border-color:#fecaca; }
.chip-c { background:#f3f4f6; color:#111827; border-color:#e5e7eb; }
mark { background: rgba(251, 191, 36, .35); color: inherit; padding:0 .1rem; border-radius:2px; }
.candidate-tile { cursor: pointer; }
2025-08-26 11:34:42 -07:00
.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; }
.spinner { display:inline-block; width:16px; height:16px; border:2px solid #93c5fd; border-top-color: transparent; border-radius:50%; animation: spin 0.8s linear infinite; vertical-align:middle; margin-left:.4rem; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Ensure hidden attribute always hides spinner within this fragment */
.spinner[hidden] { display: none !important; }
.hint { display:flex; align-items:center; gap:.5rem; background:#0b1220; border:1px solid var(--border); color:#cbd5e1; padding:.4rem .6rem; border-radius:8px; margin:.4rem 0 .6rem; }
.hint .hint-close { background:transparent; border:0; color:#9aa4b2; font-size:1rem; line-height:1; cursor:pointer; }
.hint .keys kbd { background:#1f2937; color:#e5e7eb; padding:.1rem .3rem; border-radius:4px; margin:0 .1rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size:.85em; }
.input-wrap { position: relative; display:inline-flex; align-items:center; }
.clear-btn { position:absolute; right:.35rem; background:transparent; color:#9aa4b2; border:0; cursor:pointer; font-size:1.1rem; line-height:1; padding:.1rem .2rem; }
.clear-btn:hover { color:#cbd5e1; }
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 >