feat(web): launch commander browser with deck builder CTA

This commit is contained in:
matt 2025-09-30 15:49:08 -07:00
parent 6e9ba244c9
commit 8e57588f40
27 changed files with 1960 additions and 45 deletions

View file

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
<title>MTG Deckbuilder</title>
<script src="https://unpkg.com/htmx.org@1.9.12" onerror="var s=document.createElement('script');s.src='/static/vendor/htmx-1.9.12.min.js';document.head.appendChild(s);"></script>
<script>
@ -81,6 +82,7 @@
<a href="/configs">Build from JSON</a>
{% if show_setup %}<a href="/setup">Setup/Tag</a>{% endif %}
<a href="/owned">Owned Library</a>
{% if show_commanders %}<a href="/commanders">Commanders</a>{% endif %}
<a href="/decks">Finished Decks</a>
<a href="/themes/">Themes</a>
{% if random_ui %}<a href="/random">Random</a>{% endif %}
@ -172,6 +174,10 @@
#hover-card-panel .hcp-body { display:grid; grid-template-columns: 320px 1fr; gap:18px; align-items:start; }
#hover-card-panel .hcp-img-wrap { grid-column:1 / 2; }
#hover-card-panel.compact-img .hcp-body { grid-template-columns: 120px 1fr; }
#hover-card-panel.hcp-simple { width:auto !important; max-width:min(360px, 90vw) !important; padding:12px !important; height:auto !important; max-height:none !important; overflow:hidden !important; }
#hover-card-panel.hcp-simple .hcp-body { display:flex; flex-direction:column; gap:12px; align-items:center; }
#hover-card-panel.hcp-simple .hcp-right { display:none !important; }
#hover-card-panel.hcp-simple .hcp-img { max-width:100%; }
/* Tag list as multi-column list instead of pill chips for readability */
#hover-card-panel .hcp-taglist { columns:2; column-gap:18px; font-size:13px; line-height:1.3; margin:6px 0 6px; padding:0; list-style:none; max-height:180px; overflow:auto; }
#hover-card-panel .hcp-taglist li { break-inside:avoid; padding:2px 0 2px 0; position:relative; }
@ -195,9 +201,9 @@
.list-row .dfc-toggle .icon { font-size:12px; }
.list-row .dfc-toggle[data-face='back'] { background:rgba(76,29,149,.3); }
.list-row .dfc-toggle[data-face='front'] { background:rgba(56,189,248,.2); }
#hover-card-panel.mobile { left:50% !important; top:auto !important; bottom:max(16px, 5vh); transform:translateX(-50%); width:min(92vw, 420px) !important; max-height:80vh; overflow-y:auto; padding:16px 18px; pointer-events:auto !important; }
#hover-card-panel.mobile .hcp-body { display:flex; flex-direction:column; gap:18px; }
#hover-card-panel.mobile .hcp-img { max-width:100%; margin:0 auto; }
#hover-card-panel.mobile { left:50% !important; top:50% !important; bottom:auto !important; transform:translate(-50%, -50%); width:min(94vw, 460px) !important; max-height:88vh; overflow-y:auto; padding:20px 22px; pointer-events:auto !important; }
#hover-card-panel.mobile .hcp-body { display:flex; flex-direction:column; gap:20px; }
#hover-card-panel.mobile .hcp-img { width:100%; max-width:min(90vw, 420px) !important; margin:0 auto; }
#hover-card-panel.mobile .hcp-right { width:100%; display:flex; flex-direction:column; gap:10px; align-items:flex-start; }
#hover-card-panel.mobile .hcp-header { flex-wrap:wrap; gap:8px; align-items:flex-start; }
#hover-card-panel.mobile .hcp-role { font-size:12px; letter-spacing:.55px; }
@ -923,12 +929,14 @@
var panel = ensurePanel();
if(!panel || panel.__hoverInit) return;
panel.__hoverInit = true;
var imgEl = panel.querySelector('.hcp-img');
var nameEl = panel.querySelector('.hcp-name');
var rarityEl = panel.querySelector('.hcp-rarity');
var metaEl = panel.querySelector('.hcp-meta');
var reasonsList = panel.querySelector('.hcp-reasons');
var tagsEl = panel.querySelector('.hcp-tags');
var imgEl = panel.querySelector('.hcp-img');
var nameEl = panel.querySelector('.hcp-name');
var rarityEl = panel.querySelector('.hcp-rarity');
var metaEl = panel.querySelector('.hcp-meta');
var reasonsList = panel.querySelector('.hcp-reasons');
var tagsEl = panel.querySelector('.hcp-tags');
var bodyEl = panel.querySelector('.hcp-body');
var rightCol = panel.querySelector('.hcp-right');
var coarseQuery = window.matchMedia('(pointer: coarse)');
function isMobileMode(){ return (coarseQuery && coarseQuery.matches) || window.innerWidth <= 768; }
function refreshPosition(){ if(panel.style.display==='block'){ move(window.__lastPointerEvent); } }
@ -946,12 +954,11 @@
function positionPanel(evt){
if(isMobileMode()){
panel.classList.add('mobile');
var bottomOffset = Math.max(16, Math.round(window.innerHeight * 0.05));
panel.style.bottom = bottomOffset + 'px';
panel.style.bottom = 'auto';
panel.style.left = '50%';
panel.style.top = 'auto';
panel.style.top = '50%';
panel.style.right = 'auto';
panel.style.transform = 'translateX(-50%)';
panel.style.transform = 'translate(-50%, -50%)';
panel.style.pointerEvents = 'auto';
} else {
panel.classList.remove('mobile');
@ -990,6 +997,11 @@
if(!card) return;
// Prefer attributes on container, fallback to child (image) if missing
function attr(name){ return card.getAttribute(name) || (card.querySelector('[data-'+name.slice(5)+']') && card.querySelector('[data-'+name.slice(5)+']').getAttribute(name)) || ''; }
var simpleSource = null;
if(card.closest){
simpleSource = card.closest('[data-hover-simple]');
}
var forceSimple = (card.hasAttribute && card.hasAttribute('data-hover-simple')) || !!simpleSource;
var nm = attr('data-card-name') || attr('data-original-name') || 'Card';
var rarity = (attr('data-rarity')||'').trim();
var mana = (attr('data-mana')||'').trim();
@ -1110,6 +1122,26 @@
}
panel.classList.toggle('is-payoff', role === 'payoff');
panel.classList.toggle('is-commander', isCommanderRole);
var hasDetails = !forceSimple && (
!!roleLabel || !!mana || !!rarity || (reasonsRaw && reasonsRaw.trim()) || (overlapArr && overlapArr.length) || (allTags && allTags.length)
);
panel.classList.toggle('hcp-simple', !hasDetails);
if(rightCol){
rightCol.style.display = hasDetails ? 'flex' : 'none';
}
if(bodyEl){
if(!hasDetails){
bodyEl.style.display = 'flex';
bodyEl.style.flexDirection = 'column';
bodyEl.style.alignItems = 'center';
bodyEl.style.gap = '12px';
} else {
bodyEl.style.display = '';
bodyEl.style.flexDirection = '';
bodyEl.style.alignItems = '';
bodyEl.style.gap = '';
}
}
var fuzzy = encodeURIComponent(nm);
var rawName = nm || '';
var hasBack = rawName.indexOf('//')>-1 || (attr('data-original-name')||'').indexOf('//')>-1;

View file

@ -2,12 +2,80 @@
{% block banner_subtitle %}Build a Deck{% endblock %}
{% block content %}
<h2>Build a Deck</h2>
<div style="margin:.25rem 0 1rem 0;">
<div style="margin:.25rem 0 1rem 0; display:flex; align-items:center; gap:.75rem; flex-wrap:wrap;">
<button type="button" class="btn" hx-get="/build/new" hx-target="body" hx-swap="beforeend">Build a New Deck…</button>
<span class="muted" style="margin-left:.5rem;">Quick-start wizard (name, commander, themes, ideals)</span>
<span class="muted" style="margin-left:.25rem;">Quick-start wizard (name, commander, themes, ideals)</span>
{% if return_url %}
<a href="{{ return_url }}" class="btn" style="margin-left:auto;">← Back to Commanders</a>
{% endif %}
</div>
<div id="wizard">
<!-- Wizard content will load here after the modal submit starts the build. -->
<noscript><p>Enable JavaScript to build a deck.</p></noscript>
</div>
{% if commander %}
<span id="builder-init" data-commander="{{ commander|e }}" hidden></span>
<script>
(function(){
var opened = false;
function openWizard(){
if(opened) return; opened = true;
try{
var btn = document.querySelector('button.btn[hx-get="/build/new"]');
if(btn){ btn.click(); }
}catch(_){ }
}
// Pre-fill and auto-inspect when the modal content is injected
function onModalLoaded(e){
try{
var target = (e && e.detail && e.detail.target) ? e.detail.target : null; if(!target) return;
if(!(target.tagName && target.tagName.toLowerCase() === 'body')) return;
var init = document.getElementById('builder-init');
var preset = init && init.dataset ? (init.dataset.commander || '') : '';
if(!preset) return;
var input = document.querySelector('input[name="commander"]');
if(input){
if(!input.value){ input.value = preset; }
try { input.dispatchEvent(new Event('input', {bubbles:true})); } catch(_){ }
try { input.focus(); } catch(_){ }
}
// If htmx is available, auto-load the inspect view for an exact preset name.
try {
if (window.htmx && preset && typeof window.htmx.ajax === 'function'){
window.htmx.ajax('GET', '/build/new/inspect?name=' + encodeURIComponent(preset), { target: '#newdeck-tags-slot', swap: 'innerHTML' });
// Also try to load multi-copy suggestions based on current radio defaults
setTimeout(function(){
try{
var mode = document.querySelector('input[name="tag_mode"]') || document.getElementById('modal_tag_mode');
var primary = document.getElementById('modal_primary_tag');
var secondary = document.getElementById('modal_secondary_tag');
var tertiary = document.getElementById('modal_tertiary_tag');
var params = new URLSearchParams();
params.set('commander', preset);
if (primary && primary.value) params.set('primary_tag', primary.value);
if (secondary && secondary.value) params.set('secondary_tag', secondary.value);
if (tertiary && tertiary.value) params.set('tertiary_tag', tertiary.value);
if (mode && mode.value) params.set('tag_mode', mode.value);
window.htmx.ajax('GET', '/build/new/multicopy?' + params.toString(), { target: '#newdeck-multicopy-slot', swap: 'innerHTML' });
}catch(_){ }
}, 250);
}
} catch(_){ }
}catch(_){ }
}
document.addEventListener('htmx:afterSwap', onModalLoaded);
// Open after DOM is ready; try a few hooks to ensure htmx is initialized
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(openWizard, 0);
} else {
document.addEventListener('DOMContentLoaded', function(){ setTimeout(openWizard, 0); });
}
if (window.htmx && typeof window.htmx.onLoad === 'function'){
window.htmx.onLoad(function(){ setTimeout(openWizard, 0); });
}
// Last resort: delayed attempt in case previous hooks raced htmx init
setTimeout(openWizard, 200);
})();
</script>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,201 @@
{% extends "base.html" %}
{% block content %}
<section class="commander-page">
<header class="commander-hero">
<h2>Commanders</h2>
<p class="muted">Browse the catalog and jump straight into a build with your chosen leader.</p>
</header>
<form
id="commander-filter-form"
class="commander-filters"
action="/commanders"
method="get"
hx-get="/commanders"
hx-target="#commander-results"
hx-trigger="submit, change from:#commander-color, keyup changed delay:300ms from:#commander-search"
hx-include="#commander-filter-form"
hx-push-url="true"
hx-indicator="#commander-loading"
novalidate
>
<label>
<span class="filter-label">Search</span>
<input type="search" id="commander-search" name="q" value="{{ query }}" placeholder="Search commanders, themes, or text..." autocomplete="off" />
</label>
<label>
<span class="filter-label">Color identity</span>
<select id="commander-color" name="color">
<option value="">All colors</option>
{% for code, label in color_options %}
<option value="{{ code }}" {% if color == code %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</label>
<input type="hidden" name="page" value="{{ page }}" />
<button type="submit" class="btn filter-submit">Apply</button>
</form>
<div id="commander-loading" class="commander-loading" role="status" aria-live="polite">
<span class="sr-only">Loading commanders…</span>
<div class="commander-skeleton-list" aria-hidden="true">
{% for i in range(3) %}
<article class="commander-skeleton">
<div class="skeleton-thumb shimmer"></div>
<div class="skeleton-main">
<div class="skeleton-line skeleton-title shimmer"></div>
<div class="skeleton-line skeleton-meta shimmer"></div>
<div class="skeleton-chip-row">
<span class="skeleton-chip shimmer"></span>
<span class="skeleton-chip shimmer"></span>
<span class="skeleton-chip shimmer"></span>
</div>
<div class="skeleton-line skeleton-text shimmer"></div>
</div>
<div class="skeleton-cta shimmer"></div>
</article>
{% endfor %}
</div>
</div>
<div id="commander-results">
{% include "commanders/list_fragment.html" %}
</div>
</section>
<style>
.commander-page { display:flex; flex-direction:column; gap:1.25rem; }
.commander-hero h2 { margin:0; font-size:1.75rem; }
.commander-hero p { margin:0; max-width:60ch; }
.commander-filters { display:flex; flex-wrap:wrap; gap:.75rem 1rem; align-items:flex-end; }
.commander-filters label { display:flex; flex-direction:column; gap:.35rem; min-width:220px; }
.filter-label { font-size:.85rem; color:var(--muted); letter-spacing:.03em; text-transform:uppercase; }
.commander-filters input,
.commander-filters select { background:var(--panel); color:var(--text); border:1px solid var(--border); border-radius:8px; padding:.45rem .6rem; min-height:2.4rem; }
.commander-filters input:focus,
.commander-filters select:focus { outline:2px solid var(--ring); outline-offset:2px; }
.filter-submit { height:2.4rem; align-self:flex-end; }
.commander-summary { font-size:.9rem; }
.commander-error { padding:.75rem .9rem; border:1px solid #f87171; background:rgba(248,113,113,.12); border-radius:10px; color:#fca5a5; }
.commander-empty { margin:1rem 0 0; }
.commander-list { display:flex; flex-direction:column; gap:1rem; margin-top:.5rem; }
.commander-row { display:flex; gap:1rem; padding:1rem; border:1px solid var(--border); border-radius:14px; background:var(--panel); align-items:stretch; }
.commander-thumb { width:160px; flex:0 0 auto; }
.commander-thumb img { width:160px; height:auto; border-radius:10px; border:1px solid var(--border); background:#0b0d12; display:block; }
.commander-main { flex:1 1 auto; display:flex; flex-direction:column; gap:.6rem; min-width:0; }
.commander-header { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .75rem; }
.commander-name { margin:0; font-size:1.25rem; }
.color-identity { display:flex; align-items:center; gap:.35rem; }
.commander-context { margin:0; font-size:.95rem; }
.commander-themes { display:flex; flex-wrap:wrap; gap:.4rem; }
.commander-themes-empty { font-size:.85rem; }
.commander-theme-chip { display:inline-flex; align-items:center; gap:.25rem; padding:4px 10px; border-radius:9999px; border:1px solid var(--border); background:rgba(148,163,184,.15); font-size:.75rem; letter-spacing:.03em; color:inherit; cursor:pointer; transition:background .15s ease, border-color .15s ease, transform .15s ease; appearance:none; font:inherit; }
.commander-theme-chip:focus-visible { outline:2px solid var(--ring); outline-offset:2px; }
.commander-theme-chip:hover { background:rgba(148,163,184,.25); border-color:rgba(148,163,184,.45); transform:translateY(-1px); }
.commander-partners { display:flex; flex-wrap:wrap; gap:.4rem; font-size:.85rem; }
.commander-partner-sep { opacity:.6; }
.commander-cta { margin-left:auto; display:flex; align-items:center; }
.commander-cta .btn { white-space:nowrap; }
.commander-pagination { display:flex; align-items:center; justify-content:space-between; gap:.75rem; margin-top:1rem; flex-wrap:wrap; }
.commander-summary + .commander-pagination { margin-top:.75rem; }
.commander-pagination .pagination-group { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; }
.commander-pagination .commander-page-btn { display:inline-flex; align-items:center; justify-content:center; min-width:96px; }
.commander-pagination .commander-page-btn[disabled],
.commander-pagination .commander-page-btn.disabled { opacity:.55; cursor:default; pointer-events:none; }
.commander-pagination-status { font-size:.85rem; color:var(--muted); }
.commander-loading { display:none; margin-top:1rem; }
.commander-loading.htmx-request { display:block; }
.commander-skeleton-list { display:flex; flex-direction:column; gap:1rem; }
.commander-skeleton { display:flex; gap:1rem; padding:1rem; border:1px solid var(--border); border-radius:14px; background:var(--panel); align-items:stretch; }
.skeleton-thumb { width:160px; height:220px; border-radius:10px; }
.skeleton-main { flex:1 1 auto; display:flex; flex-direction:column; gap:.6rem; }
.skeleton-line { height:16px; border-radius:9999px; }
.skeleton-title { width:45%; height:22px; }
.skeleton-meta { width:30%; }
.skeleton-text { width:65%; }
.skeleton-chip-row { display:flex; gap:.5rem; flex-wrap:wrap; }
.skeleton-chip { width:90px; height:22px; border-radius:9999px; display:inline-block; }
.skeleton-cta { width:120px; height:42px; border-radius:9999px; }
.shimmer { background:linear-gradient(90deg, rgba(148,163,184,0.25) 25%, rgba(148,163,184,0.15) 37%, rgba(148,163,184,0.25) 63%); background-size:400% 100%; animation:commander-shimmer 1.4s ease-in-out infinite; }
@keyframes commander-shimmer {
0% { background-position:100% 0; }
100% { background-position:-100% 0; }
}
@media (max-width: 900px) {
.commander-row { flex-direction:column; }
.commander-thumb img { width:100%; max-width:280px; }
.commander-cta { margin-left:0; }
.commander-cta .btn { width:100%; justify-content:center; text-align:center; }
}
@media (max-width: 640px) {
.commander-filters { align-items:stretch; }
.filter-submit { width:100%; }
.commander-filters label { flex:1 1 100%; min-width:0; }
.commander-thumb { width:min(70vw, 220px); align-self:center; }
.commander-thumb img { width:100%; }
.skeleton-thumb { width:min(70vw, 220px); height:calc(min(70vw, 220px) * 1.4); }
}
</style>
<script>
(function(){
const form = document.getElementById('commander-filter-form');
if (!form) return;
const pageInput = form.querySelector('input[name="page"]');
if (!pageInput) return;
const resetPage = () => { pageInput.value = '1'; };
const searchField = document.getElementById('commander-search');
const colorField = document.getElementById('commander-color');
if (searchField) searchField.addEventListener('input', resetPage);
if (colorField) colorField.addEventListener('change', resetPage);
const updatePageFromResults = (container) => {
if (!container) return;
const marker = container.querySelector('[data-current-page]');
if (marker) {
const current = marker.getAttribute('data-current-page');
if (current) pageInput.value = current;
}
};
document.body.addEventListener('htmx:afterSwap', (event) => {
const target = event.detail && event.detail.target;
if (!target || target.id !== 'commander-results') return;
updatePageFromResults(target);
// Intelligent scroll-to-top: only when triggered from bottom controls or when the summary/top controls are off-screen
const container = document.getElementById('commander-results');
const searchEl = document.getElementById('commander-search');
if (!container) return;
const invoker = event.detail && event.detail.elt ? event.detail.elt : null;
const fromBottom = invoker && invoker.closest && invoker.closest('[data-bottom-controls]');
// If not from bottom, check whether the top of the results is already within view; if so, skip scroll
const rect = container.getBoundingClientRect();
const topInView = rect.top >= 0 && rect.top <= (window.innerHeight * 0.25);
// If we're below the top controls (content's top is above viewport) or the click came from the bottom controls,
// jump directly to the search input (no smooth animation) for fastest navigation.
if (fromBottom || rect.top < 0) {
requestAnimationFrame(() => {
if (searchEl) {
searchEl.scrollIntoView({ behavior: 'auto', block: 'start' });
try { searchEl.focus({ preventScroll: true }); } catch(_) { /* no-op */ }
} else {
window.scrollTo({ top: 0, behavior: 'auto' });
}
});
return;
}
if (!topInView) {
requestAnimationFrame(() => {
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
});
updatePageFromResults(document.getElementById('commander-results'));
})();
</script>
{% endblock %}

View file

@ -0,0 +1,38 @@
<div class="commander-results-inner" data-current-page="{{ page }}">
{% if error %}
<div class="commander-error" role="alert">{{ error }}</div>
{% else %}
<div class="commander-summary muted">
{% if total_count %}
{% if commanders %}
Showing {{ page_start }}&nbsp;&ndash;&nbsp;{{ page_end }} of {{ result_total }} commander{% if result_total != 1 %}s{% endif %}{% if is_filtered %} (filtered){% endif %}.
{% else %}
No commanders matched your filters.
{% endif %}
{% else %}
No commander data available.
{% endif %}
</div>
{% if commanders %}
{% set pagination_position = 'top' %}
{% include "commanders/pagination_controls.html" %}
<div class="commander-list" role="list">
{% for entry in commanders %}
{% include "commanders/row_wireframe.html" %}
{% endfor %}
</div>
{% if page_count > 1 %}
{% set pagination_position = 'bottom' %}
{% include "commanders/pagination_controls.html" %}
{% endif %}
{% else %}
<p class="muted commander-empty" role="status">
{% if total_count %}
No commanders matched your filters.
{% else %}
Commander catalog is empty.
{% endif %}
</p>
{% endif %}
{% endif %}
</div>

View file

@ -0,0 +1,37 @@
<nav class="commander-pagination" role="navigation" aria-label="Commander pagination" {% if pagination_position == 'bottom' %}data-bottom-controls="1"{% endif %}>
<div class="pagination-group">
<a
class="btn ghost commander-page-btn {% if not has_prev %}disabled{% endif %}"
{% if has_prev %}
href="{{ prev_url }}"
hx-get="{{ prev_url }}"
hx-target="#commander-results"
hx-push-url="true"
data-scroll-top-on-swap="1"
{% else %}
aria-disabled="true"
tabindex="-1"
{% endif %}
>
&larr; Previous
</a>
<span class="commander-pagination-status" aria-live="polite">
Page {{ page }} of {{ page_count }}
</span>
<a
class="btn ghost commander-page-btn {% if not has_next %}disabled{% endif %}"
{% if has_next %}
href="{{ next_url }}"
hx-get="{{ next_url }}"
hx-target="#commander-results"
hx-push-url="true"
data-scroll-top-on-swap="1"
{% else %}
aria-disabled="true"
tabindex="-1"
{% endif %}
>
Next &rarr;
</a>
</div>
</nav>

View file

@ -0,0 +1,56 @@
{# Commander row partial fed by CommanderView entries #}
{% from "partials/_macros.html" import color_identity %}
{% set record = entry.record %}
<article class="commander-row" data-commander-slug="{{ record.slug }}" data-hover-simple="true">
<div class="commander-thumb">
{% set small = record.image_small_url or record.image_normal_url %}
<img
src="{{ small }}"
srcset="{{ small }} 160w, {{ record.image_normal_url or small }} 488w"
sizes="160px"
alt="{{ record.display_name }} card art"
loading="lazy"
decoding="async"
data-card-name="{{ record.display_name }}"
data-hover-simple="true"
/>
</div>
<div class="commander-main">
<div class="commander-header">
<h3 class="commander-name">{{ record.display_name }}</h3>
{{ color_identity(record.color_identity, record.is_colorless, entry.color_aria_label, entry.color_label) }}
</div>
<p class="commander-context muted">{{ record.type_line or 'Legendary Creature' }}</p>
{% if entry.themes %}
<div class="commander-themes" role="list">
{% for theme in entry.themes %}
{% set summary = theme.summary or 'Summary unavailable' %}
<button type="button"
class="commander-theme-chip"
role="listitem"
data-theme-name="{{ theme.name }}"
data-theme-slug="{{ theme.slug }}"
data-theme-summary="{{ summary }}"
title="{{ summary }}"
aria-label="{{ theme.name }} theme: {{ summary }}">
{{ theme.name }}
</button>
{% endfor %}
</div>
{% else %}
<div class="commander-themes commander-themes-empty">
<span class="muted">No themes linked yet.</span>
</div>
{% endif %}
{% if entry.partner_summary %}
<div class="commander-partners muted">
{% for note in entry.partner_summary %}
<span>{{ note }}</span>{% if not loop.last %}<span aria-hidden="true" class="commander-partner-sep"></span>{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
<div class="commander-cta">
<a class="btn" href="/build?commander={{ record.display_name|urlencode }}&return={{ return_url|urlencode }}" data-commander="{{ record.slug }}">Build</a>
</div>
</article>

View file

@ -66,6 +66,7 @@
+ 'SHOW_LOGS='+ (flags.SHOW_LOGS? '1':'0')
+ ', SHOW_DIAGNOSTICS='+ (flags.SHOW_DIAGNOSTICS? '1':'0')
+ ', SHOW_SETUP='+ (flags.SHOW_SETUP? '1':'0')
+ ', SHOW_COMMANDERS='+ (flags.SHOW_COMMANDERS? '1':'0')
+ ', RANDOM_MODES='+ (flags.RANDOM_MODES? '1':'0')
+ ', RANDOM_UI='+ (flags.RANDOM_UI? '1':'0')
+ ', RANDOM_MAX_ATTEMPTS='+ String(flags.RANDOM_MAX_ATTEMPTS ?? '')

View file

@ -4,12 +4,14 @@
<div class="actions-grid">
<a class="action-button primary" href="/build">Build a Deck</a>
<a class="action-button" href="/configs">Run a JSON Config</a>
{% if show_setup %}<a class="action-button" href="/setup">Initial Setup</a>{% endif %}
{% if show_setup %}<a class="action-button" href="/setup">Initial Setup</a>{% endif %}
<a class="action-button" href="/owned">Owned Library</a>
{% if show_commanders %}<a class="action-button" href="/commanders">Browse Commanders</a>{% endif %}
<a class="action-button" href="/decks">Finished Decks</a>
<a class="action-button" href="/themes/">Browse Themes</a>
{% if random_ui %}<a class="action-button" href="/random">Random Build</a>{% endif %}
{% if show_logs %}<a class="action-button" href="/logs">View Logs</a>{% endif %}
<a class="action-button" href="/themes/">Browse Themes</a>
{% if random_ui %}<a class="action-button" href="/random">Random Build</a>{% endif %}
{% if show_diagnostics %}<a class="action-button" href="/diagnostics">Diagnostics</a>{% endif %}
{% if show_logs %}<a class="action-button" href="/logs">View Logs</a>{% endif %}
</div>
<div id="themes-quick" style="margin-top:1rem; font-size:.85rem; color:var(--text-muted);">
<span id="themes-quick-status">Themes: …</span>

View file

@ -11,3 +11,19 @@
{{ '🔒 Unlock' if locked else '🔓 Lock' }}
</button>
{%- endmacro %}
{% macro color_identity(colors, is_colorless=False, aria_label='', title_text='') -%}
<div class="color-identity" role="img"
aria-label="{{ aria_label }}"
data-colorless="{{ '1' if is_colorless or not (colors and colors|length) else '0' }}"
{% if title_text %}title="{{ title_text }}"{% endif %}>
{% if colors and colors|length %}
{% for color in colors %}
<span class="mana mana-{{ color }}" aria-hidden="true"></span>
{% endfor %}
{% else %}
<span class="mana mana-C" aria-hidden="true"></span>
{% endif %}
<span class="sr-only">{{ aria_label }}</span>
</div>
{%- endmacro %}