mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-09-21 20:40:47 +02:00
383 lines
27 KiB
HTML
383 lines
27 KiB
HTML
<section>
|
||
{# Step phases removed #}
|
||
<div class="two-col two-col-left-rail">
|
||
<aside class="card-preview">
|
||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" data-card-name="{{ commander }}" loading="lazy" decoding="async" data-lqip="1"
|
||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=large 672w"
|
||
sizes="(max-width: 900px) 100vw, 320px" />
|
||
</a>
|
||
{% if status and status.startswith('Build complete') %}
|
||
<div style="margin-top:.75rem; display:flex; gap:.35rem; flex-wrap:wrap;">
|
||
{% if csv_path %}
|
||
<form action="/files" method="get" target="_blank" style="display:inline; margin:0;">
|
||
<input type="hidden" name="path" value="{{ csv_path }}" />
|
||
<button type="submit">Download CSV</button>
|
||
</form>
|
||
{% endif %}
|
||
{% if txt_path %}
|
||
<form action="/files" method="get" target="_blank" style="display:inline; margin:0;">
|
||
<input type="hidden" name="path" value="{{ txt_path }}" />
|
||
<button type="submit">Download TXT</button>
|
||
</form>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</aside>
|
||
<div class="grow" data-skeleton>
|
||
<div hx-get="/build/banner" hx-trigger="load"></div>
|
||
<div hx-get="/build/multicopy/check" hx-trigger="load" hx-swap="afterend"></div>
|
||
|
||
<p>Commander: <strong>{{ commander }}</strong></p>
|
||
<p>Tags: {{ tags|default([])|join(', ') }}</p>
|
||
<div style="margin:.35rem 0; color: var(--muted); display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;">
|
||
<span>Owned-only: <strong>{{ 'On' if owned_only else 'Off' }}</strong></span>
|
||
<div style="display:flex;align-items:center;gap:1rem;">
|
||
<button type="button" hx-get="/build/step4" hx-target="#wizard" hx-swap="innerHTML" style="background:#374151; color:#e5e7eb; border:none; border-radius:6px; padding:.25rem .5rem; cursor:pointer; font-size:12px;" title="Change owned settings in Review">Edit in Review</button>
|
||
<div>Prefer-owned: <strong>{{ 'On' if prefer_owned else 'Off' }}</strong></div>
|
||
</div>
|
||
<span style="margin-left:auto;"><a href="/owned" target="_blank" rel="noopener" class="btn">Manage Owned Library</a></span>
|
||
</div>
|
||
<p>Bracket: {{ bracket }}</p>
|
||
|
||
<div style="display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; margin:.25rem 0 .5rem 0;">
|
||
{% if i and n %}
|
||
<span class="chip"><span class="dot"></span> Stage {{ i }}/{{ n }}</span>
|
||
{% endif %}
|
||
{% set deck_count = (total_cards if total_cards is not none else 0) %}
|
||
<span class="chip"><span class="dot" style="background: var(--green-main);"></span> Deck {{ deck_count }}/100</span>
|
||
{% if added_total is not none %}
|
||
<span class="chip"><span class="dot" style="background: var(--blue-main);"></span> Added {{ added_total }}</span>
|
||
{% endif %}
|
||
{% if prefer_combos %}
|
||
<span class="chip" title="Combos plan"><span class="dot" style="background: var(--orange-main);"></span> Combos: {{ combo_target_count }} ({{ combo_balance }})</span>
|
||
{% endif %}
|
||
{% if clamped_overflow is defined and clamped_overflow and (clamped_overflow > 0) %}
|
||
<span class="chip" title="Trimmed overflow from this stage"><span class="dot" style="background: var(--red-main);"></span> Clamped {{ clamped_overflow }}</span>
|
||
{% endif %}
|
||
{% if stage_label and stage_label == 'Multi-Copy Package' and mc_summary is defined and mc_summary %}
|
||
<span class="chip" title="Multi-Copy package summary"><span class="dot" style="background: var(--purple-main);"></span> {{ mc_summary }}</span>
|
||
{% endif %}
|
||
<span id="locks-chip">{% if locks and locks|length > 0 %}<span class="chip" title="Locked cards">🔒 {{ locks|length }} locked</span>{% endif %}</span>
|
||
<button type="button" class="btn" style="margin-left:auto;" title="Copy permalink"
|
||
onclick="(async()=>{try{const r=await fetch('/build/permalink');const j=await r.json();const url=(j.permalink?location.origin+j.permalink:location.href+'#'+btoa(JSON.stringify(j.state||{}))); await navigator.clipboard.writeText(url); toast && toast('Permalink copied');}catch(e){alert('Copied state to console'); console.log(e);}})()">Copy Permalink</button>
|
||
<button type="button" class="btn" title="Open a saved permalink" onclick="(function(){try{var token = prompt('Paste a /build/from?state=... URL or token:'); if(!token) return; var m = token.match(/state=([^&]+)/); var t = m? m[1] : token.trim(); if(!t) return; window.location.href = '/build/from?state=' + encodeURIComponent(t); }catch(_){}})()">Open Permalink…</button>
|
||
</div>
|
||
{% set pct = ((deck_count / 100.0) * 100.0) if deck_count else 0 %}
|
||
{% set pct_clamped = (pct if pct <= 100 else 100) %}
|
||
{% set pct_int = pct_clamped|int %}
|
||
<div class="progress{% if added_cards is defined and added_cards is not none and (added_cards|length == 0) and (status and not status.startswith('Build complete')) %} flash{% endif %}" aria-label="Deck progress" title="{{ deck_count }} of 100 cards" style="margin:.25rem 0 1rem 0;" data-pct="{{ pct_int }}">
|
||
<div class="bar"></div>
|
||
</div>
|
||
|
||
{% if mc_adjustments is defined and mc_adjustments and stage_label and stage_label == 'Multi-Copy Package' %}
|
||
<div class="muted" style="margin:.35rem 0 .25rem 0;">Adjusted targets: {{ mc_adjustments|join(', ') }}</div>
|
||
{% endif %}
|
||
|
||
{% if status %}
|
||
<div style="margin-top:1rem;">
|
||
<strong>Status:</strong> {{ status }}{% if stage_label %} — <em>{{ stage_label }}</em>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if status and status.startswith('Build complete') %}
|
||
<div hx-get="/build/combos" hx-trigger="load" hx-swap="afterend"></div>
|
||
{% endif %}
|
||
|
||
{% if locked_cards is defined and locked_cards %}
|
||
<details id="locked-section" style="margin-top:.5rem;">
|
||
<summary>Locked cards (always kept)</summary>
|
||
<ul id="locked-list" style="list-style:none; padding:0; margin:.35rem 0 0; display:grid; gap:.35rem;">
|
||
{% for lk in locked_cards %}
|
||
<li style="display:flex; align-items:center; gap:.5rem; flex-wrap:wrap;">
|
||
<span class="chip"><span class="dot"></span> {{ lk.name }}</span>
|
||
<span class="muted">{% if lk.owned %}✔ Owned{% else %}✖ Not owned{% endif %}</span>
|
||
{% if lk.in_deck %}<span class="muted">• In deck</span>{% else %}<span class="muted">• Will be included on rerun</span>{% endif %}
|
||
<form hx-post="/build/lock" hx-target="closest li" hx-swap="outerHTML" onsubmit="try{toast('Unlocked {{ lk.name }}');}catch(_){}" style="display:inline; margin-left:auto;">
|
||
<input type="hidden" name="name" value="{{ lk.name }}" />
|
||
<input type="hidden" name="locked" value="0" />
|
||
<input type="hidden" name="from_list" value="1" />
|
||
<button type="submit" class="btn" title="Unlock" aria-pressed="true">Unlock</button>
|
||
</form>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</details>
|
||
{% endif %}
|
||
|
||
<!-- Last action chip (oob-updated) -->
|
||
<div id="last-action" aria-live="polite" style="margin:.25rem 0; min-height:1.5rem;"></div>
|
||
|
||
<!-- Filters toolbar -->
|
||
<div class="cards-toolbar">
|
||
<input type="text" name="filter_query" placeholder="Filter by name, role, or tag" data-pref="cards:filter_q" />
|
||
<select name="filter_owned" data-pref="cards:owned">
|
||
<option value="all">All</option>
|
||
<option value="owned">Owned</option>
|
||
<option value="not">Not owned</option>
|
||
</select>
|
||
<label style="display:flex;align-items:center;gap:.35rem;">
|
||
<input type="checkbox" name="show_reasons" data-pref="cards:show_reasons" checked /> Show reasons
|
||
</label>
|
||
<label style="display:flex;align-items:center;gap:.35rem;">
|
||
<input type="checkbox" name="collapse_groups" data-pref="cards:collapse" /> Collapse groups
|
||
</label>
|
||
<select name="filter_sort" data-pref="cards:sort" aria-label="Sort">
|
||
<option value="az">A–Z</option>
|
||
<option value="owned">Owned first</option>
|
||
<option value="gc">Game-changers first</option>
|
||
</select>
|
||
<span class="sep"></span>
|
||
<span class="hint">Visible: <strong data-results>0</strong></span>
|
||
<span class="sep"></span>
|
||
<div class="chips-inline">
|
||
<span class="chip" data-chip-owned="all">All</span>
|
||
<span class="chip" data-chip-owned="owned">Owned</span>
|
||
<span class="chip" data-chip-owned="not">Not owned</span>
|
||
<span class="chip" data-chip-clear>Clear</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sticky build controls on mobile -->
|
||
<div class="build-controls" style="position:sticky; top:0; z-index:5; background:linear-gradient(180deg, rgba(15,17,21,.95), rgba(15,17,21,.85)); border:1px solid var(--border); border-radius:10px; padding:.5rem; margin-top:1rem; display:flex; gap:.5rem; flex-wrap:wrap; align-items:center;">
|
||
<form hx-post="/build/step5/start" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; margin-right:.5rem; display:flex; align-items:center; gap:.5rem;" onsubmit="try{ toast('Restarting build…'); }catch(_){}">
|
||
<input type="hidden" name="show_skipped" value="{{ '1' if show_skipped else '0' }}" />
|
||
<button type="submit" class="btn-continue" data-action="continue">Restart Build</button>
|
||
</form>
|
||
<form hx-post="/build/step5/continue" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; display:flex; align-items:center; gap:.5rem;" onsubmit="try{ toast('Continuing…'); }catch(_){}">
|
||
<input type="hidden" name="show_skipped" value="{{ '1' if show_skipped else '0' }}" />
|
||
<button type="submit" class="btn-continue" data-action="continue" {% if status and status.startswith('Build complete') %}disabled{% endif %}>Continue</button>
|
||
</form>
|
||
<form hx-post="/build/step5/rerun" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; display:flex; align-items:center; gap:.5rem;" onsubmit="try{ toast('Rerunning stage…'); }catch(_){}">
|
||
<input type="hidden" name="show_skipped" value="{{ '1' if show_skipped else '0' }}" />
|
||
<button type="submit" class="btn-rerun" data-action="rerun" {% if status and status.startswith('Build complete') %}disabled{% endif %}>Rerun Stage</button>
|
||
</form>
|
||
<span class="sep"></span>
|
||
<div class="replace-toggle" role="group" aria-label="Replace toggle">
|
||
<form hx-post="/build/step5/toggle-replace" hx-target="closest .replace-toggle" hx-swap="outerHTML" onsubmit="return false;" style="display:inline;">
|
||
<input type="hidden" name="replace" value="{{ '1' if replace_mode else '0' }}" />
|
||
<label class="muted" style="display:flex; align-items:center; gap:.35rem;" title="When enabled, reruns of this stage will replace its picks with alternatives instead of keeping them.">
|
||
<input type="checkbox" name="replace_chk" value="1" {% if replace_mode %}checked{% endif %}
|
||
onchange="try{ const f=this.form; const h=f.querySelector('input[name=replace]'); if(h){ h.value=this.checked?'1':'0'; } f.requestSubmit(); }catch(_){ }" />
|
||
Replace stage picks
|
||
</label>
|
||
</form>
|
||
</div>
|
||
<form hx-post="/build/step5/reset-stage" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; display:flex; align-items:center; gap:.5rem;">
|
||
<button type="submit" class="btn" title="Reset this stage to pre-stage picks">Reset stage</button>
|
||
</form>
|
||
<form hx-post="/build/reset-all" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; display:flex; align-items:center; gap:.5rem;">
|
||
<button type="submit" class="btn" title="Start a brand new build (clears selections)">New build</button>
|
||
</form>
|
||
<label class="muted" style="display:flex; align-items:center; gap:.35rem; margin-left: .5rem;">
|
||
<input type="checkbox" name="__toggle_show_skipped" data-pref="build:show_skipped" {% if show_skipped %}checked{% endif %}
|
||
onchange="const val=this.checked?'1':'0'; for(const f of this.closest('section').querySelectorAll('form')){ const h=f.querySelector('input[name=show_skipped]'); if(h) h.value=val; }" />
|
||
Show skipped stages
|
||
</label>
|
||
<button type="button" class="btn-back" data-action="back" hx-get="/build/step4" hx-target="#wizard" hx-swap="innerHTML">Back</button>
|
||
</div>
|
||
|
||
{% if added_cards is not none %}
|
||
{% if history is defined and history %}
|
||
<details style="margin-top:.5rem;">
|
||
<summary>Stage timeline</summary>
|
||
<div class="muted" style="font-size:12px; margin:.25rem 0 .35rem 0;">Jump back to a previous stage, then you can continue forward again.</div>
|
||
<ul style="list-style:none; padding:0; margin:0; display:grid; gap:.25rem;">
|
||
{% for h in history %}
|
||
<li style="display:flex; align-items:center; gap:.5rem;">
|
||
<span class="chip"><span class="dot"></span> {{ h.label }}</span>
|
||
<form hx-post="/build/step5/rewind" hx-target="#wizard" hx-swap="innerHTML" style="display:inline; margin:0;">
|
||
<input type="hidden" name="to" value="{{ h.i }}" />
|
||
<button type="submit" class="btn">Go</button>
|
||
</form>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</details>
|
||
{% endif %}
|
||
<h4 style="margin-top:1rem;">Cards added this stage</h4>
|
||
{% if skipped and (not added_cards or added_cards|length == 0) %}
|
||
<div class="muted" style="margin:.25rem 0 .5rem 0;">No cards added in this stage.</div>
|
||
{% endif %}
|
||
<div class="muted" style="font-size:12px; margin:.15rem 0 .4rem 0; display:flex; gap:.75rem; align-items:center; flex-wrap:wrap;">
|
||
<span><span style="display:inline-block; border:1px solid var(--border); background:rgba(17,24,39,.9); color:#e5e7eb; border-radius:12px; font-size:12px; line-height:18px; height:18px; min-width:18px; padding:0 6px; text-align:center;">✔</span> Owned</span>
|
||
<span><span style="display:inline-block; border:1px solid var(--border); background:rgba(17,24,39,.9); color:#e5e7eb; border-radius:12px; font-size:12px; line-height:18px; height:18px; min-width:18px; padding:0 6px; text-align:center;">✖</span> Not owned</span>
|
||
</div>
|
||
|
||
{% if stage_label and stage_label.startswith('Creatures') %}
|
||
{% set groups = added_cards|groupby('sub_role') %}
|
||
{% for g in groups %}
|
||
{% set group_idx = loop.index0 %}
|
||
{% set role = g.grouper %}
|
||
{% if role %}
|
||
{% set heading = 'Theme: ' + role.title() %}
|
||
{% else %}
|
||
{% set heading = 'Additional Picks' %}
|
||
{% endif %}
|
||
<div class="group" data-group-key="{{ (role or 'other')|lower|replace(' ', '-') }}">
|
||
<div class="group-header">
|
||
<h5 style="margin:.5rem 0 .25rem 0;">{{ heading }}</h5>
|
||
<span class="count">(<span data-count>{{ g.list|length }}</span>)</span>
|
||
<button type="button" class="toggle" title="Collapse/Expand">Toggle</button>
|
||
</div>
|
||
<div class="card-grid group-grid" data-skeleton {% if virtualize %}data-virtualize="1"{% endif %}>
|
||
{% for c in g.list %}
|
||
{% set owned = (owned_set is defined and c.name and (c.name|lower in owned_set)) %}
|
||
{% set is_locked = (locks is defined and (c.name|lower in locks)) %}
|
||
<div class="card-tile{% if game_changers and (c.name in game_changers) %} game-changer{% endif %}{% if is_locked %} locked{% endif %}" data-card-name="{{ c.name }}" data-role="{{ c.role or c.sub_role or '' }}" data-tags="{{ (c.tags|join(', ')) if c.tags else '' }}" data-owned="{{ '1' if owned else '0' }}">
|
||
<button type="button" class="img-btn" title="{{ 'Unlock this card (kept across reruns)' if is_locked else 'Lock this card (keep across reruns)' }}" aria-pressed="{{ 'true' if is_locked else 'false' }}"
|
||
hx-post="/build/lock" hx-target="#lock-{{ group_idx }}-{{ loop.index0 }}" hx-swap="innerHTML"
|
||
hx-vals='{"name": "{{ c.name }}", "locked": "{{ '0' if is_locked else '1' }}"}'
|
||
hx-on="htmx:afterOnLoad: (function(){try{const tile=this.closest('.card-tile');if(!tile)return;const valsAttr=this.getAttribute('hx-vals')||'{}';const sent=JSON.parse(valsAttr.replace(/"/g,'\"'));const nowLocked=(sent.locked==='1');tile.classList.toggle('locked', nowLocked);const next=(nowLocked?'0':'1');this.setAttribute('hx-vals', JSON.stringify({name: sent.name, locked: next}));}catch(e){}})()">
|
||
<img class="card-thumb" src="https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=normal" alt="{{ c.name }} image" width="160" data-card-name="{{ c.name }}" loading="lazy" decoding="async" data-lqip="1"
|
||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=large 672w"
|
||
sizes="160px" />
|
||
</button>
|
||
<div class="owned-badge" title="{{ 'Owned' if owned else 'Not owned' }}" aria-label="{{ 'Owned' if owned else 'Not owned' }}">{% if owned %}✔{% else %}✖{% endif %}</div>
|
||
<div class="name">{{ c.name|safe }}{% if c.count and c.count > 1 %} ×{{ c.count }}{% endif %}</div>
|
||
<div class="lock-box" id="lock-{{ group_idx }}-{{ loop.index0 }}" style="display:flex; justify-content:center; gap:.25rem; margin-top:.25rem;">
|
||
<button type="button" class="btn-lock" title="{{ 'Unlock this card (kept across reruns)' if is_locked else 'Lock this card (keep across reruns)' }}" aria-pressed="{{ 'true' if is_locked else 'false' }}"
|
||
hx-post="/build/lock" hx-target="closest .lock-box" hx-swap="innerHTML"
|
||
hx-vals='{"name": "{{ c.name }}", "locked": "{{ '0' if is_locked else '1' }}"}'>{{ '🔒 Unlock' if is_locked else '🔓 Lock' }}</button>
|
||
</div>
|
||
{% if c.reason %}
|
||
<div style="display:flex; justify-content:center; margin-top:.25rem; gap:.35rem; flex-wrap:wrap;">
|
||
<button type="button" class="btn-why" aria-expanded="false">Why?</button>
|
||
<button type="button" class="btn" hx-get="/build/alternatives" hx-vals='{"name": "{{ c.name }}"}' hx-target="#alts-{{ group_idx }}-{{ loop.index0 }}" hx-swap="innerHTML" title="Suggest alternatives">Alternatives</button>
|
||
</div>
|
||
<div class="reason" role="region" aria-label="Reason">{{ c.reason }}</div>
|
||
{% else %}
|
||
<div style="display:flex; justify-content:center; margin-top:.25rem;">
|
||
<button type="button" class="btn" hx-get="/build/alternatives" hx-vals='{"name": "{{ c.name }}"}' hx-target="#alts-{{ group_idx }}-{{ loop.index0 }}" hx-swap="innerHTML" title="Suggest alternatives">Alternatives</button>
|
||
</div>
|
||
{% endif %}
|
||
<div id="alts-{{ group_idx }}-{{ loop.index0 }}" class="alts" style="margin-top:.25rem;"></div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
<div class="card-grid" data-skeleton {% if virtualize %}data-virtualize="1"{% endif %}>
|
||
{% for c in added_cards %}
|
||
{% set owned = (owned_set is defined and c.name and (c.name|lower in owned_set)) %}
|
||
{% set is_locked = (locks is defined and (c.name|lower in locks)) %}
|
||
<div class="card-tile{% if game_changers and (c.name in game_changers) %} game-changer{% endif %}{% if is_locked %} locked{% endif %}" data-card-name="{{ c.name }}" data-role="{{ c.role or c.sub_role or '' }}" data-tags="{{ (c.tags|join(', ')) if c.tags else '' }}" data-owned="{{ '1' if owned else '0' }}">
|
||
<button type="button" class="img-btn" title="{{ 'Unlock this card (kept across reruns)' if is_locked else 'Lock this card (keep across reruns)' }}" aria-pressed="{{ 'true' if is_locked else 'false' }}"
|
||
hx-post="/build/lock" hx-target="#lock-{{ loop.index0 }}" hx-swap="innerHTML"
|
||
hx-vals='{"name": "{{ c.name }}", "locked": "{{ '0' if is_locked else '1' }}"}'
|
||
hx-on="htmx:afterOnLoad: (function(){try{const tile=this.closest('.card-tile');if(!tile)return;const valsAttr=this.getAttribute('hx-vals')||'{}';const sent=JSON.parse(valsAttr.replace(/"/g,'\"'));const nowLocked=(sent.locked==='1');tile.classList.toggle('locked', nowLocked);const next=(nowLocked?'0':'1');this.setAttribute('hx-vals', JSON.stringify({name: sent.name, locked: next}));}catch(e){}})()">
|
||
<img class="card-thumb" src="https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=normal" alt="{{ c.name }} image" width="160" data-card-name="{{ c.name }}" loading="lazy" decoding="async" data-lqip="1"
|
||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=large 672w"
|
||
sizes="160px" />
|
||
</button>
|
||
<div class="owned-badge" title="{{ 'Owned' if owned else 'Not owned' }}" aria-label="{{ 'Owned' if owned else 'Not owned' }}">{% if owned %}✔{% else %}✖{% endif %}</div>
|
||
<div class="name">{{ c.name|safe }}{% if c.count and c.count > 1 %} ×{{ c.count }}{% endif %}</div>
|
||
<div class="lock-box" id="lock-{{ loop.index0 }}" style="display:flex; justify-content:center; gap:.25rem; margin-top:.25rem;">
|
||
<button type="button" class="btn-lock" title="{{ 'Unlock this card (kept across reruns)' if is_locked else 'Lock this card (keep across reruns)' }}" aria-pressed="{{ 'true' if is_locked else 'false' }}"
|
||
hx-post="/build/lock" hx-target="closest .lock-box" hx-swap="innerHTML"
|
||
hx-vals='{"name": "{{ c.name }}", "locked": "{{ '0' if is_locked else '1' }}"}'>{{ '🔒 Unlock' if is_locked else '🔓 Lock' }}</button>
|
||
</div>
|
||
{% if c.reason %}
|
||
<div style="display:flex; justify-content:center; margin-top:.25rem; gap:.35rem; flex-wrap:wrap;">
|
||
<button type="button" class="btn-why" aria-expanded="false">Why?</button>
|
||
<button type="button" class="btn" hx-get="/build/alternatives" hx-vals='{"name": "{{ c.name }}"}' hx-target="#alts-{{ loop.index0 }}" hx-swap="innerHTML" title="Suggest alternatives">Alternatives</button>
|
||
</div>
|
||
<div class="reason" role="region" aria-label="Reason">{{ c.reason }}</div>
|
||
{% else %}
|
||
<div style="display:flex; justify-content:center; margin-top:.25rem;">
|
||
<button type="button" class="btn" hx-get="/build/alternatives" hx-vals='{"name": "{{ c.name }}"}' hx-target="#alts-{{ loop.index0 }}" hx-swap="innerHTML" title="Suggest alternatives">Alternatives</button>
|
||
</div>
|
||
{% endif %}
|
||
<div id="alts-{{ loop.index0 }}" class="alts" style="margin-top:.25rem;"></div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
<div class="muted" style="font-size:12px; margin:.35rem 0 .25rem 0;">Tip: Click a card to lock or unlock it. Locked cards are kept across reruns and won’t be replaced unless you unlock them.</div>
|
||
<div data-empty hidden role="status" aria-live="polite" class="muted" style="margin:.5rem 0 0;">
|
||
No cards match your filters.
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if show_logs and log %}
|
||
<details style="margin-top:1rem;">
|
||
<summary>Show logs</summary>
|
||
<pre style="margin-top:.5rem; white-space:pre-wrap; background:#0f1115; border:1px solid var(--border); padding:1rem; border-radius:8px; max-height:40vh; overflow:auto;">{{ log }}</pre>
|
||
</details>
|
||
{% endif %}
|
||
|
||
<!-- controls now above -->
|
||
|
||
{% if status and status.startswith('Build complete') %}
|
||
{% if summary %}
|
||
{% include "partials/deck_summary.html" %}
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<script>
|
||
// Sync tile class and image-button toggle after lock button swaps
|
||
document.addEventListener('htmx:afterSwap', function(ev){
|
||
try{
|
||
const tgt = ev.target;
|
||
if(!tgt) return;
|
||
// Only act for lock-box updates
|
||
if(!tgt.classList || !tgt.classList.contains('lock-box')) return;
|
||
const tile = tgt.closest('.card-tile');
|
||
if(!tile) return;
|
||
const lockBtn = tgt.querySelector('.btn-lock');
|
||
if(lockBtn){
|
||
const isLocked = (lockBtn.getAttribute('data-locked') === '1');
|
||
tile.classList.toggle('locked', isLocked);
|
||
const imgBtn = tile.querySelector('.img-btn');
|
||
if(imgBtn){
|
||
try{
|
||
const valsAttr = imgBtn.getAttribute('hx-vals') || '{}';
|
||
const cur = JSON.parse(valsAttr.replace(/"/g, '"'));
|
||
const next = isLocked ? '0' : '1';
|
||
// Keep name stable; fallback to tile data attribute
|
||
const nm = cur.name || tile.getAttribute('data-card-name') || '';
|
||
imgBtn.setAttribute('hx-vals', JSON.stringify({ name: nm, locked: next }));
|
||
imgBtn.title = 'Click to ' + (isLocked ? 'unlock' : 'lock') + ' this card';
|
||
try { imgBtn.setAttribute('aria-pressed', isLocked ? 'true' : 'false'); } catch(_){ }
|
||
}catch(_){/* noop */}
|
||
}
|
||
}
|
||
}catch(_){/* noop */}
|
||
});
|
||
// Allow dismissing/auto-clearing the last-action chip
|
||
document.addEventListener('click', function(ev){
|
||
try{
|
||
var t = ev.target;
|
||
if (!t) return;
|
||
if (t.matches && t.matches('#last-action .chip')){
|
||
var c = document.getElementById('last-action');
|
||
if (c) c.innerHTML = '';
|
||
}
|
||
}catch(_){/* noop */}
|
||
});
|
||
setTimeout(function(){ try{ var c=document.getElementById('last-action'); if(c && c.firstElementChild){ c.innerHTML=''; } }catch(_){} }, 6000);
|
||
|
||
// Keyboard helpers: when a card-tile is focused, L toggles lock, R opens alternatives
|
||
document.addEventListener('keydown', function(e){
|
||
try{
|
||
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
||
var tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : '';
|
||
// Ignore when typing in inputs/selects
|
||
if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
|
||
var tile = document.activeElement && document.activeElement.closest ? document.activeElement.closest('.card-tile') : null;
|
||
if (!tile) return;
|
||
if (e.key === 'l' || e.key === 'L') {
|
||
e.preventDefault(); e.stopPropagation();
|
||
var lockFormBtn = tile.querySelector('.lock-box .btn-lock');
|
||
if (lockFormBtn) { lockFormBtn.click(); }
|
||
} else if (e.key === 'r' || e.key === 'R') {
|
||
e.preventDefault(); e.stopPropagation();
|
||
var altBtn = tile.querySelector('button[hx-get="/build/alternatives"]');
|
||
if (altBtn) { altBtn.click(); }
|
||
}
|
||
}catch(_){ }
|
||
});
|
||
</script>
|