2025-10-08 11:38:30 -07:00
< div id = "deck-summary" data-summary >
2025-10-28 08:21:52 -07:00
< hr class = "summary-divider" / >
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
< h4 > Deck Summary< / h4 >
2025-10-28 08:21:52 -07:00
< section class = "summary-section" >
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
< h5 > Card Types< / h5 >
2025-10-28 08:21:52 -07:00
< div class = "summary-view-controls" >
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
< span class = "muted" > View:< / span >
< div class = "seg" role = "tablist" aria-label = "Type view" >
2025-09-25 15:14:15 -07:00
< button type = "button" class = "seg-btn" data-view = "list" aria-selected = "true" onclick = "(function(btn){var list=document.getElementById('typeview-list');var thumbs=document.getElementById('typeview-thumbs');if(!list||!thumbs)return;list.classList.remove('hidden');thumbs.classList.add('hidden');btn.setAttribute('aria-selected','true');var other=btn.parentElement.querySelector('.seg-btn[data-view=thumbs]');if(other)other.setAttribute('aria-selected','false');try{localStorage.setItem('summaryTypeView','list');}catch(e){}})(this)" > List< / button >
< button type = "button" class = "seg-btn" data-view = "thumbs" onclick = "(function(btn){var list=document.getElementById('typeview-list');var thumbs=document.getElementById('typeview-thumbs');if(!list||!thumbs)return;list.classList.add('hidden');thumbs.classList.remove('hidden');btn.setAttribute('aria-selected','true');var other=btn.parentElement.querySelector('.seg-btn[data-view=list]');if(other)other.setAttribute('aria-selected','false');try{localStorage.setItem('summaryTypeView','thumbs');}catch(e){}; (function(){var tv=document.getElementById('typeview-thumbs'); if(!tv) return; tv.querySelectorAll('.stack-wrap').forEach(function(sw){var grid=sw.querySelector('.stack-grid'); if(!grid) return; var cs=getComputedStyle(sw); var cardW=parseFloat(cs.getPropertyValue('--card-w'))||160; var gap=10; var width=sw.clientWidth; if(!width||width<cardW){ sw.style.setProperty('--cols','1'); return;} var cols=Math.max(1,Math.floor((width+gap)/(cardW+gap))); sw.style.setProperty('--cols',String(cols));}); })();})(this)" > Thumbnails< / 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 >
< / div >
2025-09-25 15:14:15 -07:00
< div style = "display:none" hx-on:load = "(function(){try{var mode=localStorage.getItem('summaryTypeView')||'list';if(mode==='thumbs'){var list=document.getElementById('typeview-list');var thumbs=document.getElementById('typeview-thumbs');if(list&&thumbs){list.classList.add('hidden');thumbs.classList.remove('hidden');var lb=document.querySelector('.seg-btn[data-view=list]');var tb=document.querySelector('.seg-btn[data-view=thumbs]');if(lb&&tb){lb.setAttribute('aria-selected','false');tb.setAttribute('aria-selected','true');}thumbs.querySelectorAll('.stack-wrap').forEach(function(sw){var grid=sw.querySelector('.stack-grid');if(!grid)return;var cs=getComputedStyle(sw);var cardW=parseFloat(cs.getPropertyValue('--card-w'))||160;var gap=10;var width=sw.clientWidth;if(!width||width<cardW){sw.style.setProperty('--cols','1');return;}var cols=Math.max(1,Math.floor((width+gap)/(cardW+gap)));sw.style.setProperty('--cols',String(cols));});}}catch(e){}})()" > < / 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
{% set tb = summary.type_breakdown %}
2025-09-29 21:32:08 -07:00
{% set synergies_norm = [] %}
{% if synergies %}
{% set synergies_norm = synergies|map('trim')|map('lower')|list %}
{% 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 tb and tb.counts %}
< style >
.seg { display:inline-flex; border:1px solid var(--border); border-radius:8px; overflow:hidden; }
.seg-btn { background:#12161c; color:#e5e7eb; border:none; padding:.35rem .6rem; cursor:pointer; font-size:12px; }
.seg-btn[aria-selected="true"] { background:#1f2937; }
.typeview { margin-top:.25rem; }
.typeview.hidden { display:none; }
.stack-wrap { --card-w: 160px; --card-h: 224px; --cols: 9; --overlap: .5; overflow: visible; padding: 6px 0 calc(var(--card-h) * (1 - var(--overlap))) 0; }
.stack-grid { display: grid; grid-template-columns: repeat(var(--cols), var(--card-w)); grid-auto-rows: calc(var(--card-h) * var(--overlap)); column-gap: 10px; }
.stack-card { width: var(--card-w); height: var(--card-h); border-radius:8px; box-shadow: 0 6px 18px rgba(0,0,0,.55); border:1px solid var(--border); background:#0f1115; transition: transform .06s ease, box-shadow .06s ease; position: relative; }
.stack-card img { width: var(--card-w); height: var(--card-h); display:block; border-radius:8px; }
.stack-card:hover { z-index: 999; transform: translateY(-2px); box-shadow: 0 10px 22px rgba(0,0,0,.6); }
2025-08-26 16:25:34 -07:00
.count-badge { position:absolute; top:6px; right:6px; background:rgba(17,24,39,.9); color:#e5e7eb; border:1px solid var(--border); border-radius:12px; font-size:12px; line-height:18px; height:18px; padding:0 6px; pointer-events:none; }
.owned-badge { position:absolute; top:6px; left:6px; background:rgba(17,24,39,.9); color:#e5e7eb; border:1px solid var(--border); border-radius:12px; font-size:12px; line-height:18px; height:18px; min-width:18px; padding:0 6px; text-align:center; pointer-events:none; z-index: 2; }
2025-10-02 15:31:05 -07:00
.dfc-thumb-badge { position:absolute; bottom:8px; left:6px; background:rgba(15,23,42,.92); border:1px solid #34d399; color:#bbf7d0; border-radius:12px; font-size:11px; line-height:18px; height:18px; padding:0 6px; pointer-events:none; }
.dfc-thumb-badge.counts { border-color:#60a5fa; color:#bfdbfe; }
2025-08-26 16:25:34 -07:00
.owned-flag { font-size:.95rem; opacity:.9; }
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 >
< div id = "typeview-list" class = "typeview" >
{% for t in tb.order %}
2025-10-28 08:21:52 -07:00
< div class = "summary-type-heading" >
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
{{ t }} — {{ tb.counts[t] }}{% if tb.total %} ({{ '%.1f' % (tb.counts[t] * 100.0 / tb.total) }}%){% endif %}
< / div >
{% set clist = tb.cards.get(t, []) %}
{% if clist %}
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
< style >
.list-grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap:.35rem .75rem; margin:.25rem 0 .75rem 0; }
@media (max-width: 1199px) {
.list-grid { grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); }
}
2025-09-29 21:32:08 -07:00
.list-row { display:grid; grid-template-columns: 4ch 1.25ch minmax(0,1fr) auto 1.6em; align-items:center; column-gap:.45rem; width:100%; }
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
.list-row .count { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-variant-numeric: tabular-nums; font-feature-settings: 'tnum'; text-align:right; color:#94a3b8; }
.list-row .times { color:#94a3b8; text-align:center; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.list-row .name { display:inline-block; padding: 2px 4px; border-radius: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2025-10-02 15:31:05 -07:00
.list-row .flip-slot { min-width:2.4em; display:flex; justify-content:flex-start; align-items:center; }
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
.list-row .owned-flag { width: 1.6em; min-width: 1.6em; text-align:center; display:inline-block; }
2025-10-02 15:31:05 -07:00
.dfc-land-chip { display:inline-flex; align-items:center; gap:.25rem; padding:2px 6px; border-radius:999px; font-size:11px; font-weight:600; background:#0f172a; border:1px solid #334155; color:#e5e7eb; line-height:1; }
.dfc-land-chip.extra { border-color:#34d399; color:#a7f3d0; }
.dfc-land-chip.counts { border-color:#60a5fa; color:#bfdbfe; }
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
< / style >
2025-10-08 11:38:30 -07:00
< div class = "list-grid" { % if virtualize % } data-virtualize = "list" data-virtualize-min = "90" { % 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
{% for c in clist %}
2025-09-29 21:32:08 -07:00
{# Compute overlaps with detected deck synergies when available #}
{% set overlaps = [] %}
{% if synergies_norm and c.tags %}
{% for tg in c.tags %}
{% set tag_trim = tg|trim %}
{% if tag_trim and (tag_trim|lower) in synergies_norm and tag_trim not in overlaps %}
{% set _ = overlaps.append(tag_trim) %}
{% endif %}
{% endfor %}
{% endif %}
< div class = "list-row {% if (game_changers and (c.name in game_changers)) or ('game_changer' in (c.role or '') or 'Game Changer' in (c.role or '')) %}game-changer{% endif %}"
data-card-name="{{ c.name }}" data-original-name="{{ c.name }}" data-role="{{ c.role }}" data-tags="{{ (c.tags|map('trim')|join(', ')) if c.tags else '' }}"{% if overlaps %} data-overlaps="{{ overlaps|join(', ') }}"{% 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
{% set cnt = c.count if c.count else 1 %}
2025-08-26 16:25:34 -07:00
{% set owned = (owned_set is defined and c.name and (c.name|lower in owned_set)) %}
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
< span class = "count" > {{ cnt }}< / span >
< span class = "times" > x< / span >
2025-10-09 17:29:57 -07:00
< span class = "name dfc-anchor" title = "{{ c.name }}" data-card-name = "{{ c.name }}" data-count = "{{ cnt }}" data-role = "{{ c.role }}" data-tags = "{{ (c.tags|map('trim')|join(', ')) if c.tags else '' }}" { % if c . metadata_tags % } data-metadata-tags = "{{ (c.metadata_tags|map('trim')|join(', ')) }}" { % endif % } { % if overlaps % } data-overlaps = "{{ overlaps|join(', ') }}" { % endif % } > {{ c.name }}< / span >
2025-10-02 15:31:05 -07:00
< span class = "flip-slot" aria-hidden = "true" >
{% if c.dfc_land %}
< span class = "dfc-land-chip {% if c.dfc_adds_extra_land %}extra{% else %}counts{% endif %}" title = "{{ c.dfc_note or 'Modal double-faced land' }}" > DFC land{% if c.dfc_adds_extra_land %} +1{% endif %}< / span >
{% endif %}
< / span >
2025-08-26 16:25:34 -07:00
< span class = "owned-flag" title = "{{ 'Owned' if owned else 'Not owned' }}" aria-label = "{{ 'Owned' if owned else 'Not owned' }}" > {% if owned %}✔{% else %}✖{% endif %}< / 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 >
{% endfor %}
< / div >
{% else %}
2025-10-28 08:21:52 -07:00
< div class = "muted mb-3" > No cards in this type.< / 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
{% endif %}
{% endfor %}
< / div >
< div id = "typeview-thumbs" class = "typeview hidden" >
{% for t in tb.order %}
2025-10-28 08:21:52 -07:00
< div class = "summary-type-heading" >
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
{{ t }} — {{ tb.counts[t] }}{% if tb.total %} ({{ '%.1f' % (tb.counts[t] * 100.0 / tb.total) }}%){% endif %}
< / div >
{% set clist = tb.cards.get(t, []) %}
{% if clist %}
< div class = "stack-wrap" >
< div class = "stack-grid" >
{% for c in clist %}
{% set cnt = c.count if c.count else 1 %}
2025-08-26 16:25:34 -07:00
{% set owned = (owned_set is defined and c.name and (c.name|lower in owned_set)) %}
2025-09-29 21:32:08 -07:00
{% set overlaps = [] %}
{% if synergies_norm and c.tags %}
{% for tg in c.tags %}
{% set tag_trim = tg|trim %}
{% if tag_trim and (tag_trim|lower) in synergies_norm and tag_trim not in overlaps %}
{% set _ = overlaps.append(tag_trim) %}
{% endif %}
{% endfor %}
{% 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
< div class = "stack-card {% if (game_changers and (c.name in game_changers)) or ('game_changer' in (c.role or '') or 'Game Changer' in (c.role or '')) %}game-changer{% endif %}" >
2025-10-28 08:21:52 -07:00
< img class = "card-thumb" loading = "lazy" decoding = "async" src = "{{ c.name|card_image('normal') }}" alt = "{{ c.name }} image" data-card-name = "{{ c.name }}" data-count = "{{ cnt }}" data-role = "{{ c.role }}" data-tags = "{{ (c.tags|map('trim')|join(', ')) if c.tags else '' }}" { % if overlaps % } data-overlaps = "{{ overlaps|join(', ') }}" { % endif % }
srcset="{{ c.name|card_image('small') }} 160w, {{ c.name|card_image('normal') }} 488w"
2025-08-28 14:57:22 -07:00
sizes="(max-width: 1200px) 160px, 240px" />
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< div class = "count-badge" > {{ cnt }}x< / div >
2025-08-26 16:25:34 -07:00
< div class = "owned-badge" title = "{{ 'Owned' if owned else 'Not owned' }}" aria-label = "{{ 'Owned' if owned else 'Not owned' }}" > {% if owned %}✔{% else %}✖{% endif %}< / div >
2025-10-02 15:31:05 -07:00
{% if c.dfc_land %}
< div class = "dfc-thumb-badge {% if c.dfc_adds_extra_land %}extra{% else %}counts{% endif %}" title = "{{ c.dfc_note or 'Modal double-faced land' }}" > DFC{% if c.dfc_adds_extra_land %}+1{% endif %}< / 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
< / div >
{% endfor %}
< / div >
< / div >
{% else %}
2025-10-28 08:21:52 -07:00
< div class = "muted mb-3" > No cards in this type.< / 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
{% endif %}
{% endfor %}
< / div >
{% else %}
< div class = "muted" > No type data available.< / div >
{% endif %}
< / section >
2025-09-01 16:55:24 -07:00
2025-09-25 15:14:15 -07:00
<!-- Deck Summary initializer script moved below markup for proper element availability -->
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-10-02 15:31:05 -07:00
<!-- Land Summary -->
{% set land = summary.land_summary if summary else None %}
{% if land %}
2025-10-28 08:21:52 -07:00
< section class = "summary-section-lg" >
2025-10-02 15:31:05 -07:00
< h5 > Land Summary< / h5 >
2025-10-28 08:21:52 -07:00
< div class = "muted summary-type-heading mb-1" >
2025-10-02 15:31:05 -07:00
{{ land.headline or ('Lands: ' ~ (land.traditional or 0)) }}
< / div >
2025-10-28 08:21:52 -07:00
< div class = "deck-metrics-wrap" >
2025-10-02 15:31:05 -07:00
< div class = "muted" > Traditional land slots: < strong > {{ land.traditional or 0 }}< / strong > < / div >
< div class = "muted" > MDFC land additions: < strong > {{ land.dfc_lands or 0 }}< / strong > < / div >
< div class = "muted" > Total with MDFCs: < strong > {{ land.with_dfc or land.traditional or 0 }}< / strong > < / div >
< / div >
{% if land.dfc_cards %}
2025-10-28 08:21:52 -07:00
< details class = "mt-2" >
2025-10-02 15:31:05 -07:00
< summary > MDFC mana sources ({{ land.dfc_cards|length }})< / summary >
2025-10-28 08:21:52 -07:00
< ul class = "land-breakdown-list" >
2025-10-02 15:31:05 -07:00
{% for card in land.dfc_cards %}
{% set extra = card.adds_extra_land or card.counts_as_extra %}
{% set colors = card.colors or [] %}
2025-10-28 08:21:52 -07:00
< li class = "muted land-breakdown-item" >
< span class = "chip" > < span class = "dot dot-green" > < / span > {{ card.name }} × {{ card.count or 1 }}< / span >
2025-10-02 15:31:05 -07:00
< span > Colors: {{ colors|join(', ') if colors else '– ' }}< / span >
{% if extra %}
2025-10-28 08:21:52 -07:00
< span class = "chip land-note-chip-expand" > {{ card.note or 'Adds extra land slot' }}< / span >
2025-10-02 15:31:05 -07:00
{% else %}
2025-10-28 08:21:52 -07:00
< span class = "chip land-note-chip-counts" > {{ card.note or 'Counts as land slot' }}< / span >
2025-10-02 15:31:05 -07:00
{% endif %}
{% if card.faces %}
2025-10-28 08:21:52 -07:00
< ul class = "land-breakdown-subs" >
2025-10-02 15:31:05 -07:00
{% for face in card.faces %}
{% set face_name = face.get('face') or face.get('faceName') or 'Face' %}
{% set face_type = face.get('type') or '– ' %}
{% set mana_cost = face.get('mana_cost') %}
{% set mana_value = face.get('mana_value') %}
{% set produces = face.get('produces_mana') %}
2025-10-28 08:21:52 -07:00
< li class = "land-breakdown-sub" >
2025-10-02 15:31:05 -07:00
< span > {{ face_name }}< / span >
< span > — {{ face_type }}< / span >
{% if mana_cost %}< span > • Mana Cost: {{ mana_cost }}< / span > {% endif %}
{% if mana_value is not none %}< span > • MV: {{ mana_value }}< / span > {% endif %}
{% if produces %}< span > • Produces mana< / span > {% endif %}
< / li >
{% endfor %}
< / ul >
{% endif %}
< / li >
{% endfor %}
< / ul >
< / details >
{% endif %}
< / section >
{% endif %}
2025-08-26 16:25:34 -07:00
<!-- Mana Overview Row: Pips • Sources • Curve -->
2025-10-28 08:21:52 -07:00
< section class = "summary-section-lg" >
2025-10-08 11:38:30 -07:00
< details class = "analytics-accordion" id = "mana-overview-accordion" data-lazy-load data-analytics-type = "mana" >
2025-10-28 08:21:52 -07:00
< summary class = "combo-summary" >
2025-10-08 11:38:30 -07:00
< span > Mana Overview< / span >
2025-10-28 08:21:52 -07:00
< span class = "muted text-xs font-normal ml-2" > (pips • sources • curve)< / span >
2025-10-08 11:38:30 -07:00
< / summary >
2025-10-28 08:21:52 -07:00
< div class = "analytics-content mt-3" >
< h5 class = "mt-0 mb-2" > Mana Overview< / h5 >
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
{% set deck_colors = summary.colors or [] %}
2025-10-28 08:21:52 -07:00
< div class = "mana-row" >
2025-08-26 16:25:34 -07:00
<!-- Pips Panel -->
2025-10-28 08:21:52 -07:00
< div class = "mana-panel" >
< div class = "muted mana-panel-heading" > Mana Pips (non-lands)< / div >
2025-08-26 20:00:07 -07:00
{% set pd = summary.pip_distribution %}
2025-08-26 16:25:34 -07:00
{% if pd %}
{% set colors = deck_colors if deck_colors else ['W','U','B','R','G'] %}
2025-10-28 08:21:52 -07:00
< div class = "chart-bars" >
2025-08-26 16:25:34 -07:00
{% for color in colors %}
{% set w = (pd.weights[color] if pd.weights and color in pd.weights else 0) %}
{% set pct = (w * 100) | int %}
2025-10-28 08:21:52 -07:00
< div class = "chart-column" >
2025-10-08 11:38:30 -07:00
{% set count_val = (pd.counts[color] if pd.counts and color in pd.counts else 0) %}
{% set pc = pd['cards'] if 'cards' in pd else None %}
{% set c_cards = (pc[color] if pc and (color in pc) else []) %}
{% set parts = [] %}
{% for c in c_cards %}
{% set label = c.name ~ ((" × " ~ c.count) if c.count and c.count>1 else '') %}
{% if c.dfc %}
{% set label = label ~ ' (DFC)' %}
{% endif %}
{% set _ = parts.append(label) %}
{% endfor %}
{% set cards_line = parts|join(' • ') %}
{% set pct_f = (pd.weights[color] * 100) if pd.weights and color in pd.weights else 0 %}
2025-10-28 08:21:52 -07:00
< svg width = "28" height = "120" aria-label = "{{ color }} {{ pct }}%" class = "chart-svg" data-type = "pips" data-color = "{{ color }}" data-count = "{{ '%.1f' % count_val }}" data-pct = "{{ '%.1f' % pct_f }}" data-cards = "{{ cards_line }}" >
2025-10-08 11:38:30 -07:00
< rect x = "2" y = "2" width = "24" height = "116" fill = "#14171c" stroke = "var(--border)" rx = "4" ry = "4" pointer-events = "all" > < / rect >
2025-08-26 16:25:34 -07:00
{% set h = (pct * 1.0) | int %}
{% set bar_h = (h if h>2 else 2) %}
{% set y = 118 - bar_h %}
2025-10-08 11:38:30 -07:00
< rect x = "2" y = "{{ y }}" width = "24" height = "{{ bar_h }}" fill = "#3b82f6" rx = "4" ry = "4" pointer-events = "all" > < / rect >
2025-08-26 16:25:34 -07:00
< / svg >
2025-10-28 08:21:52 -07:00
< div class = "muted mt-1" > {{ color }}< / div >
2025-08-26 16:25:34 -07:00
< / div >
{% endfor %}
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
{% else %}
< div class = "muted" > No pip data.< / 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
< / div >
2025-08-26 16:25:34 -07:00
<!-- Sources Panel -->
2025-10-28 08:21:52 -07:00
< div class = "mana-panel" >
< div class = "flex items-center justify-between gap-3 mb-1" >
< div class = "muted mana-panel-heading" > Mana Sources< / div >
< label class = "muted text-xs form-label-icon" >
2025-08-26 20:00:07 -07:00
< input type = "checkbox" id = "toggle-show-c" / > Show colorless (C)
< / label >
< / div >
{% set mg = summary.mana_generation %}
2025-08-26 16:25:34 -07:00
{% if mg %}
{% set colors = deck_colors if deck_colors else ['W','U','B','R','G'] %}
2025-08-26 20:00:07 -07:00
{# If colorless sources exist, append 'C' to colors for display #}
{% if 'C' in mg and (mg.get('C', 0) > 0) and ('C' not in colors) %}
{% set colors = colors + ['C'] %}
{% endif %}
2025-08-26 16:25:34 -07:00
{% set ns = namespace(max_src=0) %}
{% for color in colors %}
{% set val = mg.get(color, 0) %}
{% if val > ns.max_src %}{% set ns.max_src = val %}{% endif %}
{% endfor %}
{% set denom = (ns.max_src if ns.max_src and ns.max_src > 0 else 1) %}
2025-10-28 08:21:52 -07:00
< div class = "sources-bars chart-bars" >
2025-08-26 16:25:34 -07:00
{% for color in colors %}
{% set val = mg.get(color, 0) %}
{% set pct = (val * 100 / denom) | int %}
2025-10-08 11:38:30 -07:00
{% set pct_f = (100.0 * (val / (mg.total_sources or 1))) %}
{% set mgc = mg['cards'] if 'cards' in mg else None %}
{% set c_cards = (mgc[color] if mgc and (color in mgc) else []) %}
{% set parts = [] %}
{% for c in c_cards %}
{% set _ = parts.append(c.name ~ ((" × " ~ c.count) if c.count and c.count>1 else '')) %}
{% endfor %}
{% set cards_line = parts|join(' • ') %}
2025-10-28 08:21:52 -07:00
< div class = "chart-column" data-color = "{{ color }}" >
< svg width = "28" height = "120" aria-label = "{{ color }} {{ val }}" class = "chart-svg" data-type = "sources" data-color = "{{ color }}" data-val = "{{ val }}" data-pct = "{{ '%.1f' % pct_f }}" data-cards = "{{ cards_line }}" >
2025-10-08 11:38:30 -07:00
< rect x = "2" y = "2" width = "24" height = "116" fill = "#14171c" stroke = "var(--border)" rx = "4" ry = "4" pointer-events = "all" > < / rect >
2025-08-26 16:25:34 -07:00
{% set bar_h = (pct if pct>2 else 2) %}
{% set y = 118 - bar_h %}
2025-10-08 11:38:30 -07:00
< rect x = "2" y = "{{ y }}" width = "24" height = "{{ bar_h }}" fill = "#10b981" rx = "4" ry = "4" pointer-events = "all" > < / rect >
2025-08-26 16:25:34 -07:00
< / svg >
2025-10-28 08:21:52 -07:00
< div class = "muted mt-1" > {{ color }}< / div >
2025-08-26 16:25:34 -07:00
< / div >
{% endfor %}
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-10-28 08:21:52 -07:00
< div class = "muted mt-1" > Total sources: {{ mg.total_sources or 0 }}< / div >
2025-08-26 16:25:34 -07:00
{% else %}
< div class = "muted" > No mana source data.< / 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
< / div >
2025-08-26 16:25:34 -07:00
<!-- Curve Panel -->
2025-10-28 08:21:52 -07:00
< div class = "mana-panel" >
< div class = "muted mana-panel-heading" > Mana Curve (non-lands)< / div >
2025-08-26 16:25:34 -07:00
{% set mc = summary.mana_curve %}
{% if mc %}
{% set ts = mc.total_spells or 0 %}
{% set denom = (ts if ts and ts > 0 else 1) %}
2025-10-28 08:21:52 -07:00
< div class = "chart-bars" >
2025-08-26 16:25:34 -07:00
{% for label in ['0','1','2','3','4','5','6+'] %}
{% set val = mc.get(label, 0) %}
{% set pct = (val * 100 / denom) | int %}
2025-10-08 11:38:30 -07:00
{% set cards = (mc.cards[label] if mc.cards and (label in mc.cards) else []) %}
{% set parts = [] %}
{% for c in cards %}
{% set _ = parts.append(c.name ~ ((" × " ~ c.count) if c.count and c.count>1 else '')) %}
{% endfor %}
{% set cards_line = parts|join(' • ') %}
{% set pct_f = (100.0 * (val / denom)) %}
2025-10-28 08:21:52 -07:00
< div class = "chart-column" >
< svg width = "28" height = "120" aria-label = "{{ label }} {{ val }}" class = "chart-svg" data-type = "curve" data-label = "{{ label }}" data-val = "{{ val }}" data-pct = "{{ '%.1f' % pct_f }}" data-cards = "{{ cards_line }}" >
2025-10-08 11:38:30 -07:00
< rect x = "2" y = "2" width = "24" height = "116" fill = "#14171c" stroke = "var(--border)" rx = "4" ry = "4" pointer-events = "all" > < / rect >
2025-08-26 16:25:34 -07:00
{% set bar_h = (pct if pct>2 else 2) %}
{% set y = 118 - bar_h %}
2025-10-08 11:38:30 -07:00
< rect x = "2" y = "{{ y }}" width = "24" height = "{{ bar_h }}" fill = "#f59e0b" rx = "4" ry = "4" pointer-events = "all" > < / rect >
2025-08-26 16:25:34 -07:00
< / svg >
2025-10-28 08:21:52 -07:00
< div class = "muted mt-1" > {{ label }}< / div >
2025-08-26 16:25:34 -07:00
< / div >
{% endfor %}
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-10-28 08:21:52 -07:00
< div class = "muted mt-1" > Total spells: {{ mc.total_spells or 0 }}< / div >
2025-08-26 16:25:34 -07:00
{% else %}
< div class = "muted" > No curve data.< / 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
< / div >
2025-08-26 16:25:34 -07:00
< / div >
2025-10-08 11:38:30 -07:00
< / div >
< / details >
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 >
<!-- Test Hand (7 random cards; duplicates allowed only for basic lands) -->
2025-10-28 08:21:52 -07:00
< section class = "summary-section-lg" >
2025-10-08 11:38:30 -07:00
< details class = "analytics-accordion" id = "test-hand-accordion" data-lazy-load data-analytics-type = "testhand" >
2025-10-28 08:21:52 -07:00
< summary class = "combo-summary" >
2025-10-08 11:38:30 -07:00
< span > Test Hand< / span >
2025-10-28 08:21:52 -07:00
< span class = "muted text-xs font-normal ml-2" > (draw 7 random cards)< / span >
2025-10-08 11:38:30 -07:00
< / summary >
2025-10-28 08:21:52 -07:00
< div class = "analytics-content mt-3" >
< h5 class = "flex items-center gap-3 flex-wrap mt-0 mb-1" > Test Hand
< span class = "muted text-xs font-normal" > Draw 7 at random (no repeats except for basic lands).< / span >
2025-09-11 14:54:35 -07:00
< / h5 >
2025-10-28 08:21:52 -07:00
< div class = "flex gap-2 items-center flex-wrap mb-2" >
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 = "button" id = "btn-new-hand" > New Hand< / button >
< / div >
2025-09-11 14:54:35 -07:00
< div class = "stack-wrap hand-row-overlap" id = "test-hand" >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< div class = "stack-grid" id = "test-hand-grid" > < / div >
< / div >
< script >
(function(){
var GC_SET = (function(){
try {
var els = document.querySelectorAll('#typeview-list .game-changer [data-card-name], #typeview-thumbs .game-changer [data-card-name]');
var s = new Set();
els.forEach(function(el){ var n = el.getAttribute('data-card-name'); if(n) s.add(n); });
return s;
} catch(e) { return new Set(); }
})();
var BASE_BASICS = ["Plains","Island","Swamp","Mountain","Forest","Wastes"];
function isBasicLand(name){
if (!name) return false;
if (BASE_BASICS.indexOf(name) >= 0) return true;
if (name.startsWith('Snow-Covered ')) {
var base = name.substring('Snow-Covered '.length);
return BASE_BASICS.indexOf(base) >= 0;
}
return false;
}
function collectDeck(){
var deck = [];
document.querySelectorAll('#typeview-list span[data-card-name]').forEach(function(el){
var name = el.getAttribute('data-card-name');
var cnt = parseInt(el.getAttribute('data-count') || '1', 10);
if (name) deck.push({ name: name, count: (isFinite(cnt) & & cnt>0 ? cnt : 1) });
});
return deck;
}
function buildPool(deck){
var pool = [];
deck.forEach(function(it){
var n = Math.max(1, parseInt(it.count || 1, 10));
for (var i=0;i< n ; i + + ) { pool . push ( it . name ) ; }
});
return pool;
}
function drawHand(deck){
var pool = buildPool(deck);
if (!pool.length) return [];
var picked = {};
var hand = [];
var attempts = 0;
while (hand.length < 7 & & attempts < 500 ) {
attempts++;
var idx = Math.floor(Math.random() * pool.length);
var name = pool[idx];
if (!name) continue;
var allowDup = isBasicLand(name);
if (!allowDup & & picked[name]) continue;
hand.push(name);
if (!allowDup) picked[name] = true;
pool.splice(idx, 1);
if (!pool.length) break;
}
return hand;
}
function compress(hand){
var map = {};
hand.forEach(function(n){ map[n] = (map[n]||0) + 1; });
var out = [];
Object.keys(map).forEach(function(n){ out.push({name:n, count: map[n]}); });
out.sort(function(a,b){ return hand.indexOf(a.name) - hand.indexOf(b.name); });
return out;
}
function render(hand){
var grid = document.getElementById('test-hand-grid');
if (!grid) return;
grid.innerHTML = '';
2025-09-11 14:54:35 -07:00
var host = document.getElementById('test-hand');
if (host){ host.style.setProperty('--mid', (hand.length ? (hand.length - 1)/2 : 0)); host.style.setProperty('--count', hand.length); }
hand.forEach(function(name, idx){
2025-08-26 16:25:34 -07:00
if (!name) return;
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 div = document.createElement('div');
div.className = 'stack-card';
2025-08-26 16:25:34 -07:00
if (GC_SET & & GC_SET.has(name)) {
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.className += ' game-changer';
}
2025-09-11 14:54:35 -07:00
div.style.setProperty('--i', idx);
var mid = (hand.length - 1) / 2;
var diff = Math.abs(idx - mid);
var peakRaise = 22; // px raise at center (accentuate arc)
var dropPer = 5; // linear component per distance step
// Strengthen curve so the very outer cards sit lower
var outerExtra = 24; // quadratic downward px strongest at edges
var edgeBias = 8; // cubic bias for far edges
var norm = (mid > 0 ? diff / mid : 0); // 0..1
var curve = norm * norm * outerExtra; // quadratic easing
var curve3 = norm * norm * norm * edgeBias; // cubic accentuation
var y = (diff * dropPer) + curve + curve3 - peakRaise; // center negative (raised), edges positive (lower)
// Minor smoothing so second-from-edge isn't too low
if (mid >= 2 & & Math.abs(diff - (mid - 1)) < 0.001 ) { y + = 2 ; }
div.style.setProperty('--ty', y + 'px');
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.innerHTML = (
2025-10-28 08:21:52 -07:00
'< img src = "/api/images/normal/' + encodeURIComponent(name) + '" alt = "' + name + '" data-card-name = "' + name + '" / > '
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
);
grid.appendChild(div);
});
}
function newHand(){ var deck = collectDeck(); render(drawHand(deck)); }
var btn = document.getElementById('btn-new-hand');
if (btn) btn.addEventListener('click', newHand);
2025-09-11 14:54:35 -07:00
// Fan effect — desktop default (>=900px, hover-capable pointer)
var handEl = document.getElementById('test-hand');
(function(){
if(!handEl) return;
var onEnter = function(){ handEl.classList.add('fan'); };
var onLeave = function(){ handEl.classList.remove('fan'); };
var mq = window.matchMedia('(any-hover: hover) and (pointer: fine) and (min-width: 900px)');
function attach(){ handEl.addEventListener('mouseenter', onEnter); handEl.addEventListener('mouseleave', onLeave); }
function detach(){ handEl.removeEventListener('mouseenter', onEnter); handEl.removeEventListener('mouseleave', onLeave); }
// Desktop: fan is default; Mobile/tablet: no fan
function apply(){
if (mq.matches) {
detach();
handEl.classList.add('fan');
} else {
detach();
handEl.classList.remove('fan');
}
}
try {
if (typeof mq.addEventListener === 'function') mq.addEventListener('change', apply);
else if (typeof mq.addListener === 'function') mq.addListener(apply);
} catch(_) {}
apply();
})();
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
newHand();
})();
< / script >
2025-09-11 14:54:35 -07:00
< style >
/* Base overlapping hand: 160px cards (same as deck thumbnails) */
#test-hand.hand-row-overlap{ padding-bottom:.9rem; --fan-gap:28px; --card-w:160px; --card-h:224px; }
#test-hand.hand-row-overlap .stack-grid{ display:flex !important; gap:0; overflow-x:auto; scrollbar-width:thin; }
#test-hand.hand-row-overlap .stack-card{ width:var(--card-w); height:var(--card-h); transition: transform .25s ease, margin-left .25s ease, width .25s ease, height .25s ease; flex: 0 0 auto; }
/* Dynamic overlap: show ~30% of next card */
#test-hand.hand-row-overlap .stack-card + .stack-card{ margin-left: calc(var(--card-w) * -0.7); }
#test-hand.hand-row-overlap .stack-card img{ width:var(--card-w); height:var(--card-h); display:block; }
#test-hand.hand-row-overlap .stack-card:hover{ z-index:999; transform:translateY(-4px); }
/* Desktop sizing for Test Hand */
@media (min-width:900px){
#test-hand.hand-row-overlap{ --card-w:280px; --card-h:392px; --fan-gap:40px; }
#test-hand.hand-row-overlap .stack-card + .stack-card{ margin-left: calc(var(--card-w) * -0.7); }
#test-hand.hand-row-overlap.fan{ --card-w:280px; --card-h:392px; }
}
/* Hover fan-out: spread cards horizontally, enlarge if not already large */
#test-hand.hand-row-overlap.fan{ --fan-overlap:0.40; --fan-gap:0px; }
#test-hand.hand-row-overlap.fan .stack-card + .stack-card{ margin-left:0; }
#test-hand.hand-row-overlap.fan .stack-grid{ justify-content:center; overflow:visible; padding-left:0; }
/* Fan transform now applies a 40% overlap (visible width ~60%) while keeping center aligned */
#test-hand.hand-row-overlap.fan .stack-card{ position:relative; transform: translateX(calc((var(--i) - var(--mid)) * (var(--fan-gap) - (var(--card-w) * var(--fan-overlap))))) translateY(var(--ty,0px)) rotate(calc((var(--i) - var(--mid)) * 4deg)); }
/* Responsive adjustments */
@media (max-width:900px){
#test-hand.hand-row-overlap.fan{ --card-w:240px; --card-h:336px; --fan-overlap:0.40; --fan-gap:0px; }
}
@media (max-width:640px){
#test-hand.hand-row-overlap{ --card-w:150px; --card-h:210px; }
#test-hand.hand-row-overlap.fan{ --card-w:200px; --card-h:280px; --fan-overlap:0.40; --fan-gap:0px; }
}
@media (min-width:640px) and (max-width:899px){
#test-hand.hand-row-overlap{ --card-w:160px; --card-h:224px; }
}
@media (max-width:480px){
#test-hand.hand-row-overlap{ --card-w:140px; --card-h:196px; }
#test-hand.hand-row-overlap .stack-card + .stack-card{ margin-left: calc(var(--card-w) * -0.65); }
#test-hand.hand-row-overlap.fan{ --card-w:180px; --card-h:252px; --fan-overlap:0.40; --fan-gap:0px; }
#test-hand.hand-row-overlap.fan .stack-grid{ padding-left:0; }
}
< / style >
2025-10-08 11:38:30 -07:00
< / div >
< / details >
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 >
< style >
2025-10-08 11:38:30 -07:00
.chart-tooltip { position: fixed; background: #0f1115; color: #e5e7eb; border: 1px solid var(--border); padding: .4rem .55rem; border-radius: 6px; font-size: 12px; line-height: 1.3; white-space: pre-line; z-index: 9999; display: none; box-shadow: 0 4px 16px rgba(0,0,0,.4); max-width: 90vw; }
/* Pinned tooltip gets pointer events for Copy button */
.chart-tooltip.pinned { pointer-events: auto; border-color: #f59e0b; box-shadow: 0 4px 20px rgba(245,158,11,.3); }
/* Unpinned tooltip has no pointer events (hover only) */
.chart-tooltip:not(.pinned) { pointer-events: none; }
2025-08-26 20:00:07 -07:00
/* Cross-highlight from charts to cards */
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
.chart-highlight { border-radius: 6px; background: rgba(245,158,11,.08); box-shadow: 0 0 0 2px #f59e0b inset; }
/* For list view, ensure baseline padding so no layout shift on highlight */
#typeview-list .list-row .name { display:inline-block; padding: 2px 4px; border-radius: 6px; }
2025-08-26 20:00:07 -07:00
/* Ensure stack-card gets visible highlight */
.stack-card.chart-highlight { box-shadow: 0 0 0 2px #f59e0b, 0 6px 18px rgba(0,0,0,.55); }
2025-10-08 11:38:30 -07:00
/* Chart columns get cursor pointer */
.chart-column svg { cursor: pointer; transition: opacity 0.15s ease; }
.chart-column svg:hover { opacity: 0.85; }
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< / style >
< script >
(function() {
function ensureTip() {
var tip = document.getElementById('chart-tooltip');
if (!tip) {
tip = document.createElement('div');
tip.id = 'chart-tooltip';
tip.className = 'chart-tooltip';
document.body.appendChild(tip);
}
return tip;
}
2025-08-26 20:00:07 -07:00
var tip = ensureTip();
var hoverTimer = null;
var lastNames = [];
var lastType = '';
2025-10-08 11:38:30 -07:00
var pinnedNames = [];
var pinnedType = '';
var pinnedEl = null;
2025-08-26 20:00:07 -07:00
function clearHoverTimer(){ if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } }
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
function position(e) {
tip.style.display = 'block';
var vw = window.innerWidth || document.documentElement.clientWidth;
var vh = window.innerHeight || document.documentElement.clientHeight;
2025-10-08 11:38:30 -07:00
var isMobile = vw < 768 ;
if (isMobile) {
// Mobile: fixed to lower-right corner
tip.style.right = '8px';
tip.style.bottom = '8px';
tip.style.left = 'auto';
tip.style.top = 'auto';
tip.style.maxWidth = 'calc(100vw - 16px)';
} else {
// Desktop: fixed to lower-left corner
tip.style.left = '8px';
tip.style.bottom = '8px';
tip.style.right = 'auto';
tip.style.top = 'auto';
tip.style.maxWidth = '400px';
}
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-10-08 11:38:30 -07:00
function buildTip(el, isPinned) {
// Render tooltip with safe DOM
2025-08-26 20:00:07 -07:00
tip.innerHTML = '';
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 t = el.getAttribute('data-type');
2025-08-26 20:00:07 -07:00
var header = document.createElement('div');
header.style.fontWeight = '600';
header.style.marginBottom = '.25rem';
2025-10-08 11:38:30 -07:00
header.style.display = 'flex';
header.style.alignItems = 'center';
header.style.justifyContent = 'space-between';
header.style.gap = '.5rem';
var titleSpan = document.createElement('span');
2025-08-26 20:00:07 -07:00
var listText = '';
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 (t === 'pips') {
2025-10-08 11:38:30 -07:00
titleSpan.textContent = el.dataset.color + ': ' + (el.dataset.count || '0') + ' (' + (el.dataset.pct || '0') + '%)';
2025-08-26 20:00:07 -07:00
listText = (el.dataset.cards || '').split(' • ').filter(Boolean).join('\n');
} else if (t === 'sources') {
2025-10-08 11:38:30 -07:00
titleSpan.textContent = el.dataset.color + ': ' + (el.dataset.val || '0') + ' (' + (el.dataset.pct || '0') + '%)';
2025-08-26 20:00:07 -07:00
listText = (el.dataset.cards || '').split(' • ').filter(Boolean).join('\n');
} else if (t === 'curve') {
2025-10-08 11:38:30 -07:00
titleSpan.textContent = el.dataset.label + ': ' + (el.dataset.val || '0') + ' (' + (el.dataset.pct || '0') + '%)';
2025-08-26 20:00:07 -07:00
listText = (el.dataset.cards || '').split(' • ').filter(Boolean).join('\n');
} else {
2025-10-08 11:38:30 -07:00
titleSpan.textContent = el.getAttribute('aria-label') || '';
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-10-08 11:38:30 -07:00
header.appendChild(titleSpan);
// Add Copy button that works with pinned tooltips
if (listText & & isPinned) {
2025-08-26 20:00:07 -07:00
var btn = document.createElement('button');
btn.textContent = 'Copy';
2025-10-08 11:38:30 -07:00
btn.style.fontSize = '11px';
btn.style.padding = '.15rem .35rem';
2025-08-26 20:00:07 -07:00
btn.style.border = '1px solid var(--border)';
btn.style.background = '#12161c';
btn.style.color = '#e5e7eb';
btn.style.borderRadius = '4px';
2025-10-08 11:38:30 -07:00
btn.style.cursor = 'pointer';
btn.style.flexShrink = '0';
2025-08-26 20:00:07 -07:00
btn.addEventListener('click', function(e){
e.stopPropagation();
try {
if (navigator.clipboard & & navigator.clipboard.writeText) {
navigator.clipboard.writeText(listText);
} else {
var ta = document.createElement('textarea');
ta.value = listText; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta);
}
btn.textContent = 'Copied!';
setTimeout(function(){ btn.textContent = 'Copy'; }, 1200);
} catch(_) {}
});
2025-10-08 11:38:30 -07:00
header.appendChild(btn);
}
tip.appendChild(header);
if (listText) {
var pre = document.createElement('pre');
pre.style.margin = '.25rem 0 0 0';
pre.style.whiteSpace = 'pre-wrap';
pre.style.fontSize = '12px';
pre.textContent = listText;
tip.appendChild(pre);
}
// Add hint for pinning on desktop
if (!isPinned & & window.innerWidth >= 768) {
var hint = document.createElement('div');
hint.style.marginTop = '.35rem';
hint.style.fontSize = '11px';
hint.style.color = '#9ca3af';
hint.style.fontStyle = 'italic';
hint.textContent = 'Click to pin';
tip.appendChild(hint);
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 20:00:07 -07:00
}
function normalizeList(list) {
if (!Array.isArray(list)) return [];
return list.map(function(n){
if (!n) return '';
var s = String(n);
// Strip trailing " × < num > " count suffix if present
s = s.replace(/\s× \d+$/,'');
return s.trim();
}).filter(Boolean);
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-10-08 11:38:30 -07:00
function unpin() {
if (pinnedEl) {
pinnedEl.style.outline = '';
pinnedEl = null;
}
if (pinnedNames & & pinnedNames.length) {
highlightNames(pinnedNames, false);
}
pinnedNames = [];
pinnedType = '';
tip.classList.remove('pinned');
tip.style.display = 'none';
}
function pin(el, e) {
// Unpin previous if different element
if (pinnedEl & & pinnedEl !== el) {
unpin();
}
// Toggle: if clicking same element, unpin
if (pinnedEl === el) {
unpin();
return;
}
// Pin new element
pinnedEl = el;
el.style.outline = '2px solid #f59e0b';
el.style.outlineOffset = '2px';
var dataType = el.getAttribute('data-type');
pinnedNames = normalizeList((el.dataset.cards || '').split(' • ').filter(Boolean));
pinnedType = dataType;
tip.classList.add('pinned');
buildTip(el, true);
position(e);
highlightNames(pinnedNames, 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
function attach() {
2025-10-08 11:38:30 -07:00
// Attach to SVG elements with data-type for better hover zones
document.querySelectorAll('svg[data-type]').forEach(function(el) {
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
el.addEventListener('mouseenter', function(e) {
2025-10-08 11:38:30 -07:00
// Don't show hover tooltip if this element is pinned
if (pinnedEl === el) return;
clearHoverTimer();
buildTip(el, false);
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
position(e);
2025-08-26 20:00:07 -07:00
// Cross-highlight for mana curve bars -> card items
try {
2025-10-08 11:38:30 -07:00
var dataType = el.getAttribute('data-type');
if (dataType === 'curve' || dataType === 'pips' || dataType === 'sources') {
2025-08-26 20:00:07 -07:00
lastNames = normalizeList((el.dataset.cards || '').split(' • ').filter(Boolean));
2025-10-08 11:38:30 -07:00
lastType = dataType;
// Only apply hover highlights if nothing is pinned
if (!pinnedEl) {
highlightNames(lastNames, true);
}
2025-08-26 20:00:07 -07:00
}
} 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
});
2025-10-08 11:38:30 -07:00
el.addEventListener('mousemove', function(e) {
if (pinnedEl === el) return;
position(e);
});
2025-08-26 20:00:07 -07:00
el.addEventListener('mouseleave', function() {
2025-10-08 11:38:30 -07:00
// Don't hide if pinned
if (pinnedEl) return;
2025-08-26 20:00:07 -07:00
clearHoverTimer();
hoverTimer = setTimeout(function(){
tip.style.display = 'none';
2025-10-08 11:38:30 -07:00
try { if (lastNames & & lastNames.length & & !pinnedEl) highlightNames(lastNames, false); } catch(_) {}
2025-08-26 20:00:07 -07:00
lastNames = []; lastType = '';
}, 200);
});
2025-10-08 11:38:30 -07:00
el.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
pin(el, e);
});
});
// Keep tooltip open while hovering it (for pinned tooltips with Copy button)
tip.addEventListener('mouseenter', function(){
clearHoverTimer();
2025-08-26 20:00:07 -07:00
});
tip.addEventListener('mouseleave', function(){
2025-10-08 11:38:30 -07:00
// Don't hide if pinned
if (pinnedEl) return;
2025-08-26 20:00:07 -07:00
tip.style.display = 'none';
try { if (lastNames & & lastNames.length) highlightNames(lastNames, false); } catch(_) {}
lastNames = []; lastType = '';
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-10-08 11:38:30 -07:00
// Click outside to unpin
document.addEventListener('click', function(e) {
if (!pinnedEl) return;
// Don't unpin if clicking the tooltip itself or a chart
if (tip.contains(e.target) || e.target.closest('svg[data-type]')) return;
unpin();
});
2025-08-26 20:00:07 -07:00
// Initialize Show C toggle
initShowCToggle();
}
function initShowCToggle(){
var cb = document.getElementById('toggle-show-c');
var container = document.querySelector('.sources-bars');
if (!cb || !container) return;
// Default ON; restore prior state
var pref = 'true';
try { var v = localStorage.getItem('showColorlessC'); if (v!==null) pref = v; } catch(_) {}
cb.checked = (pref !== 'false');
function apply(){
var on = cb.checked;
try { localStorage.setItem('showColorlessC', String(on)); } catch(_) {}
container.querySelectorAll('[data-color="C"]').forEach(function(el){
el.style.display = on ? '' : 'none';
});
}
cb.addEventListener('change', apply);
apply();
}
function highlightNames(names, on){
if (!Array.isArray(names) || names.length === 0) return;
2025-10-08 11:38:30 -07:00
// List view spans - target only the .name span, not the parent .list-row
2025-08-26 20:00:07 -07:00
try {
2025-10-08 11:38:30 -07:00
document.querySelectorAll('#typeview-list .list-row .name[data-card-name]').forEach(function(it){
2025-08-26 20:00:07 -07:00
var n = it.getAttribute('data-card-name');
if (!n) return;
var match = names.indexOf(n) !== -1;
// Toggle highlight only on the inline span so it doesn't fill the entire grid cell
it.classList.toggle('chart-highlight', !!(on & & match));
if (!on & & !match) it.classList.remove('chart-highlight');
});
} catch(_) {}
2025-08-28 14:57:22 -07:00
// Thumbs view images
2025-08-26 20:00:07 -07:00
try {
document.querySelectorAll('#typeview-thumbs [data-card-name]').forEach(function(it){
var n = it.getAttribute('data-card-name');
if (!n) return;
var tile = it.closest('.stack-card') || it;
var match = names.indexOf(n) !== -1;
tile.classList.toggle('chart-highlight', !!(on & & match));
if (!on & & !match) tile.classList.remove('chart-highlight');
});
} catch(_) {}
2025-08-28 14:57:22 -07:00
// If virtualized lists are enabled, auto-scroll the Step 5 grid to the first match
try {
if (on & & window.scrollCardIntoView & & Array.isArray(names) & & names.length) {
window.scrollCardIntoView(names[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
}
attach();
document.addEventListener('htmx:afterSwap', function() { attach(); });
})();
2025-10-08 11:38:30 -07:00
< / script >
< / div >