web: DRY Step 5 and alternatives (partial+macro), centralize start_ctx/owned_set, adopt builder_*

This commit is contained in:
mwisnowski 2025-09-02 11:39:14 -07:00
parent fe9aabbce9
commit 014bcc37b7
24 changed files with 1200 additions and 766 deletions

View file

@ -0,0 +1,34 @@
{# Alternatives panel partial.
Expects: name (seed display), require_owned (bool), items = [
{ 'name': display_name, 'name_lower': lower, 'owned': bool, 'tags': list[str] }
]
#}
<div class="alts" style="margin-top:.35rem; padding:.5rem; border:1px solid var(--border); border-radius:8px; background:#0f1115;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.25rem;">
<strong>Alternatives</strong>
{% set toggle_q = '0' if require_owned else '1' %}
{% set toggle_label = 'Owned only: On' if require_owned else 'Owned only: Off' %}
<button class="btn" hx-get="/build/alternatives?name={{ name|urlencode }}&owned_only={{ toggle_q }}"
hx-target="closest .alts" hx-swap="outerHTML">{{ toggle_label }}</button>
</div>
{% if not items or items|length == 0 %}
<div class="muted">No alternatives found{{ ' (owned only)' if require_owned else '' }}.</div>
{% else %}
<ul style="list-style:none; padding:0; margin:0; display:grid; gap:.25rem;">
{% for it in items %}
{% set badge = '✔' if it.owned else '✖' %}
{% set title = 'Owned' if it.owned else 'Not owned' %}
{% set tags = (it.tags or []) %}
<li>
<span class="owned-badge" title="{{ title }}">{{ badge }}</span>
<button class="btn" data-card-name="{{ it.name }}"
data-tags="{{ tags|join(', ') }}" hx-post="/build/replace"
hx-vals='{"old":"{{ name }}", "new":"{{ it.name }}"}'
hx-target="closest .alts" hx-swap="outerHTML" title="Lock this alternative and unlock the current pick">
Replace with {{ it.name }}
</button>
</li>
{% endfor %}
</ul>
{% endif %}
</div>

View file

@ -0,0 +1,21 @@
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="setupPromptTitle" style="position:fixed; inset:0; z-index:1000; display:flex; align-items:center; justify-content:center;">
<div class="modal-backdrop" style="position:absolute; inset:0; background:rgba(0,0,0,.6);"></div>
<div class="modal-content" style="position:relative; max-width:560px; width:clamp(320px, 90vw, 560px); background:#0f1115; border:1px solid var(--border); border-radius:10px; box-shadow:0 10px 30px rgba(0,0,0,.5); padding:1rem;">
<div class="modal-header">
<h3 id="setupPromptTitle">{{ title or 'Setup required' }}</h3>
</div>
<div class="modal-body">
<p>{{ message or 'The card database and tags need to be prepared before building a deck.' }}</p>
</div>
<div class="modal-footer" style="display:flex; gap:.5rem; justify-content:flex-end; margin-top:1rem;">
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Cancel</button>
<a class="btn-continue" href="{{ action_url }}" hx-boost="true" hx-target="body" hx-swap="innerHTML">{{ action_label or 'Run Setup' }}</a>
</div>
</div>
</div>
<script>
(function(){
function onKey(e){ if (e.key === 'Escape'){ e.preventDefault(); try{ var m=document.querySelector('.modal'); if(m){ m.remove(); document.removeEventListener('keydown', onKey); } }catch(_){ } } }
document.addEventListener('keydown', onKey);
})();
</script>

View file

@ -85,6 +85,7 @@
{% endif %}
{% if locked_cards is defined and locked_cards %}
{% from 'partials/_macros.html' import lock_button %}
<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;">
@ -93,12 +94,9 @@
<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>
<div class="lock-box" style="display:inline; margin-left:auto;">
{{ lock_button(lk.name, True, from_list=True, target_selector='closest li') }}
</div>
</li>
{% endfor %}
</ul>
@ -236,10 +234,9 @@
<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>
{% from 'partials/_macros.html' import lock_button %}
{{ lock_button(c.name, is_locked) }}
</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>
@ -274,10 +271,9 @@
<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>
{% from 'partials/_macros.html' import lock_button %}
{{ lock_button(c.name, is_locked) }}
</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>