mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-18 00:20:13 +01:00
overhaul: migrated to tailwind css for css management, consolidated custom css, removed inline css, removed unneeded css, and otherwise improved page styling
This commit is contained in:
parent
f1e21873e7
commit
b994978f60
81 changed files with 15784 additions and 2936 deletions
|
|
@ -32,13 +32,108 @@
|
|||
{% if it.rarity %}data-rarity="{{ it.rarity }}"{% endif %}
|
||||
{% if it.hover_simple %}data-hover-simple="1"{% endif %}
|
||||
{% if it.owned %}data-owned="1"{% endif %}
|
||||
data-tags="{{ tags|join(', ') }}" hx-post="/build/replace"
|
||||
data-tags="{{ tags|join(', ') }}"
|
||||
hx-post="/build/replace"
|
||||
hx-vals='{"old":"{{ name }}", "new":"{{ it.name }}", "owned_only":"{{ 1 if require_owned else 0 }}"}'
|
||||
hx-target="closest .alts" hx-swap="outerHTML" title="Lock this alternative and unlock the current pick">
|
||||
Replace with {{ it.name }}
|
||||
hx-target="closest .alts"
|
||||
hx-swap="outerHTML"
|
||||
title="Lock this alternative and unlock the current pick">
|
||||
{{ it.name }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
// Mobile: tap to preview, tap "Use as Replacement" button in popup to replace
|
||||
(function(){
|
||||
var altPanel = document.currentScript.previousElementSibling;
|
||||
if(!altPanel) return;
|
||||
|
||||
// Track which button triggered the popup
|
||||
var pendingReplacement = null;
|
||||
|
||||
// Better mobile detection (matches base.html logic)
|
||||
function isMobileMode(){
|
||||
var coarseQuery = window.matchMedia('(pointer: coarse)');
|
||||
return (coarseQuery && coarseQuery.matches) || window.innerWidth <= 768;
|
||||
}
|
||||
|
||||
// Intercept htmx request before it's sent
|
||||
altPanel.addEventListener('htmx:configRequest', function(e){
|
||||
var btn = e.detail.elt;
|
||||
if(!btn || !btn.classList.contains('alt-option')) return;
|
||||
|
||||
if(isMobileMode() && !btn.dataset.mobileConfirmed){
|
||||
// First tap on mobile: cancel the request, show preview instead
|
||||
e.preventDefault();
|
||||
pendingReplacement = btn;
|
||||
|
||||
// Show card preview and inject replacement button
|
||||
if(window.__hoverShowCard){
|
||||
window.__hoverShowCard(btn);
|
||||
|
||||
// Inject "Use as Replacement" button into popup
|
||||
setTimeout(function(){
|
||||
if(!isMobileMode()) return; // Double-check we're still in mobile mode
|
||||
|
||||
var hoverPanel = document.getElementById('hover-card-panel');
|
||||
if(hoverPanel && !hoverPanel.querySelector('.mobile-replace-btn')){
|
||||
var imgWrap = hoverPanel.querySelector('.hcp-img-wrap');
|
||||
if(imgWrap){
|
||||
var replaceBtn = document.createElement('button');
|
||||
replaceBtn.type = 'button';
|
||||
replaceBtn.className = 'btn mobile-replace-btn';
|
||||
replaceBtn.textContent = 'Use as Replacement';
|
||||
replaceBtn.style.cssText = 'width:100%;padding:0.75rem;font-size:15px;margin-top:8px;pointer-events:auto;position:relative;z-index:10000;';
|
||||
|
||||
var handleClick = function(ev){
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if(!pendingReplacement) return;
|
||||
|
||||
pendingReplacement.dataset.mobileConfirmed = '1';
|
||||
pendingReplacement.click();
|
||||
|
||||
// Close the popup after a short delay
|
||||
setTimeout(function(){
|
||||
var closeBtn = hoverPanel.querySelector('.hcp-close');
|
||||
if(closeBtn) closeBtn.click();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
replaceBtn.onclick = handleClick;
|
||||
replaceBtn.addEventListener('click', handleClick);
|
||||
replaceBtn.addEventListener('touchend', handleClick);
|
||||
|
||||
imgWrap.appendChild(replaceBtn);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Desktop or mobile with confirmation: allow request to proceed
|
||||
if(btn.dataset.mobileConfirmed){
|
||||
btn.removeAttribute('data-mobileConfirmed');
|
||||
pendingReplacement = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Reset pending replacement when popup closes
|
||||
document.addEventListener('click', function(e){
|
||||
if(e.target.closest('.hcp-close')){
|
||||
pendingReplacement = null;
|
||||
// Remove mobile replace button when closing
|
||||
var hoverPanel = document.getElementById('hover-card-panel');
|
||||
if(hoverPanel){
|
||||
var replaceBtn = hoverPanel.querySelector('.mobile-replace-btn');
|
||||
if(replaceBtn) replaceBtn.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@
|
|||
{% set sev = (f.severity or 'FAIL')|upper %}
|
||||
<div class="card-tile" data-card-name="{{ f.name }}" data-role="{{ f.role or '' }}" {% if sev == 'FAIL' %}style="border-color: var(--red-main);"{% elif sev == 'WARN' %}style="border-color: var(--orange-main);"{% endif %}>
|
||||
<a href="https://scryfall.com/search?q={{ f.name|urlencode }}" target="_blank" rel="noopener" class="img-btn" title="{{ f.name }}">
|
||||
<img class="card-thumb" src="https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=normal" alt="{{ f.name }} image" width="160" loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ f.name|urlencode }}&format=image&version=large 672w"
|
||||
<img class="card-thumb" src="{{ f.name|card_image('normal') }}" alt="{{ f.name }} image" width="160" loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="{{ f.name|card_image('small') }} 160w, {{ f.name|card_image('normal') }} 488w"
|
||||
sizes="160px" />
|
||||
</a>
|
||||
<div class="owned-badge" title="{{ 'Owned' if f.owned else 'Not owned' }}" aria-label="{{ 'Owned' if f.owned else 'Not owned' }}">{% if f.owned %}✔{% else %}✖{% endif %}</div>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="newDeckTitle" style="position:fixed; inset:0; z-index:1000; display:flex; align-items:flex-start; justify-content:center; padding:1rem; overflow:auto;">
|
||||
<div class="modal-backdrop" style="position:fixed; inset:0; background:rgba(0,0,0,.6);"></div>
|
||||
<div class="modal-content" style="position:relative; max-width:720px; width:clamp(320px, 90vw, 720px); background:#0f1115; border:1px solid var(--border); border-radius:10px; box-shadow:0 10px 30px rgba(0,0,0,.5); padding:1rem; max-height:min(92vh, 100%); overflow:auto; -webkit-overflow-scrolling:touch;">
|
||||
<div class="modal modal-overlay" id="new-deck-modal" role="dialog" aria-modal="true" aria-labelledby="newDeckTitle">
|
||||
<div class="modal-backdrop"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="newDeckTitle">Build a New Deck</h3>
|
||||
</div>
|
||||
{% if error %}
|
||||
<div class="error" role="alert" style="margin:.35rem 0 .5rem 0;">{{ error }}</div>
|
||||
<div class="error my-2" role="alert">{{ error }}</div>
|
||||
{% endif %}
|
||||
<form hx-post="/build/new" hx-target="#wizard" hx-swap="innerHTML" hx-on="htmx:afterRequest: (function(evt){ try{ if(evt && evt.detail && evt.detail.elt === this){ var m=this.closest('.modal'); if(m){ m.remove(); } } }catch(_){} }).call(this, event)" autocomplete="off">
|
||||
<fieldset>
|
||||
<legend>Basics</legend>
|
||||
<div class="basics-grid" style="display:grid; grid-template-columns: 2fr 1fr; gap:1rem; align-items:start;">
|
||||
<div class="basics-grid grid grid-cols-[2fr_1fr] gap-4 items-start">
|
||||
<div>
|
||||
<label style="display:block; margin-bottom:.5rem;">
|
||||
<label class="form-label">
|
||||
<span class="muted">Optional name (used for file names)</span>
|
||||
<input type="text" name="name" placeholder="e.g., Inti Discard Tempo" autocomplete="off" autocapitalize="off" spellcheck="false" />
|
||||
</label>
|
||||
<label style="display:block; margin-bottom:.5rem;">
|
||||
<label class="form-label">
|
||||
<span>Commander</span>
|
||||
<input type="text" name="commander" required placeholder="Type a commander name" value="{{ form.commander if form else '' }}" autofocus autocomplete="off" autocapitalize="off" spellcheck="false"
|
||||
role="combobox" aria-autocomplete="list" aria-controls="newdeck-candidates"
|
||||
|
|
@ -24,11 +24,11 @@
|
|||
data-hx-debounce="220" data-hx-debounce-events="input"
|
||||
data-hx-debounce-flush="blur" />
|
||||
</label>
|
||||
<small class="muted" style="display:block; margin-top:.25rem;">Start typing to see matches, then select one to load themes.</small>
|
||||
<div id="newdeck-candidates" class="muted" style="font-size:12px; min-height:1.1em;"></div>
|
||||
<small class="muted block mt-1">Start typing to see matches, then select one to load themes.</small>
|
||||
<div id="newdeck-candidates" class="muted text-xs min-h-[1.1em]"></div>
|
||||
</div>
|
||||
<div id="newdeck-commander-slot" class="muted" style="max-width:230px;">
|
||||
<em style="font-size:12px;">Pick a commander to preview here.</em>
|
||||
<div id="newdeck-commander-slot" class="muted max-w-[230px]">
|
||||
<em class="text-xs">Pick a commander to preview here.</em>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -45,11 +45,11 @@
|
|||
<input type="hidden" name="tag_mode" value="AND" />
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="newdeck-multicopy-slot" class="muted" style="margin-top:.5rem; min-height:1rem;"></div>
|
||||
<div id="newdeck-multicopy-slot" class="muted mt-2 min-h-[1rem]"></div>
|
||||
{% if enable_custom_themes %}
|
||||
{% include "build/_new_deck_additional_themes.html" %}
|
||||
{% endif %}
|
||||
<div style="margin-top:.5rem;" id="newdeck-bracket-slot">
|
||||
<div class="mt-2" id="newdeck-bracket-slot">
|
||||
<label>Bracket
|
||||
<select name="bracket">
|
||||
{% for b in brackets %}
|
||||
|
|
@ -63,47 +63,47 @@
|
|||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Preferences</legend>
|
||||
<div style="text-align: left;">
|
||||
<div style="margin-bottom: 1rem; display:flex; flex-direction:column; gap:0.75rem;">
|
||||
<label for="pref-combos-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="When enabled, the builder will try to auto-complete missing combo partners near the end of the build (respecting owned-only and locks).">
|
||||
<input type="checkbox" name="prefer_combos" id="pref-combos-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.prefer_combos %}checked{% endif %} />
|
||||
<div class="text-left">
|
||||
<div class="mb-4 flex flex-col gap-3">
|
||||
<label for="pref-combos-chk" class="form-checkbox-label" title="When enabled, the builder will try to auto-complete missing combo partners near the end of the build (respecting owned-only and locks).">
|
||||
<input type="checkbox" name="prefer_combos" id="pref-combos-chk" value="1" {% if form and form.prefer_combos %}checked{% endif %} />
|
||||
<span>Prioritize combos</span>
|
||||
</label>
|
||||
<div id="pref-combos-config" style="margin-top: 0.5rem; margin-left: 1.5rem; padding: 0.5rem; border: 1px solid var(--border); border-radius: 8px; display: none;">
|
||||
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;">
|
||||
<div id="pref-combos-config" class="mt-2 ml-6 p-2 border border-[var(--border)] rounded-lg hidden">
|
||||
<div class="flex gap-4 items-center flex-wrap">
|
||||
<label>
|
||||
<span>How many combos?</span>
|
||||
<input type="number" name="combo_count" min="0" max="10" step="1" value="{{ form.combo_count if form and form.combo_count is not none else 2 }}" style="width: 6rem; margin-left: 0.5rem;" />
|
||||
<input type="number" name="combo_count" min="0" max="10" step="1" value="{{ form.combo_count if form and form.combo_count is not none else 2 }}" class="w-24 ml-2" />
|
||||
</label>
|
||||
<div>
|
||||
<div class="muted" style="font-size: 12px; margin-bottom: 0.25rem;">Balance of early vs late-game</div>
|
||||
<label style="display: inline-flex; align-items: center; gap: 0.25rem; margin-right: 0.5rem;">
|
||||
<div class="muted text-xs mb-1">Balance of early vs late-game</div>
|
||||
<label class="inline-flex items-center gap-1 mr-2">
|
||||
<input type="radio" name="combo_balance" value="early" {% if form and form.combo_balance == 'early' %}checked{% endif %} /> Early
|
||||
</label>
|
||||
<label style="display: inline-flex; align-items: center; gap: 0.25rem; margin-right: 0.5rem;">
|
||||
<label class="inline-flex items-center gap-1 mr-2">
|
||||
<input type="radio" name="combo_balance" value="late" {% if form and form.combo_balance == 'late' %}checked{% endif %} /> Late
|
||||
</label>
|
||||
<label style="display: inline-flex; align-items: center; gap: 0.25rem;">
|
||||
<label class="inline-flex items-center gap-1">
|
||||
<input type="radio" name="combo_balance" value="mix" {% if not form or (form and (not form.combo_balance or form.combo_balance == 'mix')) %}checked{% endif %} /> Mix
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label for="pref-mc-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="When enabled, include a Multi-Copy package for matching archetypes (e.g., tokens/tribal).">
|
||||
<input type="checkbox" name="enable_multicopy" id="pref-mc-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.enable_multicopy %}checked{% endif %} />
|
||||
<label for="pref-mc-chk" class="form-checkbox-label" title="When enabled, include a Multi-Copy package for matching archetypes (e.g., tokens/tribal).">
|
||||
<input type="checkbox" name="enable_multicopy" id="pref-mc-chk" value="1" {% if form and form.enable_multicopy %}checked{% endif %} />
|
||||
<span>Enable Multi-Copy package</span>
|
||||
</label>
|
||||
<div style="display:flex; flex-direction:column; gap:0.5rem; margin-top:0.75rem;">
|
||||
<label for="use-owned-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="Limit the pool to cards you already own. Cards outside your owned library will be skipped.">
|
||||
<input type="checkbox" name="use_owned_only" id="use-owned-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.use_owned_only %}checked{% endif %} />
|
||||
<div class="flex flex-col gap-2 mt-3">
|
||||
<label for="use-owned-chk" class="form-checkbox-label" title="Limit the pool to cards you already own. Cards outside your owned library will be skipped.">
|
||||
<input type="checkbox" name="use_owned_only" id="use-owned-chk" value="1" {% if form and form.use_owned_only %}checked{% endif %} />
|
||||
<span>Use only owned cards</span>
|
||||
</label>
|
||||
<label for="prefer-owned-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="Still allow unowned cards, but rank owned cards higher when choosing picks.">
|
||||
<input type="checkbox" name="prefer_owned" id="prefer-owned-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.prefer_owned %}checked{% endif %} />
|
||||
<label for="prefer-owned-chk" class="form-checkbox-label" title="Still allow unowned cards, but rank owned cards higher when choosing picks.">
|
||||
<input type="checkbox" name="prefer_owned" id="prefer-owned-chk" value="1" {% if form and form.prefer_owned %}checked{% endif %} />
|
||||
<span>Prefer owned cards (allow unowned fallback)</span>
|
||||
</label>
|
||||
<label for="swap-mdfc-chk" style="display:grid; grid-template-columns:auto 1fr; align-items:center; column-gap:0.5rem; margin:0; width:100%; cursor:pointer; text-align:left;" title="When enabled, modal DFC lands will replace a matching basic land as they are added so land counts stay level without manual trims.">
|
||||
<input type="checkbox" name="swap_mdfc_basics" id="swap-mdfc-chk" value="1" style="margin:0; cursor:pointer;" {% if form and form.swap_mdfc_basics %}checked{% endif %} />
|
||||
<label for="swap-mdfc-chk" class="form-checkbox-label" title="When enabled, modal DFC lands will replace a matching basic land as they are added so land counts stay level without manual trims.">
|
||||
<input type="checkbox" name="swap_mdfc_basics" id="swap-mdfc-chk" value="1" {% if form and form.swap_mdfc_basics %}checked{% endif %} />
|
||||
<span>Swap basics for MDFC lands</span>
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -114,101 +114,97 @@
|
|||
{% if allow_must_haves %}
|
||||
<fieldset>
|
||||
<legend>Include/Exclude Cards</legend>
|
||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:1rem; margin-top:.5rem;" class="include-exclude-grid">
|
||||
<div class="include-exclude-grid">
|
||||
<!-- Include Cards Column (Left, Green) -->
|
||||
<div>
|
||||
<label style="display:block; margin-bottom:.5rem;">
|
||||
<span style="color: #4ade80; font-weight: 500;">✓ Must Include Cards</span>
|
||||
<small class="muted" style="display:block; font-size:11px; margin-top:.25rem;">Cards that must appear in your deck</small>
|
||||
<label class="card-list-label">
|
||||
<span class="card-list-label-include">✓ Must Include Cards</span>
|
||||
<small class="muted block text-xs mt-1">Cards that must appear in your deck</small>
|
||||
</label>
|
||||
<textarea name="include_cards" id="include_cards_textarea"
|
||||
placeholder="Lightning Bolt Counterspell Swords to Plowshares"
|
||||
style="width:100%; min-height:60px; resize:vertical; font-family:monospace; font-size:12px; border-left: 3px solid #4ade80;"
|
||||
class="include-textarea"
|
||||
autocomplete="off" autocapitalize="off" spellcheck="false">{{ form.include_cards if form and form.include_cards else '' }}</textarea>
|
||||
<!-- Include Cards Chips Container -->
|
||||
<div id="include_chips_container" style="margin-top:.5rem; min-height:30px; border:1px solid #4ade80; border-radius:6px; padding:.5rem; background:rgba(74, 222, 128, 0.05); display:flex; flex-wrap:wrap; gap:.25rem; align-items:flex-start;">
|
||||
<div id="include_chips" style="display:flex; flex-wrap:wrap; gap:.25rem; flex:1;"></div>
|
||||
<div style="color:#6b7280; font-size:11px; font-style:italic;" id="include_chips_placeholder">Enter card names above to see them as removable tags</div>
|
||||
<div id="include_chips_container" class="include-chips-container">
|
||||
<div id="include_chips" class="chips-inner"></div>
|
||||
<div class="chips-placeholder" id="include_chips_placeholder">Enter card names above to see them as removable tags</div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:.5rem; margin-top:.5rem; font-size:12px;">
|
||||
<label for="include_file_upload" class="btn" style="cursor:pointer; font-size:11px; padding:.25rem .5rem; background:#065f46; border-color:#059669;">
|
||||
<div class="card-list-controls">
|
||||
<label for="include_file_upload" class="btn btn-upload-include">
|
||||
📄 Upload .txt
|
||||
</label>
|
||||
<input type="file" id="include_file_upload" accept=".txt" style="display:none;"
|
||||
<input type="file" id="include_file_upload" accept=".txt" class="hidden"
|
||||
onchange="handleIncludeFileUpload(this)" />
|
||||
<button type="button" onclick="clearIncludes()" class="btn" style="font-size:11px; padding:.25rem .5rem; background:#7f1d1d; border-color:#dc2626;">
|
||||
<button type="button" onclick="clearIncludes()" class="btn btn-clear">
|
||||
🗑 Clear All
|
||||
</button>
|
||||
<div id="include_count" class="muted" style="font-size:11px;">0/10</div>
|
||||
<div id="include_badges" style="display:flex; gap:.25rem; font-size:10px;"></div>
|
||||
<div id="include_count" class="muted card-list-count">0/10</div>
|
||||
<div id="include_badges" class="card-list-badges"></div>
|
||||
</div>
|
||||
<div id="include_validation" style="margin-top:.5rem; font-size:12px;"></div>
|
||||
<div id="include_validation" class="card-list-validation"></div>
|
||||
</div>
|
||||
<!-- Exclude Cards Column (Right, Red) -->
|
||||
<div>
|
||||
<label style="display:block; margin-bottom:.5rem;">
|
||||
<span style="color: #ef4444; font-weight: 500;">✗ Must Exclude Cards</span>
|
||||
<small class="muted" style="display:block; font-size:11px; margin-top:.25rem;">Cards to avoid in your deck</small>
|
||||
<label class="card-list-label">
|
||||
<span class="card-list-label-exclude">✗ Must Exclude Cards</span>
|
||||
<small class="muted block text-xs mt-1">Cards to avoid in your deck</small>
|
||||
</label>
|
||||
<textarea name="exclude_cards" id="exclude_cards_textarea"
|
||||
placeholder="Sol Ring Rhystic Study Smothering Tithe"
|
||||
style="width:100%; min-height:60px; resize:vertical; font-family:monospace; font-size:12px; border-left: 3px solid #ef4444;"
|
||||
class="exclude-textarea"
|
||||
autocomplete="off" autocapitalize="off" spellcheck="false">{{ form.exclude_cards if form and form.exclude_cards else '' }}</textarea>
|
||||
<!-- Exclude Cards Chips Container -->
|
||||
<div id="exclude_chips_container" style="margin-top:.5rem; min-height:30px; border:1px solid #ef4444; border-radius:6px; padding:.5rem; background:rgba(239, 68, 68, 0.05); display:flex; flex-wrap:wrap; gap:.25rem; align-items:flex-start;">
|
||||
<div id="exclude_chips" style="display:flex; flex-wrap:wrap; gap:.25rem; flex:1;"></div>
|
||||
<div style="color:#6b7280; font-size:11px; font-style:italic;" id="exclude_chips_placeholder">Enter card names above to see them as removable tags</div>
|
||||
<div id="exclude_chips_container" class="exclude-chips-container">
|
||||
<div id="exclude_chips" class="chips-inner"></div>
|
||||
<div class="chips-placeholder" id="exclude_chips_placeholder">Enter card names above to see them as removable tags</div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:.5rem; margin-top:.5rem; font-size:12px;">
|
||||
<label for="exclude_file_upload" class="btn" style="cursor:pointer; font-size:11px; padding:.25rem .5rem; background:#7f1d1d; border-color:#dc2626;">
|
||||
<div class="card-list-controls">
|
||||
<label for="exclude_file_upload" class="btn btn-upload-exclude">
|
||||
📄 Upload .txt
|
||||
</label>
|
||||
<input type="file" id="exclude_file_upload" accept=".txt" style="display:none;"
|
||||
<input type="file" id="exclude_file_upload" accept=".txt" class="hidden"
|
||||
onchange="handleExcludeFileUpload(this)" />
|
||||
<button type="button" onclick="clearExcludes()" class="btn" style="font-size:11px; padding:.25rem .5rem; background:#7f1d1d; border-color:#dc2626;">
|
||||
<button type="button" onclick="clearExcludes()" class="btn btn-clear">
|
||||
🗑 Clear All
|
||||
</button>
|
||||
<div id="exclude_count" class="muted" style="font-size:11px;">0/15</div>
|
||||
<div id="exclude_badges" style="display:flex; gap:.25rem; font-size:10px;"></div>
|
||||
<div id="exclude_count" class="muted card-list-count">0/15</div>
|
||||
<div id="exclude_badges" class="card-list-badges"></div>
|
||||
</div>
|
||||
<div id="exclude_validation" style="margin-top:.5rem; font-size:12px;"></div>
|
||||
<div id="exclude_validation" class="card-list-validation"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:.75rem; padding:.5rem; background:rgba(59, 130, 246, 0.1); border:1px solid rgba(59, 130, 246, 0.3); border-radius:6px;">
|
||||
<div class="info-panel">
|
||||
<details>
|
||||
<summary style="cursor:pointer; font-size:12px; color:#60a5fa;">Advanced Options</summary>
|
||||
<div style="margin-top:.5rem; font-size:12px; line-height:1.5;">
|
||||
<div style="margin-bottom:.5rem;">
|
||||
<label style="display:inline-flex; align-items:center; margin-right:1rem; cursor:pointer; line-height:1;">
|
||||
<input type="radio" name="enforcement_mode" value="warn" {% if not form or (form and (not form.enforcement_mode or form.enforcement_mode == 'warn')) %}checked{% endif %} style="margin:0 4px 0 0; flex-shrink:0;" />
|
||||
<span>Warn mode</span>
|
||||
<small class="muted" style="margin-left:.25rem;">(proceed if cards missing)</small>
|
||||
<summary>Advanced Options</summary>
|
||||
<div class="info-panel-content">
|
||||
<div class="mb-2">
|
||||
<label class="form-checkbox-label">
|
||||
<input type="radio" name="enforcement_mode" value="warn" {% if not form or (form and (not form.enforcement_mode or form.enforcement_mode == 'warn')) %}checked{% endif %} />
|
||||
<span>Warn mode <small class="muted ml-1">(proceed if cards missing)</small></span>
|
||||
</label>
|
||||
<label style="display:inline-flex; align-items:center; margin-right:1rem; cursor:pointer; line-height:1;">
|
||||
<input type="radio" name="enforcement_mode" value="strict" {% if form and form.enforcement_mode == 'strict' %}checked{% endif %} style="margin:0 4px 0 0; flex-shrink:0;" />
|
||||
<span>Strict mode</span>
|
||||
<small class="muted" style="margin-left:.25rem;">(abort if cards missing)</small>
|
||||
<label class="form-checkbox-label">
|
||||
<input type="radio" name="enforcement_mode" value="strict" {% if form and form.enforcement_mode == 'strict' %}checked{% endif %} />
|
||||
<span>Strict mode <small class="muted ml-1">(abort if cards missing)</small></span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:inline-flex; align-items:center; margin-right:1rem; cursor:pointer; line-height:1;">
|
||||
<input type="checkbox" name="allow_illegal" {% if form and form.allow_illegal %}checked{% endif %} style="margin:0 4px 0 0; flex-shrink:0;" />
|
||||
<label class="form-checkbox-label">
|
||||
<input type="checkbox" name="allow_illegal" {% if form and form.allow_illegal %}checked{% endif %} />
|
||||
<span>Allow illegal cards</span>
|
||||
</label>
|
||||
<label style="display:inline-flex; align-items:center; margin-right:1rem; cursor:pointer; line-height:1;">
|
||||
<input type="checkbox" name="fuzzy_matching" {% if not form or (form and (form.fuzzy_matching is none or form.fuzzy_matching)) %}checked{% endif %} style="margin:0 4px 0 0; flex-shrink:0;" />
|
||||
<span>Fuzzy name matching</span>
|
||||
</label>
|
||||
<!-- Fuzzy matching always enabled - hidden field -->
|
||||
<input type="hidden" name="fuzzy_matching" value="1" />
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<small class="muted" style="display:block; margin-top:.5rem; font-size:11px; text-align:center;">
|
||||
Enter one card name per line. Cards are validated against the database with smart name matching.
|
||||
<small class="muted block mt-2 text-xs text-center">
|
||||
Enter one card name per line. Cards are validated with fuzzy name matching.
|
||||
</small>
|
||||
</fieldset>
|
||||
{% if not show_must_have_buttons %}
|
||||
<div class="muted" style="font-size:12px; margin-top:.75rem;">
|
||||
<div class="muted text-xs mt-3">
|
||||
Step 5 quick-add buttons are hidden (<code>SHOW_MUST_HAVE_BUTTONS=0</code>), but you can still seed must include/exclude lists here.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -217,22 +213,22 @@
|
|||
{% if enable_batch_build %}
|
||||
<fieldset>
|
||||
<legend>Build Options</legend>
|
||||
<div style="display:flex; flex-direction:column; gap:0.75rem;">
|
||||
<label style="display:block;">
|
||||
<div class="flex flex-col gap-3">
|
||||
<label class="block">
|
||||
<span>Number of decks to build</span>
|
||||
<small class="muted" style="display:block; font-size:11px; margin-top:.25rem;">Run the same configuration multiple times to see variance in results</small>
|
||||
<small class="muted block text-xs mt-1">Run the same configuration multiple times to see variance in results</small>
|
||||
</label>
|
||||
{% if ideals_ui_mode == 'slider' %}
|
||||
<div style="display:flex; align-items:center; gap:1rem;">
|
||||
<input type="range" name="build_count" id="build_count_slider" min="1" max="10" value="{{ form.build_count if form and form.build_count else 1 }}" style="flex:1;"
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="range" name="build_count" id="build_count_slider" min="1" max="10" value="{{ form.build_count if form and form.build_count else 1 }}" class="flex-1"
|
||||
oninput="document.getElementById('build_count_value').textContent = this.value; updateBuildCountLabel(this.value); updateButtonState(this.value);" />
|
||||
<span id="build_count_value" style="min-width:2.5rem; text-align:center; font-weight:500; font-size:1.1em;">{{ form.build_count if form and form.build_count else 1 }}</span>
|
||||
<span id="build_count_value" class="min-w-[2.5rem] text-center font-medium text-lg">{{ form.build_count if form and form.build_count else 1 }}</span>
|
||||
</div>
|
||||
<small id="build_count_label" class="muted" style="font-size:11px; text-align:center;">Build 1 deck (normal build)</small>
|
||||
<small id="build_count_label" class="muted text-xs text-center">Build 1 deck (normal build)</small>
|
||||
{% else %}
|
||||
<input type="number" name="build_count" id="build_count_input" min="1" max="10" value="{{ form.build_count if form and form.build_count else 1 }}" style="width:6rem;"
|
||||
<input type="number" name="build_count" id="build_count_input" min="1" max="10" value="{{ form.build_count if form and form.build_count else 1 }}" class="w-24"
|
||||
oninput="updateButtonState(this.value);" />
|
||||
<small class="muted" style="font-size:11px;">Enter 1 for normal build, 2-10 to compare multiple results</small>
|
||||
<small class="muted text-xs">Enter 1 for normal build, 2-10 to compare multiple results</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -240,9 +236,9 @@
|
|||
{# Hidden input to always send build_count=1 when feature disabled #}
|
||||
<input type="hidden" name="build_count" value="1" />
|
||||
{% endif %}
|
||||
<div class="modal-footer" style="display:flex; gap:.5rem; justify-content:space-between; margin-top:1rem;">
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Cancel</button>
|
||||
<div style="display:flex; gap:.5rem;">
|
||||
<div class="modal-footer-left">
|
||||
<button type="submit" name="quick_build" value="1" class="btn-continue" id="quick-build-btn" title="Build entire deck automatically without approval steps">Quick Build</button>
|
||||
<button type="submit" class="btn-continue" id="create-btn">Create</button>
|
||||
</div>
|
||||
|
|
@ -794,7 +790,8 @@
|
|||
formData.append('commander', commander);
|
||||
formData.append('enforcement_mode', enforcementMode ? enforcementMode.value : 'warn');
|
||||
formData.append('allow_illegal', allowIllegal ? allowIllegal.checked : false);
|
||||
formData.append('fuzzy_matching', fuzzyMatching ? fuzzyMatching.checked : true);
|
||||
// For hidden input, use .value instead of .checked (hidden inputs don't have checked property)
|
||||
formData.append('fuzzy_matching', fuzzyMatching ? (fuzzyMatching.value || 'true') : 'true');
|
||||
|
||||
console.log('Making fetch request to /build/validate/include_exclude');
|
||||
fetch('/build/validate/include_exclude', {
|
||||
|
|
@ -1536,11 +1533,32 @@
|
|||
|
||||
<style>
|
||||
/* Modal responsive tweaks (scoped) */
|
||||
@media (max-width: 720px){
|
||||
@media (max-width: 600px){
|
||||
.modal .basics-grid{ grid-template-columns: 1fr !important; }
|
||||
#newdeck-commander-slot{ max-width: 100% !important; }
|
||||
#newdeck-commander-slot aside.card-preview{ max-width: 100% !important; }
|
||||
#newdeck-commander-slot img{ width: 100% !important; max-width: 260px; height: auto; margin: 0 auto; display: block; }
|
||||
.modal .modal-content{ width: min(95vw, 560px) !important; }
|
||||
}
|
||||
/* Keep grid layout on tablet and desktop */
|
||||
@media (min-width: 601px){
|
||||
.modal .basics-grid{
|
||||
display: grid !important;
|
||||
grid-template-columns: 2fr 1fr !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Remove any existing modal to prevent duplicates
|
||||
(function() {
|
||||
// Find all modals with the same ID
|
||||
var allModals = document.querySelectorAll('#new-deck-modal');
|
||||
// If there's more than one, remove all but the last (newest) one
|
||||
if (allModals.length > 1) {
|
||||
for (var i = 0; i < allModals.length - 1; i++) {
|
||||
allModals[i].remove();
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -15,15 +15,15 @@
|
|||
<div id="newdeck-commander-slot" hx-swap-oob="true" style="max-width:230px;">
|
||||
<aside class="card-preview" data-card-name="{{ pname }}" style="max-width: 230px;">
|
||||
<a href="https://scryfall.com/search?q={{ pname|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ pname|urlencode }}&format=image&version=normal" alt="{{ pname }} card image" data-card-name="{{ pname }}" style="width:200px; height:auto; display:block; border-radius:6px;" />
|
||||
<img src="{{ pname|card_image('normal') }}" alt="{{ pname }} card image" data-card-name="{{ pname }}" style="width:200px; height:auto; display:block; border-radius:6px;" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="muted" style="font-size:12px; margin-top:.25rem; max-width: 230px;">{{ pname }}</div>
|
||||
<div class="muted" style="font-size:12px; margin-top:.25rem; max-width: 230px; word-wrap:break-word; overflow-wrap:break-word;">{{ pname }}</div>
|
||||
{% if partner_preview_payload %}
|
||||
{% set partner_secondary_name = partner_preview_payload.secondary_name %}
|
||||
{% set partner_image_url = partner_preview_payload.secondary_image_url or partner_preview_payload.image_url %}
|
||||
{% if not partner_image_url and partner_secondary_name %}
|
||||
{% set partner_image_url = 'https://api.scryfall.com/cards/named?fuzzy=' ~ partner_secondary_name|urlencode ~ '&format=image&version=normal' %}
|
||||
{% set partner_image_url = partner_secondary_name|card_image('normal') %}
|
||||
{% endif %}
|
||||
{% set partner_href = partner_preview_payload.secondary_scryfall_url or partner_preview_payload.scryfall_url %}
|
||||
{% if not partner_href and partner_secondary_name %}
|
||||
|
|
@ -224,36 +224,83 @@
|
|||
});
|
||||
}
|
||||
document.querySelectorAll('input[name="combine_mode_radio"]').forEach(function(r){ r.addEventListener('change', function(){ if(mode){ mode.value = r.value; } }); });
|
||||
function updatePartnerRecommendations(tags){
|
||||
if (!reco) return;
|
||||
Array.from(reco.querySelectorAll('button.partner-suggestion')).forEach(function(btn){ btn.remove(); });
|
||||
var unique = [];
|
||||
|
||||
function updatePartnerTags(partnerTags){
|
||||
if (!list || !reco) return;
|
||||
|
||||
// Remove old partner-added chips from available list
|
||||
Array.from(list.querySelectorAll('button.partner-added')).forEach(function(btn){ btn.remove(); });
|
||||
|
||||
// Deduplicate: remove partner tags from recommended section to avoid showing them twice
|
||||
if (partnerTags && partnerTags.length > 0) {
|
||||
var partnerTagsLower = partnerTags.map(function(t){ return String(t || '').trim().toLowerCase(); });
|
||||
Array.from(reco.querySelectorAll('button.chip-reco:not(.partner-original)')).forEach(function(btn){
|
||||
var tag = btn.dataset.tag || '';
|
||||
if (partnerTagsLower.indexOf(tag.toLowerCase()) >= 0) {
|
||||
btn.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get existing tags from the available list (original server-rendered ones)
|
||||
var existingTags = Array.from(list.querySelectorAll('button.chip:not(.partner-added)')).map(function(b){
|
||||
return {
|
||||
element: b,
|
||||
tag: (b.dataset.tag || '').trim(),
|
||||
tagLower: (b.dataset.tag || '').trim().toLowerCase()
|
||||
};
|
||||
});
|
||||
|
||||
// Build combined list: existing + new partner tags
|
||||
var combined = [];
|
||||
var seen = new Set();
|
||||
(Array.isArray(tags) ? tags : []).forEach(function(tag){
|
||||
|
||||
// Add existing tags first
|
||||
existingTags.forEach(function(item){
|
||||
if (!item.tag || seen.has(item.tagLower)) return;
|
||||
seen.add(item.tagLower);
|
||||
combined.push({ tag: item.tag, element: item.element, isPartner: false });
|
||||
});
|
||||
|
||||
// Add new partner tags
|
||||
(Array.isArray(partnerTags) ? partnerTags : []).forEach(function(tag){
|
||||
var value = String(tag || '').trim();
|
||||
if (!value) return;
|
||||
var key = value.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
unique.push(value);
|
||||
combined.push({ tag: value, element: null, isPartner: true });
|
||||
});
|
||||
var insertBefore = selAll && selAll.parentElement === reco ? selAll : null;
|
||||
unique.forEach(function(tag){
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'chip chip-reco partner-suggestion';
|
||||
btn.dataset.tag = tag;
|
||||
btn.title = 'Synergizes with selected partner pairing';
|
||||
btn.textContent = '★ ' + tag;
|
||||
if (insertBefore){ reco.insertBefore(btn, insertBefore); }
|
||||
else { reco.appendChild(btn); }
|
||||
|
||||
// Sort alphabetically
|
||||
combined.sort(function(a, b){ return a.tag.localeCompare(b.tag); });
|
||||
|
||||
// Re-render the list in sorted order
|
||||
list.innerHTML = '';
|
||||
combined.forEach(function(item){
|
||||
if (item.element) {
|
||||
// Re-append existing element
|
||||
list.appendChild(item.element);
|
||||
} else {
|
||||
// Create new partner-added chip
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'chip partner-added';
|
||||
btn.dataset.tag = item.tag;
|
||||
btn.title = 'From combined partner themes';
|
||||
btn.textContent = item.tag;
|
||||
list.appendChild(btn);
|
||||
}
|
||||
});
|
||||
var hasAny = reco.querySelectorAll('button.chip-reco').length > 0;
|
||||
|
||||
// Update visibility of recommended section
|
||||
var hasAnyReco = reco.querySelectorAll('button.chip-reco').length > 0;
|
||||
if (recoBlock){
|
||||
recoBlock.style.display = hasAny ? '' : 'none';
|
||||
recoBlock.setAttribute('data-has-reco', hasAny ? '1' : '0');
|
||||
recoBlock.style.display = hasAnyReco ? '' : 'none';
|
||||
recoBlock.setAttribute('data-has-reco', hasAnyReco ? '1' : '0');
|
||||
}
|
||||
if (selAll){ selAll.style.display = hasAny ? '' : 'none'; }
|
||||
if (selAll){ selAll.style.display = hasAnyReco ? '' : 'none'; }
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
|
|
@ -264,11 +311,11 @@
|
|||
if (!tags.length && detail.payload && Array.isArray(detail.payload.theme_tags)){
|
||||
tags = detail.payload.theme_tags;
|
||||
}
|
||||
updatePartnerRecommendations(tags);
|
||||
updatePartnerTags(tags);
|
||||
});
|
||||
|
||||
var initialPartnerTags = readPartnerPreviewTags();
|
||||
updatePartnerRecommendations(initialPartnerTags);
|
||||
updatePartnerTags(initialPartnerTags);
|
||||
updateUI();
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
{% if partner_preview %}
|
||||
{% set preview_image = partner_preview.secondary_image_url or partner_preview.image_url %}
|
||||
{% if not preview_image and partner_preview.secondary_name %}
|
||||
{% set preview_image = 'https://api.scryfall.com/cards/named?fuzzy=' ~ partner_preview.secondary_name|urlencode ~ '&format=image&version=normal' %}
|
||||
{% set preview_image = partner_preview.secondary_name|card_image('normal') %}
|
||||
{% endif %}
|
||||
{% set preview_href = partner_preview.secondary_scryfall_url or partner_preview.scryfall_url %}
|
||||
{% if not preview_href and partner_preview.secondary_name %}
|
||||
|
|
@ -463,7 +463,7 @@
|
|||
};
|
||||
function buildCardImageUrl(name){
|
||||
if (!name) return '';
|
||||
return 'https://api.scryfall.com/cards/named?fuzzy=' + encodeURIComponent(name) + '&format=image&version=normal';
|
||||
return '/api/images/normal/' + encodeURIComponent(name);
|
||||
}
|
||||
function buildScryfallUrl(name){
|
||||
if (!name) return '';
|
||||
|
|
@ -528,7 +528,9 @@
|
|||
var colorLabel = payload.color_label || '';
|
||||
var secondaryName = payload.secondary_name || payload.name || '';
|
||||
var primary = payload.primary_name || primaryName;
|
||||
var themes = Array.isArray(payload.theme_tags) ? payload.theme_tags : [];
|
||||
// Ensure theme_tags is always an array, even if it comes as a string or other type
|
||||
var themes = Array.isArray(payload.theme_tags) ? payload.theme_tags :
|
||||
(typeof payload.theme_tags === 'string' ? payload.theme_tags.split(',').map(function(t){ return t.trim(); }).filter(Boolean) : []);
|
||||
var imageUrl = payload.secondary_image_url || payload.image_url || '';
|
||||
if (!imageUrl && secondaryName){
|
||||
imageUrl = buildCardImageUrl(secondaryName);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
<form hx-post="/build/step1/confirm" hx-target="#wizard" hx-swap="innerHTML">
|
||||
<input type="hidden" name="name" value="{{ name }}" />
|
||||
<button class="img-btn" type="submit" title="Select {{ name }} (score {{ score }})">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ name|urlencode }}&format=image&version=normal" data-card-name="{{ name }}"
|
||||
<img src="{{ name|card_image('normal') }}" data-card-name="{{ name }}"
|
||||
alt="{{ name }}" loading="lazy" decoding="async" />
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
{# Strip synergy annotation for Scryfall search and image fuzzy param #}
|
||||
{% set sel_base = (selected.split(' - Synergy (')[0] if ' - Synergy (' in selected else selected) %}
|
||||
<a href="https://scryfall.com/search?q={{ sel_base|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ sel_base|urlencode }}&format=image&version=normal" alt="{{ selected }} card image" data-card-name="{{ sel_base }}" />
|
||||
<img src="{{ sel_base|card_image('normal') }}" alt="{{ selected }} card image" data-card-name="{{ sel_base }}" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow">
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
{# Strip synergy annotation for Scryfall search and image fuzzy param #}
|
||||
{% set commander_base = (commander.name.split(' - Synergy (')[0] if ' - Synergy (' in commander.name else commander.name) %}
|
||||
<a href="https://scryfall.com/search?q={{ commander_base|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander_base|urlencode }}&format=image&version=normal" alt="{{ commander.name }} card image" data-card-name="{{ commander_base }}" />
|
||||
<img src="{{ commander_base|card_image('normal') }}" alt="{{ commander.name }} card image" data-card-name="{{ commander_base }}" />
|
||||
</a>
|
||||
</aside>
|
||||
{% if partner_preview_payload %}
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
{% set partner_name_base = partner_secondary_name %}
|
||||
{% endif %}
|
||||
{% if not partner_image_url and partner_name_base %}
|
||||
{% set partner_image_url = 'https://api.scryfall.com/cards/named?fuzzy=' ~ partner_name_base|urlencode ~ '&format=image&version=normal' %}
|
||||
{% set partner_image_url = partner_name_base|card_image('normal') %}
|
||||
{% endif %}
|
||||
{% set partner_href = partner_preview_payload.secondary_scryfall_url or partner_preview_payload.scryfall_url %}
|
||||
{% if not partner_href and partner_name_base %}
|
||||
|
|
@ -35,14 +35,14 @@
|
|||
{% if partner_tags_joined %}data-tags="{{ partner_tags_joined }}" data-overlaps="{{ partner_tags_joined }}"{% endif %}>
|
||||
{% if partner_href %}<a href="{{ partner_href }}" target="_blank" rel="noopener">{% endif %}
|
||||
{% if partner_name_base %}
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ partner_name_base|urlencode }}&format=image&version=normal" alt="{{ (partner_secondary_name or 'Selected card') ~ ' card image' }}"
|
||||
<img src="{{ partner_name_base|card_image('normal') }}" alt="{{ (partner_secondary_name or 'Selected card') ~ ' card image' }}"
|
||||
width="320"
|
||||
data-card-name="{{ partner_name_base }}"
|
||||
data-original-name="{{ partner_secondary_name }}"
|
||||
data-role="{{ partner_role_label }}"
|
||||
{% if partner_tags_joined %}data-tags="{{ partner_tags_joined }}" data-overlaps="{{ partner_tags_joined }}"{% endif %}
|
||||
loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ partner_name_base|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ partner_name_base|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ partner_name_base|urlencode }}&format=image&version=large 672w"
|
||||
srcset="{{ partner_name_base|card_image('small') }} 160w, {{ partner_name_base|card_image('normal') }} 488w"
|
||||
sizes="(max-width: 900px) 100vw, 320px" />
|
||||
{% else %}
|
||||
<img src="{{ partner_image_url }}" alt="{{ (partner_secondary_name or 'Selected card') ~ ' card image' }}" loading="lazy" decoding="async" width="320" />
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
{# Ensure synergy annotation suffix is stripped for Scryfall query and image fuzzy param #}
|
||||
{% set commander_base = (commander.split(' - Synergy (')[0] if ' - Synergy (' in commander else commander) %}
|
||||
<a href="https://scryfall.com/search?q={{ commander_base|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander_base|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" data-card-name="{{ commander_base }}" />
|
||||
<img src="{{ commander_base|card_image('normal') }}" alt="{{ commander }} card image" data-card-name="{{ commander_base }}" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
|
|
|
|||
32
code/web/templates/build/_step3_skeleton.html
Normal file
32
code/web/templates/build/_step3_skeleton.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<section>
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview" data-card-name="{{ commander|urlencode }}">
|
||||
{% set commander_base = (commander.split(' - Synergy (')[0] if ' - Synergy (' in commander else commander) %}
|
||||
<a href="https://scryfall.com/search?q={{ commander_base|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="{{ commander_base|card_image('normal') }}" alt="{{ commander }} card image" data-card-name="{{ commander_base }}" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
<div hx-get="/build/banner" hx-trigger="load"></div>
|
||||
|
||||
<div style="text-align:center; padding:3rem 1rem;">
|
||||
<div class="spinner" style="margin:0 auto 1rem; width:48px; height:48px; border:4px solid rgba(0,0,0,0.1); border-top-color:#007bff; border-radius:50%; animation:spin 0.8s linear infinite;"></div>
|
||||
<h3 style="margin:0 0 0.5rem;">Automating choices...</h3>
|
||||
<p class="muted" style="margin:0;">{{ automation_message }}</p>
|
||||
</div>
|
||||
|
||||
{# Hidden form that auto-submits with defaults #}
|
||||
<form id="auto-step3-form" hx-post="/build/step3" hx-target="#wizard" hx-swap="innerHTML" hx-trigger="load delay:100ms" style="display:none;">
|
||||
{% for key, value in defaults.items() %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||
{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
{# Strip synergy annotation for Scryfall search and image fuzzy param #}
|
||||
{% set commander_base = (commander.split(' - Synergy (')[0] if ' - Synergy (' in commander else commander) %}
|
||||
<a href="https://scryfall.com/search?q={{ commander_base|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander_base|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" data-card-name="{{ commander_base }}" />
|
||||
<img src="{{ commander_base|card_image('normal') }}" alt="{{ commander }} card image" data-card-name="{{ commander_base }}" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
{% if commander_tag_slugs %}data-tags-slug="{{ commander_tag_slugs|join(' ') }}"{% endif %}
|
||||
{% if commander_overlap_tags %}data-overlaps="{{ commander_overlap_tags|join(', ') }}"{% endif %}
|
||||
{% if commander_reason_text %}data-reasons="{{ commander_reason_text|e }}"{% endif %}>
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander_base|urlencode }}&format=image&version=normal" alt="{{ commander }} card image"
|
||||
<img src="{{ commander_base|card_image('normal') }}" alt="{{ commander }} card image"
|
||||
width="320"
|
||||
data-card-name="{{ commander_base }}"
|
||||
data-original-name="{{ commander }}"
|
||||
|
|
@ -45,10 +45,10 @@
|
|||
{% if commander_overlap_tags %}data-overlaps="{{ commander_overlap_tags|join(', ') }}"{% endif %}
|
||||
{% if commander_reason_text %}data-reasons="{{ commander_reason_text|e }}"{% endif %}
|
||||
loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ commander_base|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ commander_base|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ commander_base|urlencode }}&format=image&version=large 672w"
|
||||
srcset="{{ commander_base|card_image('small') }} 160w, {{ commander_base|card_image('normal') }} 488w"
|
||||
sizes="(max-width: 900px) 100vw, 320px" />
|
||||
</div>
|
||||
<div class="muted" style="margin-top:.25rem;">
|
||||
<div class="muted mt-1">
|
||||
Commander: <span data-card-name="{{ commander }}"
|
||||
data-original-name="{{ commander }}"
|
||||
data-role="{{ commander_role_label or 'Commander' }}"
|
||||
|
|
@ -79,51 +79,51 @@
|
|||
{% if partner_tags_joined %}data-tags="{{ partner_tags_joined }}" data-overlaps="{{ partner_tags_joined }}"{% endif %}>
|
||||
{% if partner_href %}<a href="{{ partner_href }}" target="_blank" rel="noopener">{% endif %}
|
||||
{% if partner_name_base %}
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ partner_name_base|urlencode }}&format=image&version=normal" alt="{{ (partner_secondary_name or 'Selected card') ~ ' card image' }}"
|
||||
<img src="{{ partner_name_base|card_image('normal') }}" alt="{{ (partner_secondary_name or 'Selected card') ~ ' card image' }}"
|
||||
width="320"
|
||||
data-card-name="{{ partner_name_base }}"
|
||||
data-original-name="{{ partner_secondary_name }}"
|
||||
data-role="{{ partner_role_label }}"
|
||||
{% if partner_tags_joined %}data-tags="{{ partner_tags_joined }}" data-overlaps="{{ partner_tags_joined }}"{% endif %}
|
||||
loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ partner_name_base|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ partner_name_base|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ partner_name_base|urlencode }}&format=image&version=large 672w"
|
||||
srcset="{{ partner_name_base|card_image('small') }} 160w, {{ partner_name_base|card_image('normal') }} 488w"
|
||||
sizes="(max-width: 900px) 100vw, 320px" />
|
||||
{% else %}
|
||||
<img src="{{ combined.secondary_image_url or combined.image_url }}" alt="{{ (partner_secondary_name or 'Selected card') ~ ' card image' }}" loading="lazy" decoding="async" width="320" />
|
||||
{% endif %}
|
||||
{% if partner_href %}</a>{% endif %}
|
||||
</div>
|
||||
<div class="muted partner-label" style="margin-top:.35rem;">
|
||||
<div class="muted partner-label mt-1.5">
|
||||
{{ partner_role_label }}:
|
||||
<span data-card-name="{{ partner_secondary_name }}"
|
||||
data-original-name="{{ partner_secondary_name }}"
|
||||
data-role="{{ partner_role_label }}"
|
||||
{% if partner_tags_joined %}data-tags="{{ partner_tags_joined }}" data-overlaps="{{ partner_tags_joined }}"{% endif %}>{{ partner_secondary_name }}</span>
|
||||
</div>
|
||||
<div class="muted partner-meta" style="font-size:12px; margin-top:.25rem;">
|
||||
<div class="muted partner-meta text-xs mt-1">
|
||||
Pairing: {{ combined.primary_name or display_commander_name or commander }}{% if partner_secondary_name %} + {{ partner_secondary_name }}{% endif %}
|
||||
</div>
|
||||
{% if combined.color_label %}
|
||||
<div class="muted partner-meta" style="font-size:12px; margin-top:.25rem;">
|
||||
<div class="muted partner-meta text-xs mt-1">
|
||||
Colors: {{ combined.color_label }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if partner_theme_tags %}
|
||||
<div class="muted partner-meta" style="font-size:12px; margin-top:.25rem;">
|
||||
<div class="muted partner-meta text-xs mt-1">
|
||||
Theme emphasis: {{ partner_theme_tags|join(', ') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if status and status.startswith('Build complete') %}
|
||||
<div style="margin-top:.75rem; display:flex; gap:.35rem; flex-wrap:wrap;">
|
||||
<div class="mt-3 flex gap-1.5 flex-wrap">
|
||||
{% if csv_path %}
|
||||
<form action="/files" method="get" target="_blank" style="display:inline; margin:0;">
|
||||
<form action="/files" method="get" target="_blank" class="inline m-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;">
|
||||
<form action="/files" method="get" target="_blank" class="inline m-0">
|
||||
<input type="hidden" name="path" value="{{ txt_path }}" />
|
||||
<button type="submit">Download TXT</button>
|
||||
</form>
|
||||
|
|
@ -150,64 +150,64 @@
|
|||
{% endif %}
|
||||
</p>
|
||||
{% if show_color_identity %}
|
||||
<div class="muted" style="display:flex; align-items:center; gap:.35rem; margin:-.35rem 0 .5rem 0;">
|
||||
<div class="muted flex items-center gap-1.5 -my-1.5 mb-2">
|
||||
{{ color_identity(color_identity_list, is_colorless=(color_identity_list|length == 0), aria_label=color_label or '', title_text=color_label or '') }}
|
||||
<span>{{ color_label }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>Tags: {% if display_tags %}{{ display_tags|join(', ') }}{% else %}—{% endif %}</p>
|
||||
<div style="margin:.35rem 0; color: var(--muted); display:flex; gap:.5rem; align-items:center; flex-wrap:wrap;">
|
||||
<div class="my-1.5 text-muted flex gap-2 items-center flex-wrap">
|
||||
<span>Owned-only: <strong>{{ 'On' if owned_only else 'Off' }}</strong></span>
|
||||
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
|
||||
<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 class="flex items-center gap-4 flex-wrap">
|
||||
<button type="button" hx-get="/build/step4" hx-target="#wizard" hx-swap="innerHTML" class="bg-gray-700 text-gray-200 border-0 rounded-md px-2 py-1 cursor-pointer text-xs" title="Change owned settings in Review">Edit in Review</button>
|
||||
<div>Prefer-owned: <strong>{{ 'On' if prefer_owned else 'Off' }}</strong></div>
|
||||
<div>MDFC swap: <strong>{{ 'On' if swap_mdfc_basics else 'Off' }}</strong></div>
|
||||
</div>
|
||||
<span style="margin-left:auto;"><a href="/owned" target="_blank" rel="noopener" class="btn">Manage Owned Library</a></span>
|
||||
<span class="ml-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;">
|
||||
<div class="flex items-center gap-2 flex-wrap my-1 mb-2">
|
||||
{% 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>
|
||||
<span class="chip"><span class="dot dot-green"></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>
|
||||
<span class="chip"><span class="dot dot-blue"></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>
|
||||
<span class="chip" title="Combos plan"><span class="dot dot-orange"></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>
|
||||
<span class="chip" title="Trimmed overflow from this stage"><span class="dot dot-red"></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>
|
||||
<span class="chip" title="Multi-Copy package summary"><span class="dot dot-purple"></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"
|
||||
<button type="button" class="btn ml-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="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 %} my-1 mb-4" aria-label="Deck progress" title="{{ deck_count }} of 100 cards" 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>
|
||||
<div class="muted my-1">Adjusted targets: {{ mc_adjustments|join(', ') }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if status %}
|
||||
<div style="margin-top:1rem;">
|
||||
<div class="mt-4">
|
||||
<strong>Status:</strong> {{ status }}{% if stage_label %} — <em>{{ stage_label }}</em>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if gated and (not status or not status.startswith('Build complete')) %}
|
||||
<div class="alert" style="margin-top:.5rem; color:#fecaca; background:#7f1d1d; border:1px solid #991b1b; padding:.5rem .75rem; border-radius:8px;">
|
||||
<div class="alert-error">
|
||||
Compliance gating active — resolve violations above (replace or remove cards) to continue.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
@ -220,15 +220,15 @@
|
|||
|
||||
{% if locked_cards is defined and locked_cards %}
|
||||
{% from 'partials/_macros.html' import lock_button %}
|
||||
<details id="locked-section" style="margin-top:.5rem;">
|
||||
<details id="locked-section" class="mt-2">
|
||||
<summary>Locked cards (always kept)</summary>
|
||||
<ul id="locked-list" style="list-style:none; padding:0; margin:.35rem 0 0; display:grid; gap:.35rem;">
|
||||
<ul id="locked-list" class="locked-list">
|
||||
{% for lk in locked_cards %}
|
||||
<li style="display:flex; align-items:center; gap:.5rem; flex-wrap:wrap;">
|
||||
<li class="locked-item">
|
||||
<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 %}
|
||||
<div class="lock-box" style="display:inline; margin-left:auto;">
|
||||
<div class="lock-box-inline">
|
||||
{{ lock_button(lk.name, True, from_list=True, target_selector='closest li') }}
|
||||
</div>
|
||||
</li>
|
||||
|
|
@ -238,7 +238,7 @@
|
|||
{% endif %}
|
||||
|
||||
<!-- Last action chip (oob-updated) -->
|
||||
<div id="last-action" aria-live="polite" style="margin:.25rem 0; min-height:1.5rem;"></div>
|
||||
<div id="last-action" aria-live="polite" class="my-1 last-action"></div>
|
||||
|
||||
<!-- Filters toolbar -->
|
||||
<div class="cards-toolbar">
|
||||
|
|
@ -248,10 +248,10 @@
|
|||
<option value="owned">Owned</option>
|
||||
<option value="not">Not owned</option>
|
||||
</select>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;">
|
||||
<label class="form-label-icon">
|
||||
<input type="checkbox" name="show_reasons" data-pref="cards:show_reasons" checked /> Show reasons
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;">
|
||||
<label class="form-label-icon">
|
||||
<input type="checkbox" name="collapse_groups" data-pref="cards:collapse" /> Collapse groups
|
||||
</label>
|
||||
<select name="filter_sort" data-pref="cards:sort" aria-label="Sort">
|
||||
|
|
@ -271,37 +271,37 @@
|
|||
</div>
|
||||
|
||||
<!-- Sticky build controls on mobile -->
|
||||
<div class="build-controls" style="position:sticky; 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(_){}">
|
||||
<div class="build-controls">
|
||||
<form hx-post="/build/step5/start" hx-target="#wizard" hx-swap="innerHTML" class="inline-form mr-2" 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(_){}">
|
||||
<form hx-post="/build/step5/continue" hx-target="#wizard" hx-swap="innerHTML" class="inline-form" 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')) or gated %}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(_){}">
|
||||
<form hx-post="/build/step5/rerun" hx-target="#wizard" hx-swap="innerHTML" class="inline-form" 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')) or gated %}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;">
|
||||
<form hx-post="/build/step5/toggle-replace" hx-target="closest .replace-toggle" hx-swap="outerHTML" onsubmit="return false;" class="inline-form">
|
||||
<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.">
|
||||
<label class="muted form-label-icon" 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;">
|
||||
<form hx-post="/build/step5/reset-stage" hx-target="#wizard" hx-swap="innerHTML" class="inline-form">
|
||||
<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;">
|
||||
<form hx-post="/build/reset-all" hx-target="#wizard" hx-swap="innerHTML" class="inline-form">
|
||||
<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;">
|
||||
<label class="muted form-label-icon ml-2">
|
||||
<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
|
||||
|
|
@ -311,14 +311,14 @@
|
|||
|
||||
{% if added_cards is not none %}
|
||||
{% if history is defined and history %}
|
||||
<details style="margin-top:.5rem;">
|
||||
<details class="mt-2">
|
||||
<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;">
|
||||
<div class="muted text-xs my-1">Jump back to a previous stage, then you can continue forward again.</div>
|
||||
<ul class="timeline-list">
|
||||
{% for h in history %}
|
||||
<li style="display:flex; align-items:center; gap:.5rem;">
|
||||
<li class="timeline-item">
|
||||
<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;">
|
||||
<form hx-post="/build/step5/rewind" hx-target="#wizard" hx-swap="innerHTML" class="inline-form m-0">
|
||||
<input type="hidden" name="to" value="{{ h.i }}" />
|
||||
<button type="submit" class="btn">Go</button>
|
||||
</form>
|
||||
|
|
@ -327,13 +327,13 @@
|
|||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
<h4 style="margin-top:1rem;">Cards added this stage</h4>
|
||||
<h4 class="mt-4">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>
|
||||
<div class="muted my-2">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 class="muted text-xs my-1 flex gap-3 items-center flex-wrap">
|
||||
<span><span class="ownership-badge">✔</span> Owned</span>
|
||||
<span><span class="ownership-badge">✖</span> Not owned</span>
|
||||
</div>
|
||||
|
||||
{% if stage_label and stage_label.startswith('Creatures') %}
|
||||
|
|
@ -348,7 +348,7 @@
|
|||
{% 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>
|
||||
<h5 class="my-2">{{ heading }}</h5>
|
||||
<span class="count">(<span data-count>{{ g.list|length }}</span>)</span>
|
||||
<button type="button" class="toggle" title="Collapse/Expand">Toggle</button>
|
||||
</div>
|
||||
|
|
@ -360,13 +360,13 @@
|
|||
data-card-name="{{ c.name }}" data-role="{{ c.role or c.sub_role or '' }}" data-tags="{{ (c.tags|join(', ')) if c.tags else '' }}" data-tags-slug="{{ (c.tags_slug|join(', ')) if c.tags_slug else '' }}" data-owned="{{ '1' if owned else '0' }}"{% if c.reason %} data-reasons="{{ c.reason|e }}"{% endif %}
|
||||
data-must-include="{{ '1' if c.must_include else '0' }}" data-must-exclude="{{ '1' if c.must_exclude else '0' }}">
|
||||
<div class="img-btn" role="button" tabindex="0" title="Tap or click to view {{ c.name }}" aria-label="View {{ c.name }} details">
|
||||
<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"
|
||||
<img class="card-thumb" src="{{ c.name|card_image('normal') }}" alt="{{ c.name }} image" width="160" data-card-name="{{ c.name }}" loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="{{ c.name|card_image('small') }} 160w, {{ c.name|card_image('normal') }} 488w"
|
||||
sizes="160px" />
|
||||
</div>
|
||||
<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;">
|
||||
<div class="lock-box" id="lock-{{ group_idx }}-{{ loop.index0 }}" class="flex justify-center gap-1 mt-1">
|
||||
{% from 'partials/_macros.html' import lock_button %}
|
||||
{{ lock_button(c.name, is_locked) }}
|
||||
</div>
|
||||
|
|
@ -377,7 +377,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% if c.reason %}
|
||||
<div style="display:flex; justify-content:center; margin-top:.25rem; gap:.35rem; flex-wrap:wrap;">
|
||||
<div class="card-actions-center">
|
||||
<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"
|
||||
data-hx-cache="1" data-hx-cache-key="alts:{{ c.name|lower }}" data-hx-cache-ttl="20000"
|
||||
|
|
@ -385,13 +385,13 @@
|
|||
</div>
|
||||
<div class="reason" role="region" aria-label="Reason">{{ c.reason }}</div>
|
||||
{% else %}
|
||||
<div style="display:flex; justify-content:center; margin-top:.25rem;">
|
||||
<div class="flex justify-center mt-1">
|
||||
<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"
|
||||
data-hx-cache="1" data-hx-cache-key="alts:{{ c.name|lower }}" data-hx-cache-ttl="20000"
|
||||
data-hx-prefetch="/build/alternatives?name={{ c.name|urlencode }}">Alternatives</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="alts-{{ group_idx }}-{{ loop.index0 }}" class="alts" style="margin-top:.25rem;"></div>
|
||||
<div id="alts-{{ group_idx }}-{{ loop.index0 }}" class="alts mt-1"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
|
@ -406,13 +406,13 @@
|
|||
data-card-name="{{ c.name }}" data-role="{{ c.role or c.sub_role or '' }}" data-tags="{{ (c.tags|join(', ')) if c.tags else '' }}" data-tags-slug="{{ (c.tags_slug|join(', ')) if c.tags_slug else '' }}" data-owned="{{ '1' if owned else '0' }}"{% if c.reason %} data-reasons="{{ c.reason|e }}"{% endif %}
|
||||
data-must-include="{{ '1' if c.must_include else '0' }}" data-must-exclude="{{ '1' if c.must_exclude else '0' }}">
|
||||
<div class="img-btn" role="button" tabindex="0" title="Tap or click to view {{ c.name }}" aria-label="View {{ c.name }} details">
|
||||
<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"
|
||||
<img class="card-thumb" src="{{ c.name|card_image('normal') }}" alt="{{ c.name }} image" width="160" data-card-name="{{ c.name }}" loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="{{ c.name|card_image('small') }} 160w, {{ c.name|card_image('normal') }} 488w"
|
||||
sizes="160px" />
|
||||
</div>
|
||||
<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;">
|
||||
<div class="lock-box" id="lock-{{ loop.index0 }}" class="flex justify-center gap-1 mt-1">
|
||||
{% from 'partials/_macros.html' import lock_button %}
|
||||
{{ lock_button(c.name, is_locked) }}
|
||||
</div>
|
||||
|
|
@ -423,7 +423,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% if c.reason %}
|
||||
<div style="display:flex; justify-content:center; margin-top:.25rem; gap:.35rem; flex-wrap:wrap;">
|
||||
<div class="card-actions-center">
|
||||
<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"
|
||||
data-hx-cache="1" data-hx-cache-key="alts:{{ c.name|lower }}" data-hx-cache-ttl="20000"
|
||||
|
|
@ -431,31 +431,31 @@
|
|||
</div>
|
||||
<div class="reason" role="region" aria-label="Reason">{{ c.reason }}</div>
|
||||
{% else %}
|
||||
<div style="display:flex; justify-content:center; margin-top:.25rem;">
|
||||
<div class="flex justify-center mt-1">
|
||||
<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"
|
||||
data-hx-cache="1" data-hx-cache-key="alts:{{ c.name|lower }}" data-hx-cache-ttl="20000"
|
||||
data-hx-prefetch="/build/alternatives?name={{ c.name|urlencode }}">Alternatives</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="alts-{{ loop.index0 }}" class="alts" style="margin-top:.25rem;"></div>
|
||||
<div id="alts-{{ loop.index0 }}" class="alts mt-1"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if allow_must_haves and show_must_have_buttons %}
|
||||
<div class="muted" style="font-size:12px; margin:.35rem 0 .25rem 0;">Tip: Use the 🔒 Lock button to preserve the current copy in the deck. “Must include” will try to pull the card back in on future reruns, while “Must exclude” blocks the engine from selecting it again. Tap or click the card art to view details without changing the lock state.</div>
|
||||
<div class="muted text-xs my-1">Tip: Use the 🔒 Lock button to preserve the current copy in the deck. "Must include" will try to pull the card back in on future reruns, while "Must exclude" blocks the engine from selecting it again. Tap or click the card art to view details without changing the lock state.</div>
|
||||
{% else %}
|
||||
<div class="muted" style="font-size:12px; margin:.35rem 0 .25rem 0;">Tip: Use the 🔒 Lock button under each card to keep it across reruns. Tap or click the card art to view details without changing the lock state.</div>
|
||||
<div class="muted text-xs my-1">Tip: Use the 🔒 Lock button under each card to keep it across reruns. Tap or click the card art to view details without changing the lock state.</div>
|
||||
{% endif %}
|
||||
<div data-empty hidden role="status" aria-live="polite" class="muted" style="margin:.5rem 0 0;">
|
||||
<div data-empty hidden role="status" aria-live="polite" class="muted mt-2">
|
||||
No cards match your filters.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_logs and log %}
|
||||
<details style="margin-top:1rem;">
|
||||
<details class="mt-4">
|
||||
<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>
|
||||
<pre class="build-log">{{ log }}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -469,7 +469,7 @@
|
|||
hx-get="/build/step5/summary?token={{ summary_token }}"
|
||||
hx-trigger="load once, step5:refresh from:body"
|
||||
hx-swap="outerHTML">
|
||||
<div class="muted" style="margin-top:1rem;">
|
||||
<div class="muted mt-4">
|
||||
{% if summary_ready %}Loading deck summary…{% else %}Deck summary will appear after the build completes.{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue