mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-18 00:20:13 +01:00
feat(web): Core Refactor Phase A — extract sampling and cache modules; add adaptive TTL + eviction heuristics, Redis PoC, and metrics wiring. Tests added for TTL, eviction, exports, splash-adaptive, card index, and service worker. Docs+roadmap updated.
This commit is contained in:
parent
c4a7fc48ea
commit
a029d430c5
49 changed files with 3889 additions and 701 deletions
|
|
@ -328,7 +328,15 @@
|
|||
}
|
||||
var cardPop = ensureCard();
|
||||
var PREVIEW_VERSIONS = ['normal','large'];
|
||||
function normalizeCardName(raw){
|
||||
if(!raw) return raw;
|
||||
// Strip ' - Synergy (...' annotation if present
|
||||
var m = /(.*?)(\s*-\s*Synergy\s*\(.*\))$/i.exec(raw);
|
||||
if(m){ return m[1].trim(); }
|
||||
return raw;
|
||||
}
|
||||
function buildCardUrl(name, version, nocache, face){
|
||||
name = normalizeCardName(name);
|
||||
var q = encodeURIComponent(name||'');
|
||||
var url = 'https://api.scryfall.com/cards/named?fuzzy=' + q + '&format=image&version=' + (version||'normal');
|
||||
if (face === 'back') url += '&face=back';
|
||||
|
|
@ -337,6 +345,7 @@
|
|||
}
|
||||
// Generic Scryfall image URL builder
|
||||
function buildScryfallImageUrl(name, version, nocache){
|
||||
name = normalizeCardName(name);
|
||||
var q = encodeURIComponent(name||'');
|
||||
var url = 'https://api.scryfall.com/cards/named?fuzzy=' + q + '&format=image&version=' + (version||'normal');
|
||||
if (nocache) url += '&t=' + Date.now();
|
||||
|
|
@ -519,11 +528,11 @@
|
|||
var lastFlip = 0;
|
||||
function hasTwoFaces(card){
|
||||
if(!card) return false;
|
||||
var name = (card.getAttribute('data-card-name')||'') + ' ' + (card.getAttribute('data-original-name')||'');
|
||||
var name = normalizeCardName((card.getAttribute('data-card-name')||'')) + ' ' + normalizeCardName((card.getAttribute('data-original-name')||''));
|
||||
return name.indexOf('//') > -1;
|
||||
}
|
||||
function keyFor(card){
|
||||
var nm = (card.getAttribute('data-card-name')|| card.getAttribute('data-original-name')||'').toLowerCase();
|
||||
var nm = normalizeCardName(card.getAttribute('data-card-name')|| card.getAttribute('data-original-name')||'').toLowerCase();
|
||||
return LS_PREFIX + nm;
|
||||
}
|
||||
function applyStoredFace(card){
|
||||
|
|
@ -543,7 +552,7 @@
|
|||
live.id = 'dfc-live'; live.className='sr-only'; live.setAttribute('aria-live','polite');
|
||||
document.body.appendChild(live);
|
||||
}
|
||||
var nm = (card.getAttribute('data-card-name')||'').split('//')[0].trim();
|
||||
var nm = normalizeCardName(card.getAttribute('data-card-name')||'').split('//')[0].trim();
|
||||
live.textContent = 'Showing ' + (face==='front'?'front face':'back face') + ' of ' + nm;
|
||||
}
|
||||
function updateButton(btn, face){
|
||||
|
|
@ -714,8 +723,24 @@
|
|||
(function(){
|
||||
try{
|
||||
if ('serviceWorker' in navigator){
|
||||
navigator.serviceWorker.register('/static/sw.js').then(function(reg){
|
||||
window.__pwaStatus = { registered: true, scope: reg.scope };
|
||||
var ver = '{{ catalog_hash|default("dev") }}';
|
||||
var url = '/static/sw.js?v=' + encodeURIComponent(ver);
|
||||
navigator.serviceWorker.register(url).then(function(reg){
|
||||
window.__pwaStatus = { registered: true, scope: reg.scope, version: ver };
|
||||
// Listen for updates (new worker installing)
|
||||
if(reg.waiting){ reg.waiting.postMessage({ type: 'SKIP_WAITING' }); }
|
||||
reg.addEventListener('updatefound', function(){
|
||||
try {
|
||||
var nw = reg.installing; if(!nw) return;
|
||||
nw.addEventListener('statechange', function(){
|
||||
if(nw.state === 'installed' && navigator.serviceWorker.controller){
|
||||
// New version available; reload silently for freshness
|
||||
try { sessionStorage.setItem('mtg:swUpdated','1'); }catch(_){ }
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}catch(_){ }
|
||||
});
|
||||
}).catch(function(){ window.__pwaStatus = { registered: false }; });
|
||||
}
|
||||
}catch(_){ }
|
||||
|
|
|
|||
|
|
@ -74,8 +74,10 @@
|
|||
{% if inspect and inspect.ok %}
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview card-sm" data-card-name="{{ selected }}">
|
||||
<a href="https://scryfall.com/search?q={{ selected|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ selected|urlencode }}&format=image&version=normal" alt="{{ selected }} card image" data-card-name="{{ selected }}" />
|
||||
{# 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 }}" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow">
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
{# Step phases removed #}
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview" data-card-name="{{ commander.name }}">
|
||||
<a href="https://scryfall.com/search?q={{ commander.name|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander.name|urlencode }}&format=image&version=normal" alt="{{ commander.name }} card image" data-card-name="{{ commander.name }}" />
|
||||
{# 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 }}" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
{# Step phases removed #}
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview" data-card-name="{{ commander|urlencode }}">
|
||||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" data-card-name="{{ commander }}" />
|
||||
{# 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 }}" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
{# Step phases removed #}
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview" data-card-name="{{ commander|urlencode }}">
|
||||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" data-card-name="{{ commander }}" />
|
||||
{# 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 }}" />
|
||||
</a>
|
||||
</aside>
|
||||
<div class="grow" data-skeleton>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
{# Step phases removed #}
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview">
|
||||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" data-card-name="{{ commander }}" loading="lazy" decoding="async" data-lqip="1"
|
||||
srcset="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=small 160w, https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal 488w, https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=large 672w"
|
||||
{# Strip synergy annotation for Scryfall search #}
|
||||
<a href="https://scryfall.com/search?q={{ (commander.split(' - Synergy (')[0] if ' - Synergy (' in commander else commander)|urlencode }}" target="_blank" rel="noopener">
|
||||
{% set commander_base = (commander.split(' - Synergy (')[0] if ' - Synergy (' in commander else commander) %}
|
||||
<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 }}" 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"
|
||||
sizes="(max-width: 900px) 100vw, 320px" />
|
||||
</a>
|
||||
{% if status and status.startswith('Build complete') %}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@
|
|||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview">
|
||||
{% if commander %}
|
||||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" width="320" data-card-name="{{ commander }}" />
|
||||
{# 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" width="320" data-card-name="{{ commander_base }}" />
|
||||
</a>
|
||||
{% endif %}
|
||||
<div style="margin-top:.75rem; display:flex; gap:.35rem; flex-wrap:wrap;">
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@
|
|||
<div class="two-col two-col-left-rail" style="margin-top:.75rem;">
|
||||
<aside class="card-preview">
|
||||
{% if commander %}
|
||||
<a href="https://scryfall.com/search?q={{ commander|urlencode }}" target="_blank" rel="noopener">
|
||||
<img src="https://api.scryfall.com/cards/named?fuzzy={{ commander|urlencode }}&format=image&version=normal" alt="{{ commander }} card image" data-card-name="{{ commander }}" width="320" />
|
||||
{# 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 }}" width="320" />
|
||||
</a>
|
||||
<div class="muted" style="margin-top:.25rem;">Commander: <span data-card-name="{{ commander }}">{{ commander }}</span></div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -45,9 +45,10 @@
|
|||
<div class="example-card-grid" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:.85rem;">
|
||||
{% if theme.example_cards %}
|
||||
{% for c in theme.example_cards %}
|
||||
<div class="ex-card card-sample" style="text-align:center;" data-card-name="{{ c }}" data-role="example_card" data-tags="{{ theme.synergies|join(', ') }}">
|
||||
<img class="card-thumb" loading="lazy" decoding="async" alt="{{ c }} image" style="width:100%; height:auto; border:1px solid var(--border); border-radius:10px;" src="https://api.scryfall.com/cards/named?fuzzy={{ c|urlencode }}&format=image&version=small" />
|
||||
<div style="font-size:11px; margin-top:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-weight:600;" class="card-ref" data-card-name="{{ c }}" data-tags="{{ theme.synergies|join(', ') }}">{{ c }}</div>
|
||||
{% set base_c = (c.split(' - Synergy (')[0] if ' - Synergy (' in c else c) %}
|
||||
<div class="ex-card card-sample" style="text-align:center;" data-card-name="{{ base_c }}" data-role="example_card" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">
|
||||
<img class="card-thumb" loading="lazy" decoding="async" alt="{{ c }} image" style="width:100%; height:auto; border:1px solid var(--border); border-radius:10px;" src="https://api.scryfall.com/cards/named?fuzzy={{ base_c|urlencode }}&format=image&version=small" />
|
||||
<div style="font-size:11px; margin-top:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-weight:600;" class="card-ref" data-card-name="{{ base_c }}" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">{{ c }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
|
@ -58,9 +59,10 @@
|
|||
<div class="example-commander-grid" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:.85rem;">
|
||||
{% if theme.example_commanders %}
|
||||
{% for c in theme.example_commanders %}
|
||||
<div class="ex-commander commander-cell" style="text-align:center;" data-card-name="{{ c }}" data-role="commander_example" data-tags="{{ theme.synergies|join(', ') }}">
|
||||
<img class="card-thumb" loading="lazy" decoding="async" alt="{{ c }} image" style="width:100%; height:auto; border:1px solid var(--border); border-radius:10px;" src="https://api.scryfall.com/cards/named?fuzzy={{ c|urlencode }}&format=image&version=small" />
|
||||
<div style="font-size:11px; margin-top:4px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" class="card-ref" data-card-name="{{ c }}" data-tags="{{ theme.synergies|join(', ') }}">{{ c }}</div>
|
||||
{% set base_c = (c.split(' - Synergy (')[0] if ' - Synergy (' in c else c) %}
|
||||
<div class="ex-commander commander-cell" style="text-align:center;" data-card-name="{{ base_c }}" data-role="commander_example" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">
|
||||
<img class="card-thumb" loading="lazy" decoding="async" alt="{{ c }} image" style="width:100%; height:auto; border:1px solid var(--border); border-radius:10px;" src="https://api.scryfall.com/cards/named?fuzzy={{ base_c|urlencode }}&format=image&version=small" />
|
||||
<div style="font-size:11px; margin-top:4px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" class="card-ref" data-card-name="{{ base_c }}" data-tags="{{ theme.synergies|join(', ') }}" data-original-name="{{ c }}">{{ c }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
|
@ -81,4 +83,25 @@
|
|||
(function(){
|
||||
try { var h=document.getElementById('theme-detail-heading-{{ theme.id }}'); if(h){ h.focus({preventScroll:false}); } } catch(_e){}
|
||||
})();
|
||||
// Post-render normalization: ensure any annotated ' - Synergy (...)' names use base name for Scryfall URLs
|
||||
(function(){
|
||||
try {
|
||||
document.querySelectorAll('.example-card-grid img.card-thumb, .example-commander-grid img.card-thumb').forEach(function(img){
|
||||
var orig = img.getAttribute('data-original-name') || img.getAttribute('data-card-name') || '';
|
||||
var m = /(.*?)(\s*-\s*Synergy\s*\(.*\))$/i.exec(orig);
|
||||
if(m){
|
||||
var base = m[1].trim();
|
||||
if(base){
|
||||
img.setAttribute('data-card-name', base);
|
||||
var current = img.getAttribute('src')||'';
|
||||
// Replace fuzzy param only if it still contains the annotated portion
|
||||
var before = decodeURIComponent((current.split('fuzzy=')[1]||'').split('&')[0] || '');
|
||||
if(before && before !== base){
|
||||
img.src = 'https://api.scryfall.com/cards/named?fuzzy=' + encodeURIComponent(base) + '&format=image&version=small';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch(_){ }
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<div class="preview-controls" style="display:flex; gap:1rem; align-items:center; margin:.5rem 0 .75rem; font-size:11px;">
|
||||
<label style="display:inline-flex; gap:4px; align-items:center;"><input type="checkbox" id="curated-only-toggle"/> Curated Only</label>
|
||||
<label style="display:inline-flex; gap:4px; align-items:center;"><input type="checkbox" id="reasons-toggle" checked/> Reasons <span style="opacity:.55; font-size:10px; cursor:help;" title="Toggle why the payoff is included (i.e. overlapping themes or other reasoning)">?</span></label>
|
||||
<label style="display:inline-flex; gap:4px; align-items:center;"><input type="checkbox" id="show-duplicates-toggle"/> Show Collapsed Duplicates</label>
|
||||
<span id="preview-status" aria-live="polite" style="opacity:.65;"></span>
|
||||
</div>
|
||||
<details id="preview-rationale" class="preview-rationale" style="margin:.25rem 0 .85rem; font-size:11px; background:var(--panel-alt); border:1px solid var(--border); padding:.55rem .7rem; border-radius:8px;">
|
||||
|
|
@ -18,7 +19,17 @@
|
|||
<span id="hover-compact-indicator" style="font-size:10px; opacity:.7;">Mode: <span data-mode>normal</span></span>
|
||||
</div>
|
||||
<ul id="rationale-points" style="margin:.5rem 0 0 .9rem; padding:0; list-style:disc; line-height:1.35;">
|
||||
<li>Computing…</li>
|
||||
{% if preview.commander_rationale and preview.commander_rationale|length > 0 %}
|
||||
{% for r in preview.commander_rationale %}
|
||||
<li>
|
||||
<strong>{{ r.label }}</strong>: {{ r.value }}
|
||||
{% if r.detail %}<span style="opacity:.75;">({{ r.detail|join(', ') }})</span>{% endif %}
|
||||
{% if r.instances %}<span style="opacity:.65;"> ({{ r.instances }} instances)</span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li>Computing…</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
|
@ -27,9 +38,10 @@
|
|||
<div class="col-left">
|
||||
{% if not minimal %}{% if not suppress_curated %}<h4 style="margin:.25rem 0 .5rem; font-size:13px; letter-spacing:.05em; text-transform:uppercase; opacity:.8;">Example Cards</h4>{% else %}<h4 style="margin:.25rem 0 .5rem; font-size:13px; letter-spacing:.05em; text-transform:uppercase; opacity:.8;">Sampled Synergy Cards</h4>{% endif %}{% endif %}
|
||||
<hr style="border:0; border-top:1px solid var(--border); margin:.35rem 0 .6rem;" />
|
||||
<div class="cards-flow" style="display:flex; flex-wrap:wrap; gap:10px;" data-synergies="{{ preview.synergies_used|join(',') if preview.synergies_used }}">
|
||||
<div class="cards-flow" style="display:flex; flex-wrap:wrap; gap:10px;" data-synergies="{{ preview.synergies_used|join(',') if preview.synergies_used }}" data-pin-scope="{{ preview.theme_id }}">
|
||||
{% set inserted = {'examples': False, 'curated_synergy': False, 'payoff': False, 'enabler_support': False, 'wildcard': False} %}
|
||||
{% for c in preview.sample if (not suppress_curated and ('example' in c.roles or 'curated_synergy' in c.roles)) or 'payoff' in c.roles or 'enabler' in c.roles or 'support' in c.roles or 'wildcard' in c.roles %}
|
||||
{% if c.dup_collapsed %}{% set dup_class = ' is-collapsed-duplicate' %}{% else %}{% set dup_class = '' %}{% endif %}
|
||||
{% set primary = c.roles[0] if c.roles else '' %}
|
||||
{% if (not suppress_curated) and 'example' in c.roles and not inserted.examples %}<div class="group-separator" data-group="examples" style="flex-basis:100%; font-size:10px; text-transform:uppercase; letter-spacing:.05em; opacity:.65; margin-top:.25rem;">Curated Examples</div>{% set _ = inserted.update({'examples': True}) %}{% endif %}
|
||||
{% if (not suppress_curated) and primary == 'curated_synergy' and not inserted.curated_synergy %}<div class="group-separator" data-group="curated_synergy" style="flex-basis:100%; font-size:10px; text-transform:uppercase; letter-spacing:.05em; opacity:.65; margin-top:.5rem;">Curated Synergy</div>{% set _ = inserted.update({'curated_synergy': True}) %}{% endif %}
|
||||
|
|
@ -40,11 +52,13 @@
|
|||
{% if preview.synergies_used and c.tags %}
|
||||
{% for tg in c.tags %}{% if tg in preview.synergies_used %}{% set _ = overlaps.append(tg) %}{% endif %}{% endfor %}
|
||||
{% endif %}
|
||||
<div class="card-sample{% if overlaps %} has-overlap{% endif %}" style="width:230px;" data-card-name="{{ c.name }}" data-role="{{ c.roles[0] if c.roles }}" data-reasons="{{ c.reasons|join('; ') if c.reasons }}" data-tags="{{ c.tags|join(', ') if c.tags }}" data-overlaps="{{ overlaps|join(',') }}" data-mana="{{ c.mana_cost if c.mana_cost }}" data-rarity="{{ c.rarity if c.rarity }}">
|
||||
<div class="card-sample{{ dup_class }}{% if overlaps %} has-overlap{% endif %}" style="width:230px;" data-card-name="{{ c.name }}" data-role="{{ c.roles[0] if c.roles }}" data-reasons="{{ c.reasons|join('; ') if c.reasons }}" data-tags="{{ c.tags|join(', ') if c.tags }}" data-overlaps="{{ overlaps|join(',') }}" data-mana="{{ c.mana_cost if c.mana_cost }}" data-rarity="{{ c.rarity if c.rarity }}" {% if c.dup_group_size %}data-dup-group-size="{{ c.dup_group_size }}"{% endif %} {% if c.dup_anchor %}data-dup-anchor="1"{% endif %} {% if c.dup_collapsed %}data-dup-collapsed="1" data-dup-anchor-name="{{ c.dup_anchor_name }}"{% endif %}>
|
||||
<div class="thumb-wrap" style="position:relative;">
|
||||
<img class="card-thumb" width="230" loading="lazy" decoding="async" src="https://api.scryfall.com/cards/named?fuzzy={{ c.name|urlencode }}&format=image&version=small" alt="{{ c.name }} image" data-card-name="{{ c.name }}" data-role="{{ c.roles[0] if c.roles }}" data-tags="{{ c.tags|join(', ') if c.tags }}" {% if overlaps %}data-overlaps="{{ overlaps|join(',') }}"{% endif %} data-placeholder-color="#0b0d12" style="filter:blur(4px); transition:filter .35s ease; background:linear-gradient(145deg,#0b0d12,#111b29);" onload="this.style.filter='blur(0)';" />
|
||||
<span class="role-chip role-{{ c.roles[0] if c.roles }}" title="Primary role: {{ c.roles[0] if c.roles }}">{{ c.roles[0][0]|upper if c.roles }}</span>
|
||||
{% if overlaps %}<span class="overlap-badge" title="Synergy overlaps: {{ overlaps|join(', ') }}">{{ overlaps|length }}</span>{% endif %}
|
||||
{% if c.dup_anchor and c.dup_group_size and c.dup_group_size > 1 %}<span class="dup-badge" title="{{ c.dup_group_size - 1 }} similar cards collapsed" style="position:absolute; bottom:4px; right:4px; background:#4b5563; color:#fff; font-size:10px; padding:2px 5px; border-radius:10px;">+{{ c.dup_group_size - 1 }}</span>{% endif %}
|
||||
<button type="button" class="pin-btn" aria-label="Pin card" title="Pin card" data-pin-btn style="position:absolute; top:4px; right:4px; background:rgba(0,0,0,0.55); color:#fff; border:1px solid var(--border); border-radius:6px; font-size:10px; padding:2px 5px; cursor:pointer;">☆</button>
|
||||
</div>
|
||||
<div class="meta" style="font-size:12px; margin-top:2px;">
|
||||
<div class="ci-ribbon" aria-label="Color identity" style="display:flex; gap:2px; margin-bottom:2px; min-height:10px;"></div>
|
||||
|
|
@ -187,6 +201,11 @@
|
|||
.theme-preview-expanded .rarity-mythic { color:#fb923c; }
|
||||
@media (max-width: 950px){ .theme-preview-expanded .two-col { grid-template-columns: 1fr; } .theme-preview-expanded .col-right { order:-1; } }
|
||||
</style>
|
||||
<style>
|
||||
.card-sample.pinned { outline:2px solid var(--accent); outline-offset:2px; }
|
||||
.card-sample .pin-btn.active { background:var(--accent); color:#000; }
|
||||
.card-sample.is-collapsed-duplicate { display:none; }
|
||||
</style>
|
||||
<script>
|
||||
// sessionStorage preview fragment cache (keyed by theme + limit + commander). Stores HTML + ETag.
|
||||
(function(){ if(document.querySelector('.theme-preview-expanded.minimal-variant')) return;
|
||||
|
|
@ -201,6 +220,68 @@
|
|||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Collapsed duplicate toggle logic (persist in localStorage global scope)
|
||||
(function(){
|
||||
try {
|
||||
var toggle = document.getElementById('show-duplicates-toggle');
|
||||
if(!toggle) return;
|
||||
var STORE_KEY = 'preview.showCollapsedDuplicates';
|
||||
function apply(){
|
||||
var show = !!toggle.checked;
|
||||
document.querySelectorAll('.card-sample.is-collapsed-duplicate').forEach(function(el){
|
||||
el.style.display = show ? '' : 'none';
|
||||
});
|
||||
}
|
||||
var saved = localStorage.getItem(STORE_KEY);
|
||||
if(saved === '1'){ toggle.checked = true; }
|
||||
apply();
|
||||
toggle.addEventListener('change', function(){
|
||||
localStorage.setItem(STORE_KEY, toggle.checked ? '1':'0');
|
||||
apply();
|
||||
});
|
||||
} catch(_){}
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Client-side pin/unpin personalized examples (localStorage scoped by theme_id)
|
||||
(function(){
|
||||
try {
|
||||
var root = document.querySelector('.cards-flow[data-pin-scope]');
|
||||
if(!root) return;
|
||||
var scope = root.getAttribute('data-pin-scope');
|
||||
var storeKey = 'preview.pins.'+scope;
|
||||
function loadPins(){
|
||||
try { return JSON.parse(localStorage.getItem(storeKey) || '[]'); } catch(_) { return []; }
|
||||
}
|
||||
function savePins(pins){ try { localStorage.setItem(storeKey, JSON.stringify(pins.slice(0,100))); } catch(_){} }
|
||||
function setState(){
|
||||
var pins = loadPins();
|
||||
var cards = root.querySelectorAll('.card-sample');
|
||||
cards.forEach(function(cs){
|
||||
var name = cs.getAttribute('data-card-name');
|
||||
var btn = cs.querySelector('[data-pin-btn]');
|
||||
var pinned = pins.indexOf(name) !== -1;
|
||||
cs.classList.toggle('pinned', pinned);
|
||||
if(btn){ btn.classList.toggle('active', pinned); btn.textContent = pinned ? '★' : '☆'; btn.setAttribute('aria-label', pinned ? 'Unpin card' : 'Pin card'); }
|
||||
});
|
||||
}
|
||||
root.addEventListener('click', function(e){
|
||||
var btn = e.target.closest('[data-pin-btn]');
|
||||
if(!btn) return;
|
||||
var card = btn.closest('.card-sample');
|
||||
if(!card) return;
|
||||
var name = card.getAttribute('data-card-name');
|
||||
var pins = loadPins();
|
||||
var idx = pins.indexOf(name);
|
||||
if(idx === -1) pins.push(name); else pins.splice(idx,1);
|
||||
savePins(pins);
|
||||
setState();
|
||||
});
|
||||
setState();
|
||||
} catch(_){ }
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Lazy-load fallback for browsers ignoring loading=lazy (very old) + intersection observer prefetch enhancement
|
||||
(function(){
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue