mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-01-04 08:38:51 +01:00
feat: Added Partners, Backgrounds, and related variation selections to commander building.
This commit is contained in:
parent
641b305955
commit
d416c9b238
65 changed files with 11835 additions and 691 deletions
|
|
@ -662,7 +662,7 @@
|
|||
window.__dfcFlipCard = function(card){ if(!card) return; flip(card, card.querySelector('.dfc-toggle')); };
|
||||
window.__dfcGetFace = function(card){ if(!card) return 'front'; return card.getAttribute(FACE_ATTR) || 'front'; };
|
||||
function scan(){
|
||||
document.querySelectorAll('.card-sample, .commander-cell, .commander-thumb, .card-tile, .candidate-tile, .stack-card, .card-preview, .owned-row, .list-row').forEach(ensureButton);
|
||||
document.querySelectorAll('.card-sample, .commander-cell, .commander-thumb, .commander-card, .card-tile, .candidate-tile, .stack-card, .card-preview, .owned-row, .list-row').forEach(ensureButton);
|
||||
}
|
||||
document.addEventListener('pointermove', function(e){ window.__lastPointerEvent = e; }, { passive:true });
|
||||
document.addEventListener('DOMContentLoaded', scan);
|
||||
|
|
@ -1206,9 +1206,9 @@
|
|||
if(!el) return null;
|
||||
// If inside flip button
|
||||
var btn = el.closest && el.closest('.dfc-toggle');
|
||||
if(btn) return btn.closest('.card-sample, .commander-cell, .commander-thumb, .card-tile, .candidate-tile, .card-preview, .stack-card');
|
||||
if(btn) return btn.closest('.card-sample, .commander-cell, .commander-thumb, .commander-card, .card-tile, .candidate-tile, .card-preview, .stack-card');
|
||||
// Recognized container classes (add .stack-card for finished/random deck thumbnails)
|
||||
var container = el.closest && el.closest('.card-sample, .commander-cell, .commander-thumb, .card-tile, .candidate-tile, .card-preview, .stack-card');
|
||||
var container = el.closest && el.closest('.card-sample, .commander-cell, .commander-thumb, .commander-card, .card-tile, .candidate-tile, .card-preview, .stack-card');
|
||||
if(container) return container;
|
||||
// Image-based detection (any card image carrying data-card-name)
|
||||
if(el.matches && (el.matches('img.card-thumb') || el.matches('img[data-card-name]') || el.classList.contains('commander-img'))){
|
||||
|
|
@ -1264,7 +1264,7 @@
|
|||
window.hoverShowByName = function(name){
|
||||
try {
|
||||
var el = document.querySelector('[data-card-name="'+CSS.escape(name)+'"]');
|
||||
if(el){ window.__hoverShowCard(el.closest('.card-sample, .commander-cell, .commander-thumb, .card-tile, .candidate-tile, .card-preview, .stack-card') || el); }
|
||||
if(el){ window.__hoverShowCard(el.closest('.card-sample, .commander-cell, .commander-thumb, .commander-card, .card-tile, .candidate-tile, .card-preview, .stack-card') || el); }
|
||||
} catch(_) {}
|
||||
};
|
||||
// Keyboard accessibility & focus traversal (P2 UI Hover keyboard accessibility)
|
||||
|
|
|
|||
|
|
@ -32,12 +32,16 @@
|
|||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Themes</legend>
|
||||
<div id="newdeck-tags-slot" class="muted">
|
||||
<em>Select a commander to see theme recommendations and choices.</em>
|
||||
<input type="hidden" name="primary_tag" />
|
||||
<input type="hidden" name="secondary_tag" />
|
||||
<input type="hidden" name="tertiary_tag" />
|
||||
<input type="hidden" name="tag_mode" value="AND" />
|
||||
<div id="newdeck-tags-slot"{% if not tag_slot_html %} class="muted"{% endif %}>
|
||||
{% if tag_slot_html %}
|
||||
{{ tag_slot_html | safe }}
|
||||
{% else %}
|
||||
<em>Select a commander to see theme recommendations and choices.</em>
|
||||
<input type="hidden" name="primary_tag" />
|
||||
<input type="hidden" name="secondary_tag" />
|
||||
<input type="hidden" name="tertiary_tag" />
|
||||
<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>
|
||||
{% if enable_custom_themes %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,17 @@
|
|||
{% from 'partials/_macros.html' import color_identity %}
|
||||
{% set pname = commander.name %}
|
||||
{% set partner_preview_payload = partner_preview if partner_preview else None %}
|
||||
{% set preview_colors = partner_preview_payload.color_identity if partner_preview_payload else [] %}
|
||||
{% if preview_colors is none %}
|
||||
{% set preview_colors = [] %}
|
||||
{% endif %}
|
||||
{% set preview_label = partner_preview_payload.color_label if partner_preview_payload else '' %}
|
||||
{% if not preview_label and preview_colors %}
|
||||
{% set preview_label = preview_colors|join(' / ') %}
|
||||
{% endif %}
|
||||
{% if not preview_label and partner_preview_payload and (preview_colors|length == 0) %}
|
||||
{% set preview_label = 'Colorless (C)' %}
|
||||
{% endif %}
|
||||
<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">
|
||||
|
|
@ -6,6 +19,38 @@
|
|||
</a>
|
||||
</aside>
|
||||
<div class="muted" style="font-size:12px; margin-top:.25rem; max-width: 230px;">{{ 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' %}
|
||||
{% endif %}
|
||||
{% set partner_href = partner_preview_payload.secondary_scryfall_url or partner_preview_payload.scryfall_url %}
|
||||
{% if not partner_href and partner_secondary_name %}
|
||||
{% set partner_href = 'https://scryfall.com/search?q=' ~ partner_secondary_name|urlencode %}
|
||||
{% endif %}
|
||||
{% if partner_image_url %}
|
||||
<aside class="card-preview partner-card-preview" style="max-width: 230px; margin-top:.75rem;">
|
||||
{% if partner_href %}<a href="{{ partner_href }}" target="_blank" rel="noopener">{% endif %}
|
||||
<img src="{{ partner_image_url }}" alt="{{ (partner_secondary_name or 'Selected card') ~ ' card image' }}" data-card-name="{{ partner_secondary_name or '' }}" style="width:200px; height:auto; display:block; border-radius:6px;" loading="lazy" decoding="async" />
|
||||
{% if partner_href %}</a>{% endif %}
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% if partner_secondary_name %}
|
||||
<div class="muted" style="font-size:12px; margin-top:.25rem; max-width: 230px;">
|
||||
{% if partner_preview_payload.secondary_role_label %}<strong>{{ partner_preview_payload.secondary_role_label }}</strong>: {% endif %}{{ partner_secondary_name }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="muted" style="font-size:12px; margin-top:.35rem; display:flex; align-items:center; gap:.35rem; flex-wrap:wrap;">
|
||||
{{ color_identity(preview_colors, is_colorless=(preview_colors|length == 0), aria_label=preview_label or '', title_text=preview_label or '') }}
|
||||
<span>{{ preview_label }}</span>
|
||||
</div>
|
||||
{% if partner_preview_payload.theme_tags %}
|
||||
<div class="muted" style="font-size:12px; margin-top:.25rem;">
|
||||
Combined themes: {{ partner_preview_payload.theme_tags|join(', ') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<script>
|
||||
try {
|
||||
var nm = document.querySelector('input[name="name"]');
|
||||
|
|
@ -52,18 +97,18 @@
|
|||
<button type="button" id="modal-reset-tags" class="chip" style="margin-left:.35rem;">Reset themes</button>
|
||||
<span id="modal-tag-count" class="muted" style="font-size:12px;"></span>
|
||||
</div>
|
||||
{% if recommended and recommended|length %}
|
||||
<div style="display:flex; align-items:center; gap:.5rem; margin:.25rem 0 .35rem 0;">
|
||||
<div class="muted" style="font-size:12px;">Recommended</div>
|
||||
<div id="modal-tag-reco-block" data-has-reco="{% if recommended and recommended|length %}1{% else %}0{% endif %}" {% if not (recommended and recommended|length) %}style="display:none;"{% endif %}>
|
||||
<div class="muted" style="font-size:12px; margin:.25rem 0 .35rem 0;">Recommended</div>
|
||||
<div id="modal-tag-reco" aria-label="Recommended themes" data-original-tags='{{ (recommended or []) | tojson }}' style="display:flex; gap:.35rem; flex-wrap:wrap; margin-bottom:.5rem;">
|
||||
{% if recommended and recommended|length %}
|
||||
{% for r in recommended %}
|
||||
{% set tip = (recommended_reasons[r] if (recommended_reasons is defined and recommended_reasons and recommended_reasons.get(r)) else 'Recommended for this commander') %}
|
||||
<button type="button" class="chip chip-reco" data-tag="{{ r }}" title="{{ tip }}">★ {{ r }}</button>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<button type="button" id="modal-reco-select-all" class="chip" title="Add recommended up to 3" {% if not (recommended and recommended|length) %}style="display:none;"{% endif %}>Select all</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal-tag-reco" aria-label="Recommended themes" style="display:flex; gap:.35rem; flex-wrap:wrap; margin-bottom:.5rem;">
|
||||
{% for r in recommended %}
|
||||
{% set tip = (recommended_reasons[r] if (recommended_reasons is defined and recommended_reasons and recommended_reasons.get(r)) else 'Recommended for this commander') %}
|
||||
<button type="button" class="chip chip-reco" data-tag="{{ r }}" title="{{ tip }}">★ {{ r }}</button>
|
||||
{% endfor %}
|
||||
<button type="button" id="modal-reco-select-all" class="chip" title="Add recommended up to 3">Select all</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="modal-tag-list" aria-label="Available themes" style="display:flex; gap:.35rem; flex-wrap:wrap;">
|
||||
{% for t in tags %}
|
||||
<button type="button" class="chip" data-tag="{{ t }}">{{ t }}</button>
|
||||
|
|
@ -83,6 +128,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% set partner_id_prefix = 'modal' %}
|
||||
{% set partner_scope = 'modal' %}
|
||||
{% include "build/_partner_controls.html" %}
|
||||
|
||||
{# Always update the bracket dropdown on commander change; hide 1–2 only when gc_commander is true #}
|
||||
<div id="newdeck-bracket-slot" hx-swap-oob="true">
|
||||
<label>Bracket
|
||||
|
|
@ -102,6 +151,7 @@
|
|||
<script>
|
||||
(function(){
|
||||
var list = document.getElementById('modal-tag-list');
|
||||
var recoBlock = document.getElementById('modal-tag-reco-block');
|
||||
var reco = document.getElementById('modal-tag-reco');
|
||||
var selAll = document.getElementById('modal-reco-select-all');
|
||||
var resetBtn = document.getElementById('modal-reset-tags');
|
||||
|
|
@ -112,6 +162,18 @@
|
|||
var countEl = document.getElementById('modal-tag-count');
|
||||
var selSummary = document.getElementById('modal-selected-themes');
|
||||
if (!list) return;
|
||||
var previewScope = 'modal';
|
||||
function readPartnerPreviewTags(){
|
||||
if (typeof window === 'undefined') return [];
|
||||
var store = window.partnerPreviewState;
|
||||
if (!store) return [];
|
||||
var state = store[previewScope];
|
||||
if (!state) return [];
|
||||
if (Array.isArray(state.theme_tags) && state.theme_tags.length){ return state.theme_tags.slice(); }
|
||||
var payload = state.payload;
|
||||
if (payload && Array.isArray(payload.theme_tags)){ return payload.theme_tags.slice(); }
|
||||
return [];
|
||||
}
|
||||
|
||||
function getSel(){ var a=[]; if(p&&p.value)a.push(p.value); if(s&&s.value)a.push(s.value); if(t&&t.value)a.push(t.value); return a; }
|
||||
function setSel(a){ a = Array.from(new Set(a||[])).filter(Boolean).slice(0,3); if(p) p.value=a[0]||''; if(s) s.value=a[1]||''; if(t) t.value=a[2]||''; updateUI(); }
|
||||
|
|
@ -135,10 +197,78 @@
|
|||
try{ document.dispatchEvent(new CustomEvent('newdeck:tagsChanged')); }catch(_){ }
|
||||
}
|
||||
if (resetBtn) resetBtn.addEventListener('click', function(){ setSel([]); });
|
||||
list.querySelectorAll('button.chip').forEach(function(btn){ var tag=btn.dataset.tag||''; btn.addEventListener('click', function(){ toggle(tag); }); });
|
||||
if (reco){ reco.querySelectorAll('button.chip-reco').forEach(function(btn){ var tag=btn.dataset.tag||''; btn.addEventListener('click', function(){ toggle(tag); }); }); }
|
||||
if (selAll){ selAll.addEventListener('click', function(){ try{ var cur=getSel(); var recs = reco? Array.from(reco.querySelectorAll('button.chip-reco')).map(function(b){return b.dataset.tag||'';}).filter(Boolean):[]; var combined=cur.slice(); recs.forEach(function(x){ if(combined.indexOf(x)===-1) combined.push(x); }); setSel(combined.slice(-3)); }catch(_){} }); }
|
||||
list.addEventListener('click', function(e){
|
||||
var btn = e.target.closest('button.chip');
|
||||
if (!btn || !list.contains(btn)) return;
|
||||
var tag = btn.dataset.tag || '';
|
||||
if (tag){ toggle(tag); }
|
||||
});
|
||||
if (reco){
|
||||
reco.addEventListener('click', function(e){
|
||||
var btn = e.target.closest('button');
|
||||
if (!btn || !reco.contains(btn)) return;
|
||||
if (btn.id === 'modal-reco-select-all'){
|
||||
try {
|
||||
var cur = getSel();
|
||||
var recs = Array.from(reco.querySelectorAll('button.chip-reco')).map(function(b){ return b.dataset.tag || ''; }).filter(Boolean);
|
||||
var combined = cur.slice();
|
||||
recs.forEach(function(x){ if (combined.indexOf(x) === -1) combined.push(x); });
|
||||
setSel(combined.slice(-3));
|
||||
} catch(_){ }
|
||||
return;
|
||||
}
|
||||
if (btn.classList.contains('chip-reco')){
|
||||
var tag = btn.dataset.tag || '';
|
||||
if (tag){ toggle(tag); }
|
||||
}
|
||||
});
|
||||
}
|
||||
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 = [];
|
||||
var seen = new Set();
|
||||
(Array.isArray(tags) ? tags : []).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);
|
||||
});
|
||||
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); }
|
||||
});
|
||||
var hasAny = reco.querySelectorAll('button.chip-reco').length > 0;
|
||||
if (recoBlock){
|
||||
recoBlock.style.display = hasAny ? '' : 'none';
|
||||
recoBlock.setAttribute('data-has-reco', hasAny ? '1' : '0');
|
||||
}
|
||||
if (selAll){ selAll.style.display = hasAny ? '' : 'none'; }
|
||||
updateUI();
|
||||
}
|
||||
|
||||
document.addEventListener('partner:preview', function(evt){
|
||||
var detail = (evt && evt.detail) || {};
|
||||
if (detail.scope && detail.scope !== previewScope) return;
|
||||
var tags = Array.isArray(detail.theme_tags) && detail.theme_tags.length ? detail.theme_tags : [];
|
||||
if (!tags.length && detail.payload && Array.isArray(detail.payload.theme_tags)){
|
||||
tags = detail.payload.theme_tags;
|
||||
}
|
||||
updatePartnerRecommendations(tags);
|
||||
});
|
||||
|
||||
var initialPartnerTags = readPartnerPreviewTags();
|
||||
updatePartnerRecommendations(initialPartnerTags);
|
||||
updateUI();
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
959
code/web/templates/build/_partner_controls.html
Normal file
959
code/web/templates/build/_partner_controls.html
Normal file
|
|
@ -0,0 +1,959 @@
|
|||
{% set prefix = partner_id_prefix if partner_id_prefix is defined else 'partner' %}
|
||||
{% set feature_available = partner_feature_available if partner_feature_available is defined else False %}
|
||||
{% set partner_capable = partner_capable if partner_capable is defined else False %}
|
||||
{% set partner_options = partner_options if partner_options is defined else [] %}
|
||||
{% set background_options = background_options if background_options is defined else [] %}
|
||||
{% set partner_select_label = partner_select_label if partner_select_label is defined else 'Partner commander' %}
|
||||
{% set partner_select_placeholder = partner_select_placeholder if partner_select_placeholder is defined else 'Select a partner' %}
|
||||
{% set partner_auto_assigned = partner_auto_assigned if partner_auto_assigned is defined else False %}
|
||||
{% set partner_auto_opt_out = partner_auto_opt_out if partner_auto_opt_out is defined else False %}
|
||||
{% set partner_auto_default = partner_auto_default if partner_auto_default is defined else None %}
|
||||
{% set partner_prefill_available = partner_prefill_available if partner_prefill_available is defined else False %}
|
||||
{% set partner_note_id = prefix ~ '-partner-autonote' %}
|
||||
{% set partner_warning_id = prefix ~ '-partner-warnings' %}
|
||||
{% set partner_suggestions_enabled = partner_suggestions_enabled if partner_suggestions_enabled is defined else False %}
|
||||
{% set partner_suggestions = partner_suggestions if partner_suggestions is defined else [] %}
|
||||
{% set partner_suggestions_hidden = partner_suggestions_hidden if partner_suggestions_hidden is defined else [] %}
|
||||
{% set partner_suggestions_total = partner_suggestions_total if partner_suggestions_total is defined else 0 %}
|
||||
{% set partner_suggestions_metadata = partner_suggestions_metadata if partner_suggestions_metadata is defined else {} %}
|
||||
{% set partner_suggestions_loaded = partner_suggestions_loaded if partner_suggestions_loaded is defined else False %}
|
||||
{% set partner_suggestions_error = partner_suggestions_error if partner_suggestions_error is defined else None %}
|
||||
{% set partner_suggestions_available = partner_suggestions_available if partner_suggestions_available is defined else False %}
|
||||
{% set partner_suggestions_has_hidden = partner_suggestions_has_hidden if partner_suggestions_has_hidden is defined else False %}
|
||||
{% if feature_available %}
|
||||
<fieldset>
|
||||
<legend>Partner Mechanics</legend>
|
||||
{% if not partner_capable %}
|
||||
<p class="muted" style="font-size:12px;">This commander doesn't support partner mechanics or backgrounds.</p>
|
||||
{% else %}
|
||||
<input type="hidden" name="partner_enabled" value="{{ partner_hidden_value or '1' }}" />
|
||||
<input type="hidden" name="partner_auto_opt_out" value="{{ '1' if partner_auto_opt_out else '0' }}" data-partner-auto-opt="{{ prefix }}" />
|
||||
<input type="hidden" name="partner_selection_source" value="" data-partner-selection-source="{{ prefix }}" />
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.5rem;">Choose either a partner commander or a background—never both.</div>
|
||||
{% if partner_role_hint %}
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.35rem;">{{ partner_role_hint }}</div>
|
||||
{% endif %}
|
||||
{% if primary_partner_with %}
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.35rem;">
|
||||
Pairs naturally with <strong>{{ primary_partner_with|join(', ') }}</strong>.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if partner_options and partner_options|length and (not background_options or not background_options|length) %}
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.35rem;">No Backgrounds available for this commander.</div>
|
||||
{% elif background_options and background_options|length and (not partner_options or not partner_options|length) %}
|
||||
<div class="muted" style="font-size:12px; margin-bottom:.35rem;">This commander can't select a partner commander but can choose a Background.</div>
|
||||
{% endif %}
|
||||
{% if partner_error %}
|
||||
<div style="color:#a00; margin-bottom:.5rem; font-weight:600;">{{ partner_error }}</div>
|
||||
{% endif %}
|
||||
<div id="{{ partner_note_id }}" class="partner-autonote" data-partner-autonote="{{ prefix }}" data-autonote="{{ partner_auto_note or '' }}" style="color:#046d1f; margin-bottom:.5rem; font-size:12px;" role="status" aria-live="polite" aria-atomic="true" aria-hidden="{{ 'false' if partner_auto_note else 'true' }}" {% if not partner_auto_note %}hidden{% endif %}>
|
||||
<span class="sr-only">Partner pairing update:</span>
|
||||
<span data-partner-note-copy>{% if partner_auto_note %}{{ partner_auto_note }}{% endif %}</span>
|
||||
</div>
|
||||
{% if partner_prefill_available and partner_auto_default %}
|
||||
<div style="display:flex; align-items:center; gap:.5rem; margin-bottom:.5rem; flex-wrap:wrap;">
|
||||
<button type="button" class="chip{% if not partner_auto_opt_out %} active{% endif %}" data-partner-autotoggle="{{ prefix }}" data-partner-default="{{ partner_auto_default }}" aria-pressed="{% if not partner_auto_opt_out %}true{% else %}false{% endif %}" aria-describedby="{{ partner_note_id }}">
|
||||
{% if partner_auto_opt_out %}Enable default partner{% else %}Use default partner ({{ partner_auto_default }}){% endif %}
|
||||
</button>
|
||||
<span class="muted" style="font-size:12px;">Toggle to opt-out and choose a different partner.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if partner_suggestions_enabled %}
|
||||
<div class="partner-suggestions" data-partner-suggestions="{{ prefix }}" data-partner-scope="{{ partner_scope if partner_scope is defined else prefix }}" data-api-endpoint="{{ partner_suggestions_endpoint if partner_suggestions_endpoint is defined else '/api/partner/suggestions' }}" data-primary-name="{{ primary_commander_display }}" data-suggestions-json='{{ partner_suggestions | tojson }}' data-hidden-json='{{ partner_suggestions_hidden | tojson }}' data-total="{{ partner_suggestions_total }}" data-loaded="{{ '1' if partner_suggestions_loaded else '0' }}" data-error="{{ partner_suggestions_error or '' }}" data-has-hidden="{{ '1' if partner_suggestions_has_hidden else '0' }}" data-available="{{ '1' if partner_suggestions_available else '0' }}" data-metadata-json='{{ partner_suggestions_metadata | tojson }}' style="display:grid; gap:.35rem; margin-bottom:.75rem;">
|
||||
<div class="partner-suggestions__header" style="display:flex; justify-content:space-between; align-items:center; gap:.5rem; flex-wrap:wrap;">
|
||||
<span style="font-weight:600;">Suggested partners</span>
|
||||
<div class="partner-suggestions__controls" style="display:flex; gap:.35rem; flex-wrap:wrap;">
|
||||
<button type="button" class="chip" data-partner-suggestions-refresh="{{ prefix }}" aria-label="Refresh partner suggestions">Refresh</button>
|
||||
<button type="button" class="chip" data-partner-suggestions-more="{{ prefix }}" hidden>Show more</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="partner-suggestions__list" data-partner-suggestions-list style="display:flex; flex-wrap:wrap; gap:.35rem;"></div>
|
||||
<div class="partner-suggestions__meta muted" data-partner-suggestions-meta style="font-size:12px;"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="partner-controls" data-partner-controls="{{ prefix }}" data-partner-scope="{{ partner_scope if partner_scope is defined else prefix }}" data-primary-name="{{ primary_commander_display }}" style="display:grid; gap:.5rem; margin-bottom:.5rem;">
|
||||
{% if partner_options and partner_options|length %}
|
||||
<label style="display:grid; gap:.35rem;">
|
||||
<span>{{ partner_select_label }}</span>
|
||||
<select name="secondary_commander" id="{{ prefix }}-partner-secondary" data-partner-select="secondary" aria-describedby="{{ partner_note_id }} {{ partner_warning_id }}">
|
||||
<option value="">{{ partner_select_placeholder }}</option>
|
||||
{% for opt in partner_options %}
|
||||
{% set is_selected = (selected_secondary_commander|lower == opt.name|lower) %}
|
||||
<option value="{{ opt.name }}" data-pairing-mode="{{ opt.pairing_mode }}" data-mode-label="{{ opt.mode_label }}" data-color-label="{{ opt.color_label }}" data-color-code="{{ opt.color_code }}" data-image-url="{{ opt.image_url or '' }}" data-scryfall-url="{{ opt.scryfall_url or '' }}" data-role-label="{{ opt.role_label or 'Partner commander' }}" {% if is_selected %}selected{% endif %}>
|
||||
{{ opt.name }} — {{ opt.color_label }}
|
||||
{% if opt.pairing_mode == 'partner_with' %}(Partner With){% elif opt.pairing_mode == 'partner_restricted' and opt.restriction_label %} (Partner - {{ opt.restriction_label }}){% elif opt.pairing_mode == 'doctor_companion' and opt.role_label %} ({{ opt.role_label }}){% endif %}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if background_options and background_options|length %}
|
||||
<label style="display:grid; gap:.35rem;">
|
||||
<span>Background</span>
|
||||
<select name="background" id="{{ prefix }}-partner-background" data-partner-select="background" aria-describedby="{{ partner_note_id }} {{ partner_warning_id }}">
|
||||
<option value="">Select a background</option>
|
||||
{% for opt in background_options %}
|
||||
<option value="{{ opt.name }}" data-color-label="{{ opt.color_label }}" data-color-code="{{ opt.color_code if opt.color_code is defined else '' }}" data-image-url="{{ opt.image_url or '' }}" data-scryfall-url="{{ opt.scryfall_url or '' }}" data-role-label="{{ opt.role_label or 'Background' }}" {% if selected_background == opt.name %}selected{% endif %}>{{ opt.name }} — {{ opt.color_label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div style="display:flex; gap:.5rem; margin-bottom:.5rem; flex-wrap:wrap;">
|
||||
<button type="button" class="chip" data-partner-clear="{{ prefix }}">Clear selection</button>
|
||||
</div>
|
||||
<div class="partner-preview" data-partner-preview="{{ prefix }}" {% if partner_preview %}data-preview-json='{{ partner_preview | tojson }}'{% else %}hidden{% endif %}>
|
||||
{% 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' %}
|
||||
{% endif %}
|
||||
{% set preview_href = partner_preview.secondary_scryfall_url or partner_preview.scryfall_url %}
|
||||
{% if not preview_href and partner_preview.secondary_name %}
|
||||
{% set preview_href = 'https://scryfall.com/search?q=' ~ partner_preview.secondary_name|urlencode %}
|
||||
{% endif %}
|
||||
{% set preview_role = partner_preview.secondary_role_label or partner_preview.role_label %}
|
||||
{% set preview_primary = partner_preview.primary_name or primary_commander_display %}
|
||||
{% set preview_secondary = partner_preview.secondary_name %}
|
||||
{% set preview_themes = partner_preview.theme_tags %}
|
||||
{% set preview_mode_label = partner_preview.partner_mode_label %}
|
||||
{% set preview_color_label = partner_preview.color_label %}
|
||||
<div class="partner-preview__layout">
|
||||
{% if preview_image %}
|
||||
<div class="partner-preview__art">
|
||||
{% if preview_href %}<a href="{{ preview_href }}" target="_blank" rel="noopener">{% endif %}
|
||||
<img src="{{ preview_image }}" alt="{{ (preview_secondary or 'Selected card') ~ ' card image' }}" loading="lazy" decoding="async" />
|
||||
{% if preview_href %}</a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="partner-preview__details">
|
||||
<div class="partner-preview__header">{{ preview_mode_label }}{% if preview_color_label %} • {{ preview_color_label }}{% endif %}</div>
|
||||
{% if preview_role %}
|
||||
<div class="partner-preview__role">{{ preview_role }}</div>
|
||||
{% endif %}
|
||||
{% if preview_secondary %}
|
||||
<div class="partner-preview__pairing">Pairing: {{ preview_primary }}{% if preview_secondary %} + {{ preview_secondary }}{% endif %}</div>
|
||||
{% endif %}
|
||||
{% if preview_themes %}
|
||||
<div class="partner-preview__themes muted">Theme emphasis: {{ preview_themes|join(', ') }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="{{ partner_warning_id }}" data-partner-warnings="{{ prefix }}" data-warnings-json='{{ (partner_warnings or []) | tojson }}' style="background:#fff7e5; border:1px solid #f0c36d; border-radius:8px; padding:.75rem; font-size:12px; color:#7a4b02;" role="alert" aria-live="polite" aria-hidden="{{ 'false' if partner_warnings and partner_warnings|length else 'true' }}" {% if not (partner_warnings and partner_warnings|length) %}hidden{% endif %}>
|
||||
{% if partner_warnings and partner_warnings|length %}
|
||||
<strong>Warnings</strong>
|
||||
<ul style="margin:.35rem 0 0 1.1rem;">
|
||||
{% for warn in partner_warnings %}
|
||||
<li>{{ warn }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<script>
|
||||
(function(){
|
||||
var prefix = '{{ prefix }}';
|
||||
var controls = document.querySelector('[data-partner-controls="' + prefix + '"]');
|
||||
if (!controls || controls.dataset.partnerInit === '1') return;
|
||||
controls.dataset.partnerInit = '1';
|
||||
var scope = controls.getAttribute('data-partner-scope') || prefix;
|
||||
var selects = Array.prototype.slice.call(controls.querySelectorAll('[data-partner-select]'));
|
||||
var clearBtn = document.querySelector('[data-partner-clear="' + prefix + '"]');
|
||||
var optInput = document.querySelector('input[name="partner_auto_opt_out"][data-partner-auto-opt="' + prefix + '"]');
|
||||
var autoToggle = document.querySelector('[data-partner-autotoggle="' + prefix + '"]');
|
||||
var defaultPartner = autoToggle ? autoToggle.getAttribute('data-partner-default') : null;
|
||||
var previewBox = document.querySelector('[data-partner-preview="' + prefix + '"]');
|
||||
var warningsBox = document.querySelector('[data-partner-warnings="' + prefix + '"]');
|
||||
var autoNoteBox = document.querySelector('[data-partner-autonote="' + prefix + '"]');
|
||||
var autoNoteCopy = autoNoteBox ? autoNoteBox.querySelector('[data-partner-note-copy]') : null;
|
||||
var primaryName = controls.getAttribute('data-primary-name') || '';
|
||||
var fieldset = controls.closest('fieldset');
|
||||
var partnerEnabledInput = fieldset ? fieldset.querySelector('input[name="partner_enabled"]') : null;
|
||||
var selectionSourceInput = fieldset ? fieldset.querySelector('input[name="partner_selection_source"][data-partner-selection-source="' + prefix + '"]') : null;
|
||||
var initialAutoNote = autoNoteBox ? (autoNoteBox.getAttribute('data-autonote') || '') : '';
|
||||
function setSelectionSource(value){
|
||||
if (!selectionSourceInput) return;
|
||||
if (value && typeof value === 'string'){
|
||||
selectionSourceInput.value = value;
|
||||
} else {
|
||||
selectionSourceInput.value = '';
|
||||
}
|
||||
}
|
||||
function updateSuggestionsMeta(){
|
||||
if (!suggestionsMeta || !suggestionsState){ return; }
|
||||
var message = '';
|
||||
var isError = false;
|
||||
if (suggestionsState.loading){
|
||||
message = 'Loading partner suggestions…';
|
||||
} else if (suggestionsState.error){
|
||||
message = suggestionsState.error;
|
||||
isError = true;
|
||||
} else if (suggestionsState.visible && suggestionsState.visible.length){
|
||||
var shown = suggestionsState.visible.length;
|
||||
if (suggestionsState.expanded && Array.isArray(suggestionsState.hidden)){
|
||||
shown += suggestionsState.hidden.length;
|
||||
}
|
||||
if (suggestionsState.total && suggestionsState.total > 0){
|
||||
message = 'Showing ' + shown + ' of ' + suggestionsState.total + ' suggestions.';
|
||||
} else {
|
||||
message = 'Suggestions generated from recent deck data.';
|
||||
}
|
||||
var meta = suggestionsState.metadata || {};
|
||||
if (meta.generated_at){
|
||||
message += ' Updated ' + meta.generated_at + '.';
|
||||
}
|
||||
} else if (suggestionsState.loaded){
|
||||
message = 'No partner suggestions available for this commander yet.';
|
||||
} else {
|
||||
message = '';
|
||||
}
|
||||
suggestionsMeta.textContent = message;
|
||||
suggestionsMeta.hidden = !message;
|
||||
if (isError){
|
||||
suggestionsMeta.style.color = '#a00';
|
||||
} else {
|
||||
suggestionsMeta.style.color = '';
|
||||
}
|
||||
}
|
||||
|
||||
function markSuggestionActive(){
|
||||
if (!suggestionsList || !suggestionsState){ return; }
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
var partnerValue = partnerSel && partnerSel.value ? partnerSel.value.toLowerCase() : '';
|
||||
var backgroundValue = bgSel && bgSel.value ? bgSel.value.toLowerCase() : '';
|
||||
var buttons = suggestionsList.querySelectorAll('[data-partner-suggestion]');
|
||||
buttons.forEach(function(btn){
|
||||
var mode = (btn.getAttribute('data-mode') || 'partner').toLowerCase();
|
||||
var name = (btn.getAttribute('data-name') || '').toLowerCase();
|
||||
var active = false;
|
||||
if (mode === 'background'){
|
||||
active = !!backgroundValue && backgroundValue === name;
|
||||
} else {
|
||||
active = !!partnerValue && partnerValue === name;
|
||||
}
|
||||
btn.classList.toggle('active', active);
|
||||
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function renderSuggestions(){
|
||||
if (!suggestionsBox || !suggestionsList || !suggestionsState){
|
||||
return;
|
||||
}
|
||||
suggestionsList.innerHTML = '';
|
||||
if (suggestionsState.error){
|
||||
updateSuggestionsMeta();
|
||||
if (suggestionsMoreButton){ suggestionsMoreButton.hidden = true; }
|
||||
return;
|
||||
}
|
||||
var items = Array.isArray(suggestionsState.visible) ? suggestionsState.visible.slice() : [];
|
||||
if (suggestionsState.expanded && Array.isArray(suggestionsState.hidden)){
|
||||
items = items.concat(suggestionsState.hidden);
|
||||
}
|
||||
if (!items.length){
|
||||
updateSuggestionsMeta();
|
||||
if (suggestionsMoreButton){ suggestionsMoreButton.hidden = true; }
|
||||
return;
|
||||
}
|
||||
items.forEach(function(item){
|
||||
if (!item || !item.name) return;
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'chip partner-suggestion-chip';
|
||||
btn.style.display = 'flex';
|
||||
btn.style.flexDirection = 'column';
|
||||
btn.style.alignItems = 'flex-start';
|
||||
btn.style.gap = '2px';
|
||||
btn.setAttribute('data-partner-suggestion', '1');
|
||||
btn.setAttribute('data-mode', item.mode || 'partner');
|
||||
btn.setAttribute('data-name', item.name);
|
||||
if (item.mode_label){ btn.setAttribute('data-mode-label', item.mode_label); }
|
||||
if (item.summary){ btn.setAttribute('data-summary', item.summary); }
|
||||
if (typeof item.score_percent === 'number'){ btn.setAttribute('data-score', String(item.score_percent)); }
|
||||
var titleParts = [];
|
||||
if (item.summary){ titleParts.push(item.summary); }
|
||||
if (Array.isArray(item.reasons) && item.reasons.length){ titleParts = titleParts.concat(item.reasons); }
|
||||
if (titleParts.length){ btn.title = titleParts.join(' • '); }
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'partner-suggestion-chip__name';
|
||||
nameSpan.textContent = item.name;
|
||||
nameSpan.style.fontWeight = '600';
|
||||
btn.appendChild(nameSpan);
|
||||
var summaryText = '';
|
||||
if (item.summary){ summaryText = item.summary; }
|
||||
else if (typeof item.score_percent === 'number'){ summaryText = item.score_percent + '% match'; }
|
||||
else if (item.mode_label){ summaryText = item.mode_label; }
|
||||
if (summaryText){
|
||||
var summarySpan = document.createElement('span');
|
||||
summarySpan.className = 'partner-suggestion-chip__meta muted';
|
||||
summarySpan.textContent = summaryText;
|
||||
summarySpan.style.fontSize = '11px';
|
||||
summarySpan.style.opacity = '0.85';
|
||||
btn.appendChild(summarySpan);
|
||||
}
|
||||
suggestionsList.appendChild(btn);
|
||||
});
|
||||
if (suggestionsMoreButton){
|
||||
if (!suggestionsState.expanded && Array.isArray(suggestionsState.hidden) && suggestionsState.hidden.length){
|
||||
suggestionsMoreButton.hidden = false;
|
||||
suggestionsMoreButton.textContent = 'Show more (' + suggestionsState.hidden.length + ')';
|
||||
} else {
|
||||
suggestionsMoreButton.hidden = true;
|
||||
}
|
||||
}
|
||||
markSuggestionActive();
|
||||
updateSuggestionsMeta();
|
||||
}
|
||||
|
||||
function revealHiddenSuggestions(){
|
||||
if (!suggestionsState){ return; }
|
||||
if (Array.isArray(suggestionsState.hidden) && suggestionsState.hidden.length){
|
||||
suggestionsState.expanded = true;
|
||||
renderSuggestions();
|
||||
} else {
|
||||
fetchSuggestions({ includeHidden: true });
|
||||
}
|
||||
}
|
||||
|
||||
function collectSelectNames(kind){
|
||||
var selector = '[data-partner-select="' + kind + '"]';
|
||||
var sel = controls.querySelector(selector);
|
||||
if (!sel){ return []; }
|
||||
var values = [];
|
||||
Array.prototype.forEach.call(sel.options, function(opt){
|
||||
if (!opt || !opt.value){ return; }
|
||||
values.push(opt.value);
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function setSuggestionsLoading(flag){
|
||||
if (!suggestionsState){ return; }
|
||||
suggestionsState.loading = !!flag;
|
||||
if (suggestionsRefreshButton){
|
||||
if (flag){
|
||||
suggestionsRefreshButton.classList.add('loading');
|
||||
suggestionsRefreshButton.setAttribute('aria-busy', 'true');
|
||||
} else {
|
||||
suggestionsRefreshButton.classList.remove('loading');
|
||||
suggestionsRefreshButton.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
updateSuggestionsMeta();
|
||||
}
|
||||
|
||||
function fetchSuggestions(options){
|
||||
if (!suggestionsBox || !suggestionsState){ return; }
|
||||
if (typeof window === 'undefined' || typeof window.fetch !== 'function'){ return; }
|
||||
if (!primaryName){ return; }
|
||||
var includeHidden = !!(options && options.includeHidden);
|
||||
try {
|
||||
var endpoint = suggestionsBox.getAttribute('data-api-endpoint') || '/api/partner/suggestions';
|
||||
var params = new URLSearchParams();
|
||||
params.set('commander', primaryName);
|
||||
var partnerNames = collectSelectNames('secondary');
|
||||
var backgroundNames = collectSelectNames('background');
|
||||
partnerNames.forEach(function(name){ params.append('partner', name); });
|
||||
backgroundNames.forEach(function(name){ params.append('background', name); });
|
||||
params.set('limit', '8');
|
||||
params.set('visible_limit', '3');
|
||||
var modeSet = {};
|
||||
var modes = (options && Array.isArray(options.modes)) ? options.modes : ['partner_with', 'partner', 'doctor_companion', 'background'];
|
||||
modes.forEach(function(mode){
|
||||
var normalized = String(mode || '').trim();
|
||||
if (!normalized){ return; }
|
||||
var lower = normalized.toLowerCase();
|
||||
if (modeSet[lower]){ return; }
|
||||
modeSet[lower] = true;
|
||||
params.append('mode', normalized);
|
||||
});
|
||||
if (includeHidden){ params.set('include_hidden', '1'); }
|
||||
if (options && options.forceRefresh){ params.set('refresh', '1'); }
|
||||
var fetchUrl = endpoint + (endpoint.indexOf('?') === -1 ? '?' : '&') + params.toString();
|
||||
if (suggestionsAbort){
|
||||
try { suggestionsAbort.abort(); } catch(_){ }
|
||||
}
|
||||
suggestionsAbort = new AbortController();
|
||||
setSuggestionsLoading(true);
|
||||
fetch(fetchUrl, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
signal: suggestionsAbort.signal,
|
||||
}).then(function(resp){
|
||||
if (!resp.ok){
|
||||
throw new Error('suggestions request failed');
|
||||
}
|
||||
return resp.json();
|
||||
}).then(function(data){
|
||||
suggestionsAbort = null;
|
||||
suggestionsState.error = '';
|
||||
suggestionsState.loaded = true;
|
||||
suggestionsState.metadata = data && data.metadata ? data.metadata : {};
|
||||
suggestionsState.total = (data && typeof data.total === 'number') ? data.total : 0;
|
||||
suggestionsState.visible = Array.isArray(data && data.visible) ? data.visible : [];
|
||||
suggestionsState.hidden = Array.isArray(data && data.hidden) ? data.hidden : [];
|
||||
suggestionsState.expanded = includeHidden && suggestionsState.hidden.length ? true : false;
|
||||
suggestionsBox.setAttribute('data-loaded', '1');
|
||||
suggestionsBox.setAttribute('data-error', '');
|
||||
renderSuggestions();
|
||||
}).catch(function(err){
|
||||
if (err && err.name === 'AbortError'){ return; }
|
||||
suggestionsState.error = 'Unable to load partner suggestions.';
|
||||
suggestionsBox.setAttribute('data-error', suggestionsState.error);
|
||||
renderSuggestions();
|
||||
}).finally(function(){
|
||||
setSuggestionsLoading(false);
|
||||
});
|
||||
} catch(_err){
|
||||
suggestionsState.error = 'Unable to load partner suggestions.';
|
||||
renderSuggestions();
|
||||
}
|
||||
}
|
||||
var initialWarnings = [];
|
||||
if (warningsBox && warningsBox.dataset.warningsJson){
|
||||
try { initialWarnings = JSON.parse(warningsBox.dataset.warningsJson); }
|
||||
catch(_){ initialWarnings = []; }
|
||||
}
|
||||
var serverPayload = null;
|
||||
if (previewBox && previewBox.dataset.previewJson){
|
||||
try{ serverPayload = JSON.parse(previewBox.dataset.previewJson); }
|
||||
catch(_){ serverPayload = null; }
|
||||
}
|
||||
setServerPayload(serverPayload);
|
||||
var suggestionsBox = document.querySelector('[data-partner-suggestions="' + prefix + '"]');
|
||||
var suggestionsList = null;
|
||||
var suggestionsMeta = null;
|
||||
var suggestionsMoreButton = null;
|
||||
var suggestionsRefreshButton = null;
|
||||
var suggestionsAbort = null;
|
||||
var suggestionsState = null;
|
||||
function parseSuggestionsAttr(element, attr, fallback){
|
||||
if (!element){ return fallback; }
|
||||
var raw = element.getAttribute(attr);
|
||||
if (!raw){ return fallback; }
|
||||
try { return JSON.parse(raw); }
|
||||
catch(_){ return fallback; }
|
||||
}
|
||||
if (suggestionsBox){
|
||||
suggestionsList = suggestionsBox.querySelector('[data-partner-suggestions-list]');
|
||||
suggestionsMeta = suggestionsBox.querySelector('[data-partner-suggestions-meta]');
|
||||
suggestionsMoreButton = suggestionsBox.querySelector('[data-partner-suggestions-more="' + prefix + '"]');
|
||||
suggestionsRefreshButton = suggestionsBox.querySelector('[data-partner-suggestions-refresh="' + prefix + '"]');
|
||||
suggestionsState = {
|
||||
visible: parseSuggestionsAttr(suggestionsBox, 'data-suggestions-json', []),
|
||||
hidden: parseSuggestionsAttr(suggestionsBox, 'data-hidden-json', []),
|
||||
metadata: parseSuggestionsAttr(suggestionsBox, 'data-metadata-json', {}),
|
||||
total: parseInt(suggestionsBox.getAttribute('data-total') || '0', 10) || 0,
|
||||
error: suggestionsBox.getAttribute('data-error') || '',
|
||||
loaded: suggestionsBox.getAttribute('data-loaded') === '1',
|
||||
expanded: false,
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
var modeLabels = {
|
||||
'partner': 'Partner',
|
||||
'partner_with': 'Partner With',
|
||||
'doctor_companion': "Doctor & Companion",
|
||||
'background': 'Choose a Background'
|
||||
};
|
||||
function buildCardImageUrl(name){
|
||||
if (!name) return '';
|
||||
return 'https://api.scryfall.com/cards/named?fuzzy=' + encodeURIComponent(name) + '&format=image&version=normal';
|
||||
}
|
||||
function buildScryfallUrl(name){
|
||||
if (!name) return '';
|
||||
return 'https://scryfall.com/search?q=' + encodeURIComponent(name);
|
||||
}
|
||||
function defaultRoleForMode(mode){
|
||||
if (!mode) return '';
|
||||
switch(String(mode).toLowerCase()){
|
||||
case 'background':
|
||||
return 'Background';
|
||||
case 'doctor_companion':
|
||||
return "Doctor pairing";
|
||||
default:
|
||||
return 'Partner commander';
|
||||
}
|
||||
}
|
||||
var previewAbort = null;
|
||||
if (typeof window !== 'undefined' && !window.partnerPreviewState){
|
||||
try { window.partnerPreviewState = {}; } catch(_){ }
|
||||
}
|
||||
|
||||
function setPreviewState(detail){
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!detail || typeof detail !== 'object') return;
|
||||
var scopeKey = detail.scope || scope || prefix;
|
||||
if (!scopeKey) return;
|
||||
var store = window.partnerPreviewState || {};
|
||||
store[scopeKey] = {
|
||||
theme_tags: Array.isArray(detail.theme_tags) ? detail.theme_tags.slice() : [],
|
||||
payload: detail.payload || null,
|
||||
warnings: Array.isArray(detail.warnings) ? detail.warnings.slice() : [],
|
||||
auto_note: detail.auto_note || null,
|
||||
partner_mode: detail.partner_mode || null,
|
||||
resolved_secondary: detail.resolved_secondary || null,
|
||||
resolved_background: detail.resolved_background || null,
|
||||
secondary_role_label: detail.secondary_role_label || detail.role_label || null,
|
||||
};
|
||||
try { window.partnerPreviewState = store; } catch(_){ }
|
||||
}
|
||||
|
||||
function escapeHtml(str){
|
||||
return String(str || "").replace(/[&<>"']/g, function(ch){
|
||||
return ({"&":"&","<":"<",">":">","\"":""","'":"'"}[ch]);
|
||||
});
|
||||
}
|
||||
|
||||
function clearPreview(){
|
||||
if (!previewBox) return;
|
||||
previewBox.hidden = true;
|
||||
previewBox.innerHTML = '';
|
||||
markSuggestionActive();
|
||||
}
|
||||
|
||||
function renderPreview(payload){
|
||||
if (!previewBox) return;
|
||||
if (!payload){
|
||||
clearPreview();
|
||||
return;
|
||||
}
|
||||
var mode = payload.partner_mode || payload.mode || '';
|
||||
var modeLabel = payload.partner_mode_label || payload.mode_label || modeLabels[mode] || 'Partner Mechanics';
|
||||
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 : [];
|
||||
var imageUrl = payload.secondary_image_url || payload.image_url || '';
|
||||
if (!imageUrl && secondaryName){
|
||||
imageUrl = buildCardImageUrl(secondaryName);
|
||||
}
|
||||
var scryfallUrl = payload.secondary_scryfall_url || payload.scryfall_url || '';
|
||||
if (!scryfallUrl && secondaryName){
|
||||
scryfallUrl = buildScryfallUrl(secondaryName);
|
||||
}
|
||||
var roleLabel = payload.secondary_role_label || payload.role_label || defaultRoleForMode(mode);
|
||||
var html = '<div class="partner-preview__layout">';
|
||||
var normalizedTags = Array.isArray(themes) ? themes.filter(function(tag){ return tag && String(tag).trim(); }).map(function(tag){ return String(tag).trim(); }) : [];
|
||||
themes = normalizedTags;
|
||||
var tagString = normalizedTags.length ? normalizedTags.join(', ') : '';
|
||||
if (imageUrl){
|
||||
var attrParts = [];
|
||||
if (secondaryName){
|
||||
attrParts.push('data-card-name="' + escapeHtml(secondaryName) + '"');
|
||||
attrParts.push('data-original-name="' + escapeHtml(secondaryName) + '"');
|
||||
}
|
||||
if (roleLabel){
|
||||
attrParts.push('data-role="' + escapeHtml(roleLabel) + '"');
|
||||
}
|
||||
if (tagString){
|
||||
attrParts.push('data-tags="' + escapeHtml(tagString) + '"');
|
||||
attrParts.push('data-overlaps="' + escapeHtml(tagString) + '"');
|
||||
}
|
||||
html += '<div class="partner-preview__art card-preview"' + (attrParts.length ? ' ' + attrParts.join(' ') : '') + '>';
|
||||
if (scryfallUrl){
|
||||
html += '<a href="' + escapeHtml(scryfallUrl) + '" target="_blank" rel="noopener">';
|
||||
}
|
||||
html += '<img src="' + escapeHtml(imageUrl) + '" alt="' + escapeHtml((secondaryName || 'Selected card') + ' card image') + '" loading="lazy" decoding="async" data-card-name="' + escapeHtml(secondaryName || '') + '"';
|
||||
if (roleLabel){ html += ' data-role="' + escapeHtml(roleLabel) + '"'; }
|
||||
if (tagString){ html += ' data-tags="' + escapeHtml(tagString) + '" data-overlaps="' + escapeHtml(tagString) + '"'; }
|
||||
html += ' />';
|
||||
if (scryfallUrl){
|
||||
html += '</a>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<div class="partner-preview__details">';
|
||||
html += '<div class="partner-preview__header">' + escapeHtml(modeLabel);
|
||||
if (colorLabel){ html += ' • ' + escapeHtml(colorLabel); }
|
||||
html += '</div>';
|
||||
if (roleLabel){
|
||||
html += '<div class="partner-preview__role">' + escapeHtml(roleLabel) + '</div>';
|
||||
}
|
||||
if (secondaryName){
|
||||
var pairing = escapeHtml(primary);
|
||||
if (pairing){ pairing += ' + '; }
|
||||
html += '<div class="partner-preview__pairing">Pairing: ' + pairing + escapeHtml(secondaryName) + '</div>';
|
||||
}
|
||||
if (themes && themes.length){
|
||||
html += '<div class="partner-preview__themes muted">Theme emphasis: ' + themes.map(escapeHtml).join(', ') + '</div>';
|
||||
}
|
||||
html += '</div></div>';
|
||||
previewBox.innerHTML = html;
|
||||
previewBox.hidden = false;
|
||||
markSuggestionActive();
|
||||
}
|
||||
|
||||
function setServerPayload(payload){
|
||||
serverPayload = (payload && typeof payload === 'object') ? payload : null;
|
||||
if (!previewBox) return;
|
||||
if (serverPayload){
|
||||
try {
|
||||
previewBox.setAttribute('data-preview-json', JSON.stringify(serverPayload));
|
||||
} catch(_){
|
||||
previewBox.removeAttribute('data-preview-json');
|
||||
}
|
||||
} else {
|
||||
previewBox.removeAttribute('data-preview-json');
|
||||
}
|
||||
}
|
||||
|
||||
function updateAutoNote(note){
|
||||
if (!autoNoteBox) return;
|
||||
var text = (note && String(note).trim()) || '';
|
||||
autoNoteBox.setAttribute('aria-live', 'polite');
|
||||
if (autoNoteCopy){
|
||||
autoNoteCopy.textContent = text;
|
||||
} else {
|
||||
autoNoteBox.textContent = text;
|
||||
}
|
||||
autoNoteBox.hidden = !text;
|
||||
autoNoteBox.setAttribute('aria-hidden', (!text).toString());
|
||||
try { autoNoteBox.setAttribute('data-autonote', text); } catch(_){ }
|
||||
}
|
||||
|
||||
function updateWarnings(list){
|
||||
if (!warningsBox) return;
|
||||
var warnings = Array.isArray(list) ? list.filter(function(msg){ return msg && String(msg).trim(); }) : [];
|
||||
try { warningsBox.setAttribute('data-warnings-json', JSON.stringify(warnings)); } catch(_){ }
|
||||
if (!warnings.length){
|
||||
warningsBox.innerHTML = '';
|
||||
warningsBox.hidden = true;
|
||||
warningsBox.setAttribute('aria-hidden', 'true');
|
||||
return;
|
||||
}
|
||||
var html = '<strong>Warnings</strong><ul style="margin:.35rem 0 0 1.1rem;">';
|
||||
warnings.forEach(function(msg){
|
||||
html += '<li>' + escapeHtml(String(msg)) + '</li>';
|
||||
});
|
||||
html += '</ul>';
|
||||
warningsBox.innerHTML = html;
|
||||
warningsBox.hidden = false;
|
||||
warningsBox.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
function dispatchPreview(detail){
|
||||
if (typeof document === 'undefined') return;
|
||||
setPreviewState(detail);
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent('partner:preview', { detail: detail }));
|
||||
} catch(_){ }
|
||||
}
|
||||
|
||||
function requestPreviewUpdate(){
|
||||
if (typeof window === 'undefined' || typeof window.fetch !== 'function') return;
|
||||
if (!primaryName) return;
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
var secondaryVal = partnerSel ? (partnerSel.value || '') : '';
|
||||
var bgVal = bgSel ? (bgSel.value || '') : '';
|
||||
var enabledVal = partnerEnabledInput ? (partnerEnabledInput.value || '') : '1';
|
||||
if (previewAbort){
|
||||
try { previewAbort.abort(); } catch(_){ }
|
||||
}
|
||||
previewAbort = new AbortController();
|
||||
var formData = new FormData();
|
||||
formData.append('commander', primaryName);
|
||||
formData.append('partner_enabled', enabledVal || '1');
|
||||
formData.append('secondary_commander', secondaryVal);
|
||||
formData.append('background', bgVal);
|
||||
formData.append('partner_auto_opt_out', optInput ? (optInput.value || '0') : '0');
|
||||
formData.append('scope', scope || prefix);
|
||||
formData.append('selection_source', selectionSourceInput ? (selectionSourceInput.value || '') : '');
|
||||
fetch('/build/partner/preview', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Accept': 'application/json' },
|
||||
signal: previewAbort.signal,
|
||||
}).then(function(resp){
|
||||
if (!resp.ok){ throw new Error('preview request failed'); }
|
||||
return resp.json();
|
||||
}).then(function(data){
|
||||
previewAbort = null;
|
||||
if (!data) return;
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'preview')){
|
||||
setServerPayload(data.preview);
|
||||
if (data.preview){ renderPreview(data.preview); }
|
||||
else { clearPreview(); }
|
||||
}
|
||||
updateAutoNote(data && data.auto_note);
|
||||
updateWarnings(data && data.warnings);
|
||||
var evtDetail = {
|
||||
scope: (data && data.scope) || scope || prefix,
|
||||
payload: (data && data.preview) || null,
|
||||
theme_tags: (data && data.theme_tags) || [],
|
||||
warnings: (data && data.warnings) || [],
|
||||
auto_note: (data && data.auto_note) || null,
|
||||
partner_mode: (data && data.partner_mode) || null,
|
||||
resolved_secondary: Object.prototype.hasOwnProperty.call(data || {}, 'resolved_secondary') ? (data && data.resolved_secondary) : undefined,
|
||||
resolved_background: Object.prototype.hasOwnProperty.call(data || {}, 'resolved_background') ? (data && data.resolved_background) : undefined,
|
||||
secondary_role_label: data && data.preview ? (data.preview.secondary_role_label || data.preview.role_label || null) : null,
|
||||
};
|
||||
dispatchPreview(evtDetail);
|
||||
if (partnerSel && Object.prototype.hasOwnProperty.call(data, 'resolved_secondary')){
|
||||
partnerSel.value = data.resolved_secondary || '';
|
||||
}
|
||||
if (bgSel && Object.prototype.hasOwnProperty.call(data, 'resolved_background')){
|
||||
bgSel.value = data.resolved_background || '';
|
||||
}
|
||||
}).catch(function(err){
|
||||
if (err && err.name === 'AbortError'){ return; }
|
||||
previewAbort = null;
|
||||
});
|
||||
}
|
||||
|
||||
updateAutoNote(initialAutoNote);
|
||||
updateWarnings(initialWarnings);
|
||||
var initialHasPreview = !!(serverPayload && Array.isArray(serverPayload.theme_tags) && serverPayload.theme_tags.length);
|
||||
if (initialHasPreview || initialWarnings.length || (initialAutoNote && initialAutoNote.trim())){
|
||||
setTimeout(function(){
|
||||
dispatchPreview({
|
||||
scope: scope || prefix,
|
||||
payload: serverPayload,
|
||||
theme_tags: (serverPayload && serverPayload.theme_tags) || [],
|
||||
warnings: initialWarnings,
|
||||
auto_note: initialAutoNote || null,
|
||||
partner_mode: serverPayload ? (serverPayload.partner_mode || serverPayload.mode || null) : null,
|
||||
secondary_role_label: serverPayload ? (serverPayload.secondary_role_label || serverPayload.role_label || null) : null,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function renderFromServer(){
|
||||
if (serverPayload){
|
||||
renderPreview(serverPayload);
|
||||
} else {
|
||||
clearPreview();
|
||||
}
|
||||
}
|
||||
|
||||
function renderFromSelection(sel, modeOverride){
|
||||
if (!sel){
|
||||
if (serverPayload){ renderFromServer(); } else { clearPreview(); }
|
||||
return;
|
||||
}
|
||||
var option = sel.options[sel.selectedIndex];
|
||||
if (!option || !option.value){
|
||||
if (serverPayload){ renderFromServer(); } else { clearPreview(); }
|
||||
return;
|
||||
}
|
||||
var mode = modeOverride || option.getAttribute('data-pairing-mode') || 'partner';
|
||||
var image = option.getAttribute('data-image-url') || '';
|
||||
var link = option.getAttribute('data-scryfall-url') || '';
|
||||
var role = option.getAttribute('data-role-label') || '';
|
||||
if (!image){ image = buildCardImageUrl(option.value); }
|
||||
if (!link){ link = buildScryfallUrl(option.value); }
|
||||
if (!role){ role = defaultRoleForMode(mode); }
|
||||
var payload = {
|
||||
partner_mode: mode,
|
||||
partner_mode_label: option.getAttribute('data-mode-label') || modeLabels[mode] || 'Partner Mechanics',
|
||||
color_label: option.getAttribute('data-color-label') || '',
|
||||
secondary_name: option.value,
|
||||
primary_name: primaryName,
|
||||
secondary_image_url: image,
|
||||
secondary_scryfall_url: link,
|
||||
secondary_role_label: role,
|
||||
};
|
||||
payload.secondary_role_label = role;
|
||||
payload.theme_tags = payload.theme_tags || [];
|
||||
renderPreview(payload);
|
||||
}
|
||||
|
||||
function setOptOut(flag){
|
||||
if (optInput){ optInput.value = flag ? '1' : '0'; }
|
||||
if (autoToggle){
|
||||
autoToggle.classList.toggle('active', !flag);
|
||||
autoToggle.setAttribute('aria-pressed', (!flag).toString());
|
||||
var label = flag ? 'Enable default partner' : 'Use default partner';
|
||||
if (!flag && defaultPartner){ label += ' (' + defaultPartner + ')'; }
|
||||
autoToggle.textContent = label;
|
||||
}
|
||||
markSuggestionActive();
|
||||
}
|
||||
|
||||
function applySuggestionSelection(mode, name){
|
||||
if (!name){ return; }
|
||||
setSelectionSource('suggestion');
|
||||
var normalizedMode = String(mode || '').toLowerCase();
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
if (normalizedMode === 'background'){
|
||||
if (bgSel){ bgSel.value = name; }
|
||||
if (partnerSel){ partnerSel.value = ''; }
|
||||
if (autoToggle){ setOptOut(true); }
|
||||
if (bgSel){
|
||||
renderFromSelection(bgSel, 'background');
|
||||
requestPreviewUpdate();
|
||||
} else {
|
||||
renderFromServer();
|
||||
}
|
||||
} else {
|
||||
if (partnerSel){ partnerSel.value = name; }
|
||||
if (bgSel){ bgSel.value = ''; }
|
||||
if (autoToggle){
|
||||
syncBySelection();
|
||||
} else if (partnerSel){
|
||||
renderFromSelection(partnerSel);
|
||||
requestPreviewUpdate();
|
||||
} else {
|
||||
renderFromServer();
|
||||
}
|
||||
}
|
||||
markSuggestionActive();
|
||||
}
|
||||
|
||||
function syncBySelection(){
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
if (!partnerSel || !autoToggle || !defaultPartner) return;
|
||||
if (partnerSel.value && partnerSel.value.toLowerCase() === defaultPartner.toLowerCase()){
|
||||
setOptOut(false);
|
||||
renderFromSelection(partnerSel);
|
||||
requestPreviewUpdate();
|
||||
} else if (partnerSel.value) {
|
||||
setOptOut(true);
|
||||
renderFromSelection(partnerSel);
|
||||
requestPreviewUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
if (autoToggle){
|
||||
autoToggle.addEventListener('click', function(){
|
||||
var currentOptOut = optInput && optInput.value === '1';
|
||||
if (currentOptOut){
|
||||
setOptOut(false);
|
||||
setSelectionSource('auto');
|
||||
if (defaultPartner){
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
if (partnerSel){ partnerSel.value = defaultPartner; }
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
if (bgSel){ bgSel.value = ''; }
|
||||
renderFromSelection(partnerSel);
|
||||
} else {
|
||||
renderFromServer();
|
||||
}
|
||||
requestPreviewUpdate();
|
||||
} else {
|
||||
setOptOut(true);
|
||||
setSelectionSource('');
|
||||
selects.forEach(function(sel){
|
||||
if (sel && sel.getAttribute('data-partner-select') === 'secondary'){
|
||||
sel.value = '';
|
||||
}
|
||||
});
|
||||
renderFromServer();
|
||||
requestPreviewUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selects.forEach(function(sel){
|
||||
sel.addEventListener('change', function(){
|
||||
setSelectionSource('manual');
|
||||
var key = sel.getAttribute('data-partner-select');
|
||||
if (key === 'secondary' && sel.value){
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
if (bgSel){ bgSel.value = ''; }
|
||||
if (autoToggle){
|
||||
syncBySelection();
|
||||
} else {
|
||||
renderFromSelection(sel);
|
||||
requestPreviewUpdate();
|
||||
}
|
||||
markSuggestionActive();
|
||||
return;
|
||||
}
|
||||
if (key === 'background' && sel.value){
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
if (partnerSel){ partnerSel.value = ''; }
|
||||
if (autoToggle){ setOptOut(true); }
|
||||
renderFromSelection(sel, 'background');
|
||||
requestPreviewUpdate();
|
||||
markSuggestionActive();
|
||||
return;
|
||||
}
|
||||
if (!sel.value){
|
||||
renderFromServer();
|
||||
requestPreviewUpdate();
|
||||
}
|
||||
markSuggestionActive();
|
||||
});
|
||||
});
|
||||
|
||||
if (suggestionsState){
|
||||
if (suggestionsList){
|
||||
suggestionsList.addEventListener('click', function(evt){
|
||||
var target = evt.target.closest('[data-partner-suggestion]');
|
||||
if (!target){ return; }
|
||||
evt.preventDefault();
|
||||
var mode = target.getAttribute('data-mode') || 'partner';
|
||||
var name = target.getAttribute('data-name') || '';
|
||||
applySuggestionSelection(mode, name);
|
||||
});
|
||||
}
|
||||
if (suggestionsMoreButton){
|
||||
suggestionsMoreButton.addEventListener('click', function(){
|
||||
revealHiddenSuggestions();
|
||||
});
|
||||
}
|
||||
if (suggestionsRefreshButton){
|
||||
suggestionsRefreshButton.addEventListener('click', function(){
|
||||
fetchSuggestions({ includeHidden: suggestionsState.expanded, forceRefresh: true });
|
||||
});
|
||||
}
|
||||
if (suggestionsState.visible && suggestionsState.visible.length){
|
||||
renderSuggestions();
|
||||
} else if (suggestionsState.error){
|
||||
updateSuggestionsMeta();
|
||||
} else {
|
||||
fetchSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
if (clearBtn){
|
||||
clearBtn.addEventListener('click', function(){
|
||||
selects.forEach(function(sel){ if (sel){ sel.value = ''; } });
|
||||
if (autoToggle){ setOptOut(true); }
|
||||
setSelectionSource('');
|
||||
renderFromServer();
|
||||
requestPreviewUpdate();
|
||||
markSuggestionActive();
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.newDeckPartnerState){
|
||||
try {
|
||||
var restore = window.newDeckPartnerState;
|
||||
var partnerSel = controls.querySelector('[data-partner-select="secondary"]');
|
||||
var bgSel = controls.querySelector('[data-partner-select="background"]');
|
||||
if (partnerSel && restore.secondary){ partnerSel.value = restore.secondary; }
|
||||
if (bgSel && restore.background){ bgSel.value = restore.background; }
|
||||
if (restore.enabled === false){ selects.forEach(function(sel){ if (sel){ sel.value = ''; } }); }
|
||||
if (partnerSel && partnerSel.value){ renderFromSelection(partnerSel); }
|
||||
else if (bgSel && bgSel.value){ renderFromSelection(bgSel, 'background'); }
|
||||
delete window.newDeckPartnerState;
|
||||
} catch(_){ }
|
||||
}
|
||||
|
||||
if (optInput && optInput.value === '1'){
|
||||
setOptOut(true);
|
||||
renderFromServer();
|
||||
} else {
|
||||
setOptOut(!defaultPartner);
|
||||
if (defaultPartner){ syncBySelection(); }
|
||||
else if (serverPayload){ renderFromServer(); }
|
||||
}
|
||||
markSuggestionActive();
|
||||
|
||||
try {
|
||||
var slot = document.getElementById('newdeck-tags-slot');
|
||||
if (slot) slot.setAttribute('data-has-content', '1');
|
||||
} catch(_){ }
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<section>
|
||||
{# Step phases removed #}
|
||||
{% set partner_preview_payload = partner_preview if partner_preview else (combined_commander if combined_commander else None) %}
|
||||
<div class="two-col two-col-left-rail">
|
||||
<aside class="card-preview" data-card-name="{{ commander.name }}">
|
||||
{# Strip synergy annotation for Scryfall search and image fuzzy param #}
|
||||
|
|
@ -8,6 +9,67 @@
|
|||
<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>
|
||||
{% if partner_preview_payload %}
|
||||
{% set partner_secondary_name = partner_preview_payload.secondary_name %}
|
||||
{% set partner_role_label = partner_preview_payload.secondary_role_label or 'Partner commander' %}
|
||||
{% set partner_theme_tags = partner_preview_payload.theme_tags if partner_preview_payload.theme_tags else [] %}
|
||||
{% set partner_tags_joined = partner_theme_tags|join(', ') %}
|
||||
{% set partner_primary_name = partner_preview_payload.primary_name or commander.name %}
|
||||
{% set partner_image_url = partner_preview_payload.secondary_image_url or partner_preview_payload.image_url %}
|
||||
{% if partner_secondary_name %}
|
||||
{% set partner_name_base = (partner_secondary_name.split(' - Synergy (')[0] if ' - Synergy (' in partner_secondary_name else partner_secondary_name) %}
|
||||
{% else %}
|
||||
{% 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' %}
|
||||
{% endif %}
|
||||
{% set partner_href = partner_preview_payload.secondary_scryfall_url or partner_preview_payload.scryfall_url %}
|
||||
{% if not partner_href and partner_name_base %}
|
||||
{% set partner_href = 'https://scryfall.com/search?q=' ~ partner_name_base|urlencode %}
|
||||
{% endif %}
|
||||
<div class="commander-card partner-card" tabindex="0"
|
||||
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 %}>
|
||||
{% 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' }}"
|
||||
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"
|
||||
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" />
|
||||
{% endif %}
|
||||
{% if partner_href %}</a>{% endif %}
|
||||
</div>
|
||||
<div class="muted partner-label" style="margin-top:.35rem;">
|
||||
{{ 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;">
|
||||
Pairing: {{ partner_primary_name }}{% if partner_secondary_name %} + {{ partner_secondary_name }}{% endif %}
|
||||
</div>
|
||||
{% if partner_preview_payload.color_label %}
|
||||
<div class="muted partner-meta" style="font-size:12px; margin-top:.25rem;">
|
||||
Colors: {{ partner_preview_payload.color_label }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if partner_theme_tags %}
|
||||
<div class="muted partner-meta" style="font-size:12px; margin-top:.25rem;">
|
||||
Theme emphasis: {{ partner_theme_tags|join(', ') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="grow" data-skeleton>
|
||||
<div hx-get="/build/banner" hx-trigger="load"></div>
|
||||
|
||||
|
|
@ -40,29 +102,33 @@
|
|||
</div>
|
||||
<div id="combine-help-tip" class="muted" style="font-size:12px; margin:-.15rem 0 .5rem 0;">Tip: Choose OR for a stronger initial theme pool; switch to AND to tighten synergy.</div>
|
||||
<div id="tag-order" class="muted" style="font-size:12px; margin-bottom:.4rem;"></div>
|
||||
{% if recommended and recommended|length %}
|
||||
<div style="display:flex; align-items:center; gap:.5rem; margin:.25rem 0 .35rem 0;">
|
||||
<div class="muted" style="font-size:12px;">Recommended</div>
|
||||
<button type="button" id="reco-why" class="chip" aria-expanded="false" aria-controls="reco-why-panel" title="Why these are recommended?">Why?</button>
|
||||
<div id="tag-reco-block" data-has-reco="{% if recommended and recommended|length %}1{% else %}0{% endif %}" {% if not (recommended and recommended|length) %}style="display:none;"{% endif %}>
|
||||
<div id="tag-reco-header" style="display:flex; align-items:center; gap:.5rem; margin:.25rem 0 .35rem 0;">
|
||||
<div class="muted" style="font-size:12px;">Recommended</div>
|
||||
<button type="button" id="reco-why" class="chip" aria-expanded="false" aria-controls="reco-why-panel" title="Why these are recommended?" {% if not (recommended and recommended|length) %}style="display:none;"{% endif %}>Why?</button>
|
||||
</div>
|
||||
<div id="reco-why-panel" role="group" aria-label="Why Recommended" aria-hidden="true" data-default-reasons='{{ (recommended_reasons or {}) | tojson }}' style="display:none; border:1px solid #e2e2e2; border-radius:8px; padding:.75rem; margin:-.15rem 0 .5rem 0; background:#f7f7f7; box-shadow:0 2px 8px rgba(0,0,0,.06);">
|
||||
<div class="reco-why-title" style="font-size:12px; color:#222; margin-bottom:.5rem;">Why these themes? <span class="muted" style="color:#555;">Signals from oracle text, color identity, and your local build history.</span></div>
|
||||
<ul class="reco-why-list" style="margin:.25rem 0; padding-left:1.1rem;">
|
||||
{% if recommended and recommended|length %}
|
||||
{% for r in recommended %}
|
||||
{% set tip = (recommended_reasons[r] if (recommended_reasons is defined and recommended_reasons and recommended_reasons.get(r)) else 'From this commander\'s theme list') %}
|
||||
<li style="font-size:12px; color:#222; line-height:1.35;"><strong>{{ r }}</strong>: <span style="color:#333;">{{ tip }}</span></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="tag-reco-list" aria-label="Recommended themes" data-original-tags='{{ (recommended or []) | tojson }}' style="display:flex; gap:.35rem; flex-wrap:wrap; margin-bottom:.5rem;">
|
||||
{% if recommended and recommended|length %}
|
||||
{% for r in recommended %}
|
||||
{% set is_sel_r = (r == (primary_tag or '')) or (r == (secondary_tag or '')) or (r == (tertiary_tag or '')) %}
|
||||
{% set tip = (recommended_reasons[r] if (recommended_reasons is defined and recommended_reasons and recommended_reasons.get(r)) else 'Recommended for this commander') %}
|
||||
<button type="button" class="chip chip-reco{% if is_sel_r %} active{% endif %}" data-tag="{{ r }}" aria-pressed="{% if is_sel_r %}true{% else %}false{% endif %}" title="{{ tip }}">★ {{ r }}</button>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<button type="button" id="reco-select-all" class="chip" title="Add recommended up to 3" {% if not (recommended and recommended|length) %}style="display:none;"{% endif %}>Select all</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="reco-why-panel" role="group" aria-label="Why Recommended" aria-hidden="true" style="display:none; border:1px solid #e2e2e2; border-radius:8px; padding:.75rem; margin:-.15rem 0 .5rem 0; background:#f7f7f7; box-shadow:0 2px 8px rgba(0,0,0,.06);">
|
||||
<div style="font-size:12px; color:#222; margin-bottom:.5rem;">Why these themes? <span class="muted" style="color:#555;">Signals from oracle text, color identity, and your local build history.</span></div>
|
||||
<ul style="margin:.25rem 0; padding-left:1.1rem;">
|
||||
{% for r in recommended %}
|
||||
{% set tip = (recommended_reasons[r] if (recommended_reasons is defined and recommended_reasons and recommended_reasons.get(r)) else 'From this commander\'s theme list') %}
|
||||
<li style="font-size:12px; color:#222; line-height:1.35;"><strong>{{ r }}</strong>: <span style="color:#333;">{{ tip }}</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="tag-reco-list" aria-label="Recommended themes" style="display:flex; gap:.35rem; flex-wrap:wrap; margin-bottom:.5rem;">
|
||||
{% for r in recommended %}
|
||||
{% set is_sel_r = (r == (primary_tag or '')) or (r == (secondary_tag or '')) or (r == (tertiary_tag or '')) %}
|
||||
{% set tip = (recommended_reasons[r] if (recommended_reasons is defined and recommended_reasons and recommended_reasons.get(r)) else 'Recommended for this commander') %}
|
||||
<button type="button" class="chip chip-reco{% if is_sel_r %} active{% endif %}" data-tag="{{ r }}" aria-pressed="{% if is_sel_r %}true{% else %}false{% endif %}" title="{{ tip }}">★ {{ r }}</button>
|
||||
{% endfor %}
|
||||
<button type="button" id="reco-select-all" class="chip" title="Add recommended up to 3">Select all</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="tag-chip-list" aria-label="Available themes" style="display:flex; gap:.35rem; flex-wrap:wrap;">
|
||||
{% for t in tags %}
|
||||
{% set is_sel = (t == (primary_tag or '')) or (t == (secondary_tag or '')) or (t == (tertiary_tag or '')) %}
|
||||
|
|
@ -74,6 +140,10 @@
|
|||
{% endif %}
|
||||
</fieldset>
|
||||
|
||||
{% set partner_id_prefix = 'step2' %}
|
||||
{% set partner_scope = 'step2' %}
|
||||
{% include "build/_partner_controls.html" %}
|
||||
|
||||
<fieldset>
|
||||
<legend>Budget/Power Bracket</legend>
|
||||
<div style="display:grid; gap:.5rem;">
|
||||
|
|
@ -108,8 +178,8 @@
|
|||
<script>
|
||||
(function(){
|
||||
var chipHost = document.getElementById('tag-chip-list');
|
||||
var recoBlock = document.getElementById('tag-reco-block');
|
||||
var recoHost = document.getElementById('tag-reco-list');
|
||||
var selAll = document.getElementById('reco-select-all');
|
||||
var resetBtn = document.getElementById('reset-tags');
|
||||
var primary = document.getElementById('primary_tag');
|
||||
var secondary = document.getElementById('secondary_tag');
|
||||
|
|
@ -117,12 +187,52 @@
|
|||
var tagMode = document.getElementById('tag_mode');
|
||||
var countEl = document.getElementById('tag-count');
|
||||
var orderEl = document.getElementById('tag-order');
|
||||
var whyBtn = document.getElementById('reco-why');
|
||||
var whyPanel = document.getElementById('reco-why-panel');
|
||||
var commander = '{{ commander.name|e }}';
|
||||
var clearPersisted = '{{ (clear_persisted|default(false)) and "1" or "0" }}' === '1';
|
||||
if (!chipHost) return;
|
||||
|
||||
function escapeHtml(str){
|
||||
return String(str || "").replace(/[&<>"']/g, function(ch){
|
||||
return ({"&":"&","<":"<",">":">","\"":""","'":"'"}[ch]);
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectAllBtn(){ return document.getElementById('reco-select-all'); }
|
||||
function getRecoHost(){ return document.getElementById('tag-reco-list'); }
|
||||
function getRecoBlock(){ return document.getElementById('tag-reco-block'); }
|
||||
function getWhyBtn(){ return document.getElementById('reco-why'); }
|
||||
function getWhyPanel(){ return document.getElementById('reco-why-panel'); }
|
||||
function originalRecommendedTags(){
|
||||
var host = getRecoHost();
|
||||
if (!host || !host.dataset.originalTags) return [];
|
||||
try { var parsed = JSON.parse(host.dataset.originalTags); return Array.isArray(parsed) ? parsed : []; }
|
||||
catch(_){ return []; }
|
||||
}
|
||||
function defaultReasonMap(){
|
||||
var panel = getWhyPanel();
|
||||
if (!panel || !panel.getAttribute('data-default-reasons')) return {};
|
||||
try { var parsed = JSON.parse(panel.getAttribute('data-default-reasons')); return parsed && typeof parsed === 'object' ? parsed : {}; }
|
||||
catch(_){ return {}; }
|
||||
}
|
||||
|
||||
var previewScope = 'step2';
|
||||
|
||||
function storageKey(suffix){ return 'step2-' + (commander || 'unknown') + '-' + suffix; }
|
||||
|
||||
function readPartnerPreviewTags(){
|
||||
if (typeof window === 'undefined') return [];
|
||||
var store = window.partnerPreviewState;
|
||||
if (!store) return [];
|
||||
var state = store[previewScope];
|
||||
if (!state) return [];
|
||||
if (Array.isArray(state.theme_tags) && state.theme_tags.length){ return state.theme_tags.slice(); }
|
||||
var payload = state.payload;
|
||||
if (payload && Array.isArray(payload.theme_tags)){ return payload.theme_tags.slice(); }
|
||||
return [];
|
||||
}
|
||||
|
||||
function getSelected(){
|
||||
var arr = [];
|
||||
if (primary && primary.value) arr.push(primary.value);
|
||||
|
|
@ -231,91 +341,196 @@
|
|||
});
|
||||
if (resetBtn) resetBtn.addEventListener('click', function(){ setSelected([]); updateChipsState(); });
|
||||
|
||||
// attach handlers to existing chips
|
||||
Array.prototype.forEach.call(chipHost.querySelectorAll('button.chip'), function(btn){
|
||||
chipHost.addEventListener('click', function(e){
|
||||
var btn = e.target.closest('button.chip');
|
||||
if (!btn || !chipHost.contains(btn)) return;
|
||||
var t = btn.dataset.tag || '';
|
||||
btn.addEventListener('click', function(){ toggleTag(t); });
|
||||
btn.addEventListener('keydown', function(e){
|
||||
if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); toggleTag(t); }
|
||||
else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
var chips = Array.prototype.slice.call(chipHost.querySelectorAll('button.chip'));
|
||||
var ix = chips.indexOf(e.currentTarget);
|
||||
var next = (e.key === 'ArrowRight') ? chips[Math.min(ix+1, chips.length-1)] : chips[Math.max(ix-1, 0)];
|
||||
if (next) { try { next.focus(); } catch(_){ } }
|
||||
}
|
||||
});
|
||||
if (!t) return;
|
||||
toggleTag(t);
|
||||
});
|
||||
|
||||
chipHost.addEventListener('keydown', function(e){
|
||||
var btn = e.target.closest('button.chip');
|
||||
if (!btn || !chipHost.contains(btn)) return;
|
||||
var t = btn.dataset.tag || '';
|
||||
if (!t) return;
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
toggleTag(t);
|
||||
} else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
var chips = Array.prototype.slice.call(chipHost.querySelectorAll('button.chip'));
|
||||
var ix = chips.indexOf(btn);
|
||||
if (ix >= 0){
|
||||
var next = (e.key === 'ArrowRight') ? chips[Math.min(ix+1, chips.length-1)] : chips[Math.max(ix-1, 0)];
|
||||
if (next && next.focus){
|
||||
try { next.focus(); } catch(_){ }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// attach handlers to recommended chips and select-all
|
||||
if (recoHost){
|
||||
Array.prototype.forEach.call(recoHost.querySelectorAll('button.chip-reco'), function(btn){
|
||||
var t = btn.dataset.tag || '';
|
||||
btn.addEventListener('click', function(){ toggleTag(t); });
|
||||
});
|
||||
if (selAll){
|
||||
selAll.addEventListener('click', function(){
|
||||
recoHost.addEventListener('click', function(e){
|
||||
var btn = e.target.closest('button');
|
||||
if (!btn || !recoHost.contains(btn)) return;
|
||||
if (btn.id === 'reco-select-all'){
|
||||
e.preventDefault();
|
||||
try {
|
||||
var sel = getSelected();
|
||||
var recs = Array.prototype.slice.call(recoHost.querySelectorAll('button.chip-reco')).map(function(b){ return b.dataset.tag || ''; }).filter(Boolean);
|
||||
var combined = sel.slice();
|
||||
recs.forEach(function(t){ if (combined.indexOf(t) === -1) combined.push(t); });
|
||||
combined = combined.slice(-3); // keep last 3
|
||||
combined = combined.slice(-3);
|
||||
setSelected(combined);
|
||||
updateChipsState();
|
||||
updateSelectAllState();
|
||||
} catch(_){ }
|
||||
});
|
||||
}
|
||||
// Why recommended panel toggle
|
||||
var whyBtn = document.getElementById('reco-why');
|
||||
var whyPanel = document.getElementById('reco-why-panel');
|
||||
function setWhy(open){
|
||||
if (!whyBtn || !whyPanel) return;
|
||||
whyBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
whyPanel.style.display = open ? 'block' : 'none';
|
||||
whyPanel.setAttribute('aria-hidden', open ? 'false' : 'true');
|
||||
}
|
||||
if (whyBtn && whyPanel){
|
||||
whyBtn.addEventListener('click', function(e){
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (btn.classList.contains('chip-reco')){
|
||||
var t = btn.dataset.tag || '';
|
||||
if (t){ toggleTag(t); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWhyPanel(open){
|
||||
if (!whyBtn || !whyPanel) return;
|
||||
whyBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
whyPanel.style.display = open ? 'block' : 'none';
|
||||
whyPanel.setAttribute('aria-hidden', open ? 'false' : 'true');
|
||||
}
|
||||
|
||||
if (whyBtn && whyPanel){
|
||||
whyBtn.addEventListener('click', function(e){
|
||||
e.stopPropagation();
|
||||
var isOpen = whyBtn.getAttribute('aria-expanded') === 'true';
|
||||
toggleWhyPanel(!isOpen);
|
||||
if (!isOpen){ try { whyPanel.focus && whyPanel.focus(); } catch(_){ } }
|
||||
});
|
||||
document.addEventListener('click', function(e){
|
||||
try {
|
||||
var isOpen = whyBtn.getAttribute('aria-expanded') === 'true';
|
||||
setWhy(!isOpen);
|
||||
if (!isOpen){ try { whyPanel.focus && whyPanel.focus(); } catch(_){} }
|
||||
});
|
||||
document.addEventListener('click', function(e){
|
||||
try {
|
||||
var isOpen = whyBtn.getAttribute('aria-expanded') === 'true';
|
||||
if (!isOpen) return;
|
||||
if (whyPanel.contains(e.target) || whyBtn.contains(e.target)) return;
|
||||
setWhy(false);
|
||||
} catch(_){}
|
||||
});
|
||||
document.addEventListener('keydown', function(e){
|
||||
if (e.key === 'Escape'){ setWhy(false); }
|
||||
});
|
||||
if (!isOpen) return;
|
||||
if (whyPanel.contains(e.target) || whyBtn.contains(e.target)) return;
|
||||
toggleWhyPanel(false);
|
||||
} catch(_){ }
|
||||
});
|
||||
document.addEventListener('keydown', function(e){
|
||||
if (e.key === 'Escape'){ toggleWhyPanel(false); }
|
||||
});
|
||||
}
|
||||
|
||||
function refreshWhyPanel(partnerTags){
|
||||
var panel = getWhyPanel();
|
||||
if (!panel) return;
|
||||
var list = panel.querySelector('.reco-why-list');
|
||||
if (!list) return;
|
||||
var reasons = defaultReasonMap();
|
||||
var base = originalRecommendedTags();
|
||||
var seen = new Set();
|
||||
var items = [];
|
||||
base.forEach(function(tag){
|
||||
var value = String(tag || '').trim();
|
||||
if (!value) return;
|
||||
var key = value.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
var tip = reasons && reasons[value] ? reasons[value] : 'From this commander\'s theme list';
|
||||
items.push('<li style="font-size:12px; color:#222; line-height:1.35;"><strong>' + escapeHtml(value) + '</strong>: <span style="color:#333;">' + escapeHtml(tip) + '</span></li>');
|
||||
});
|
||||
(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);
|
||||
items.push('<li style="font-size:12px; color:#222; line-height:1.35;"><strong>' + escapeHtml(value) + '</strong>: <span style="color:#333;">Synergizes with selected partner pairing</span></li>');
|
||||
});
|
||||
list.innerHTML = items.join('');
|
||||
if (!items.length){
|
||||
toggleWhyPanel(false);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePartnerRecommendations(tags){
|
||||
var host = getRecoHost();
|
||||
var block = getRecoBlock();
|
||||
if (!host || !block) return;
|
||||
var selectAllBtn = getSelectAllBtn();
|
||||
Array.prototype.slice.call(host.querySelectorAll('button.partner-suggestion')).forEach(function(btn){ btn.remove(); });
|
||||
var unique = [];
|
||||
var seen = new Set();
|
||||
(Array.isArray(tags) ? tags : []).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);
|
||||
});
|
||||
var insertBefore = selectAllBtn && selectAllBtn.parentElement === host ? selectAllBtn : 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.setAttribute('aria-pressed', getSelected().indexOf(tag) >= 0 ? 'true' : 'false');
|
||||
btn.title = 'Synergizes with selected partner pairing';
|
||||
btn.textContent = '★ ' + tag;
|
||||
if (insertBefore){ host.insertBefore(btn, insertBefore); }
|
||||
else { host.appendChild(btn); }
|
||||
});
|
||||
var hasAny = host.querySelectorAll('button.chip-reco').length > 0;
|
||||
block.style.display = hasAny ? '' : 'none';
|
||||
block.setAttribute('data-has-reco', hasAny ? '1' : '0');
|
||||
var btnEl = getWhyBtn();
|
||||
if (btnEl){ btnEl.style.display = hasAny ? '' : 'none'; }
|
||||
if (selectAllBtn){ selectAllBtn.style.display = hasAny ? '' : 'none'; }
|
||||
refreshWhyPanel(unique);
|
||||
updateSelectAllState();
|
||||
updateChipsState();
|
||||
}
|
||||
|
||||
function updateSelectAllState(){
|
||||
try {
|
||||
if (!selAll) return;
|
||||
var selAllBtn = getSelectAllBtn();
|
||||
if (!selAllBtn) return;
|
||||
var sel = getSelected();
|
||||
var recs = recoHost ? Array.prototype.slice.call(recoHost.querySelectorAll('button.chip-reco')).map(function(b){ return b.dataset.tag || ''; }).filter(Boolean) : [];
|
||||
var host = getRecoHost();
|
||||
var recs = host ? Array.prototype.slice.call(host.querySelectorAll('button.chip-reco')).map(function(b){ return b.dataset.tag || ''; }).filter(Boolean) : [];
|
||||
var unselected = recs.filter(function(t){ return sel.indexOf(t) === -1; });
|
||||
var atCap = sel.length >= 3;
|
||||
var noNew = unselected.length === 0;
|
||||
var disable = atCap || noNew;
|
||||
selAll.disabled = disable;
|
||||
selAll.setAttribute('aria-disabled', disable ? 'true' : 'false');
|
||||
selAllBtn.disabled = disable;
|
||||
selAllBtn.setAttribute('aria-disabled', disable ? 'true' : 'false');
|
||||
if (disable){
|
||||
selAll.title = atCap ? 'Already have 3 themes selected' : 'All recommended already selected';
|
||||
selAllBtn.title = atCap ? 'Already have 3 themes selected' : 'All recommended already selected';
|
||||
} else {
|
||||
selAll.title = 'Add recommended up to 3';
|
||||
selAllBtn.title = 'Add recommended up to 3';
|
||||
}
|
||||
} catch(_){ }
|
||||
}
|
||||
|
||||
document.addEventListener('partner:preview', function(evt){
|
||||
var detail = (evt && evt.detail) || {};
|
||||
if (detail.scope && detail.scope !== previewScope) return;
|
||||
var tags = Array.isArray(detail.theme_tags) && detail.theme_tags.length ? detail.theme_tags : [];
|
||||
if (!tags.length && detail.payload && Array.isArray(detail.payload.theme_tags)){
|
||||
tags = detail.payload.theme_tags;
|
||||
}
|
||||
updatePartnerRecommendations(tags);
|
||||
});
|
||||
|
||||
var initialPartnerTags = readPartnerPreviewTags();
|
||||
if (initialPartnerTags.length){
|
||||
updatePartnerRecommendations(initialPartnerTags);
|
||||
} else {
|
||||
refreshWhyPanel([]);
|
||||
}
|
||||
|
||||
// initial: set from template-selected values, then maybe load persisted if none
|
||||
updateChipsState();
|
||||
loadPersisted();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,25 @@
|
|||
{% from 'partials/_macros.html' import color_identity %}
|
||||
{% set combined = combined_commander if combined_commander else {} %}
|
||||
{% set display_commander_name = commander_display_name or commander %}
|
||||
{% if not display_commander_name %}
|
||||
{% set display_commander_name = commander %}
|
||||
{% endif %}
|
||||
{% set color_identity_list = commander_color_identity if commander_color_identity else [] %}
|
||||
{% if not color_identity_list and summary and summary.colors %}
|
||||
{% set color_identity_list = summary.colors %}
|
||||
{% endif %}
|
||||
{% set color_label = commander_color_label %}
|
||||
{% if not color_label and color_identity_list %}
|
||||
{% set color_label = color_identity_list|join(' / ') %}
|
||||
{% endif %}
|
||||
{% if not color_label and (color_identity_list|length == 0) and combined %}
|
||||
{% set color_label = 'Colorless (C)' %}
|
||||
{% endif %}
|
||||
{% set display_tags_source = deck_theme_tags if deck_theme_tags else (tags if tags else commander_combined_tags) %}
|
||||
{% set hover_tags_source = deck_theme_tags if deck_theme_tags else commander_combined_tags %}
|
||||
{% set hover_tags_joined = hover_tags_source|join(', ') %}
|
||||
{% set display_tags = display_tags_source if display_tags_source else [] %}
|
||||
{% set show_color_identity = color_label or (color_identity_list|length > 0) %}
|
||||
<section>
|
||||
{# Step phases removed #}
|
||||
<div class="two-col two-col-left-rail">
|
||||
|
|
@ -9,16 +31,16 @@
|
|||
data-card-name="{{ commander_base }}"
|
||||
data-original-name="{{ commander }}"
|
||||
data-role="{{ commander_role_label or 'Commander' }}"
|
||||
{% if commander_combined_tags %}data-tags="{{ commander_combined_tags|join(', ') }}"{% endif %}
|
||||
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
|
||||
{% 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="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 }}"
|
||||
data-original-name="{{ commander }}"
|
||||
data-role="{{ commander_role_label or 'Commander' }}"
|
||||
{% if commander_combined_tags %}data-tags="{{ commander_combined_tags|join(', ') }}"{% endif %}
|
||||
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
|
||||
{% 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 %}
|
||||
|
|
@ -30,11 +52,67 @@
|
|||
Commander: <span data-card-name="{{ commander }}"
|
||||
data-original-name="{{ commander }}"
|
||||
data-role="{{ commander_role_label or 'Commander' }}"
|
||||
{% if commander_combined_tags %}data-tags="{{ commander_combined_tags|join(', ') }}"{% endif %}
|
||||
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
|
||||
{% 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 %}>{{ commander }}</span>
|
||||
{% if commander_reason_text %}data-reasons="{{ commander_reason_text|e }}"{% endif %}>{{ display_commander_name or commander }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if combined and combined.secondary_name %}
|
||||
{% set partner_secondary_name = combined.secondary_name %}
|
||||
{% set partner_role_label = combined.secondary_role_label or ('Background' if (combined.partner_mode == 'background') else 'Partner commander') %}
|
||||
{% set partner_theme_tags = combined.theme_tags if combined.theme_tags else [] %}
|
||||
{% set partner_tags_joined = partner_theme_tags|join(', ') %}
|
||||
{% if partner_secondary_name %}
|
||||
{% set partner_name_base = (partner_secondary_name.split(' - Synergy (')[0] if ' - Synergy (' in partner_secondary_name else partner_secondary_name) %}
|
||||
{% else %}
|
||||
{% set partner_name_base = partner_secondary_name %}
|
||||
{% endif %}
|
||||
{% set partner_href = combined.secondary_scryfall_url or combined.scryfall_url %}
|
||||
{% if not partner_href and partner_name_base %}
|
||||
{% set partner_href = 'https://scryfall.com/search?q=' ~ partner_name_base|urlencode %}
|
||||
{% endif %}
|
||||
<div class="commander-card partner-card" tabindex="0"
|
||||
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 %}>
|
||||
{% 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' }}"
|
||||
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"
|
||||
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;">
|
||||
{{ 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;">
|
||||
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;">
|
||||
Colors: {{ combined.color_label }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if partner_theme_tags %}
|
||||
<div class="muted partner-meta" style="font-size:12px; margin-top:.25rem;">
|
||||
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;">
|
||||
|
|
@ -63,15 +141,21 @@
|
|||
data-card-name="{{ commander }}"
|
||||
data-original-name="{{ commander }}"
|
||||
data-role="{{ commander_role_label or 'Commander' }}"
|
||||
{% if commander_combined_tags %}data-tags="{{ commander_combined_tags|join(', ') }}"{% endif %}
|
||||
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
|
||||
{% 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 %}>{{ commander }}</strong>
|
||||
{% if commander_reason_text %}data-reasons="{{ commander_reason_text|e }}"{% endif %}>{{ display_commander_name or commander }}</strong>
|
||||
{% else %}
|
||||
<strong>None selected</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>Tags: {{ deck_theme_tags|default([])|join(', ') }}</p>
|
||||
{% if show_color_identity %}
|
||||
<div class="muted" style="display:flex; align-items:center; gap:.35rem; margin:-.35rem 0 .5rem 0;">
|
||||
{{ 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;">
|
||||
<span>Owned-only: <strong>{{ 'On' if owned_only else 'Off' }}</strong></span>
|
||||
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@
|
|||
{% if display_name %}
|
||||
<div><strong>{{ display_name }}</strong></div>
|
||||
{% endif %}
|
||||
{% set hover_tags_source = deck_theme_tags if deck_theme_tags else (tags if tags else commander_combined_tags) %}
|
||||
{% set hover_tags_joined = hover_tags_source|join(', ') %}
|
||||
<div class="muted">Commander:
|
||||
<strong class="commander-hover"
|
||||
data-card-name="{{ commander }}"
|
||||
data-original-name="{{ commander }}"
|
||||
data-role="{{ commander_role_label }}"
|
||||
{% if commander_combined_tags %}data-tags="{{ commander_combined_tags|join(', ') }}"{% endif %}
|
||||
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
|
||||
{% 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 %}>{{ commander }}</strong>
|
||||
|
|
@ -29,7 +31,7 @@
|
|||
data-card-name="{{ commander_base }}"
|
||||
data-original-name="{{ commander }}"
|
||||
data-role="{{ commander_role_label }}"
|
||||
{% if commander_combined_tags %}data-tags="{{ commander_combined_tags|join(', ') }}"{% endif %}
|
||||
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
|
||||
{% 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 %}>
|
||||
|
|
@ -38,7 +40,7 @@
|
|||
data-card-name="{{ commander_base }}"
|
||||
data-original-name="{{ commander }}"
|
||||
data-role="{{ commander_role_label }}"
|
||||
{% if commander_combined_tags %}data-tags="{{ commander_combined_tags|join(', ') }}"{% endif %}
|
||||
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
|
||||
{% 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 %}
|
||||
|
|
@ -47,7 +49,7 @@
|
|||
<div class="muted" style="margin-top:.25rem;">Commander: <span data-card-name="{{ commander }}"
|
||||
data-original-name="{{ commander }}"
|
||||
data-role="{{ commander_role_label }}"
|
||||
{% if commander_combined_tags %}data-tags="{{ commander_combined_tags|join(', ') }}"{% endif %}
|
||||
{% if hover_tags_joined %}data-tags="{{ hover_tags_joined }}"{% endif %}
|
||||
{% 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 %}>{{ commander }}</span></div>
|
||||
|
|
|
|||
|
|
@ -71,6 +71,13 @@
|
|||
{% endif %}
|
||||
<div id="dfcMetrics" class="muted" style="margin-top:.5rem;">Loading MDFC metrics…</div>
|
||||
</div>
|
||||
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
|
||||
<h3 style="margin-top:0">Dual-Commander diagnostics</h3>
|
||||
<div class="muted" style="margin-bottom:.35rem;">Latest partner, partner-with, doctor, and background pairings with color sources.</div>
|
||||
<div id="partnerMetricsSummary" class="muted">Loading partner metrics…</div>
|
||||
<div id="partnerMetricsModes" class="muted" style="margin-top:.5rem;"></div>
|
||||
<div id="partnerColorSources" style="margin-top:.5rem;"></div>
|
||||
</div>
|
||||
<div class="card" style="background: var(--panel); border:1px solid var(--border); border-radius:10px; padding:.75rem; margin-bottom:.75rem">
|
||||
<h3 style="margin-top:0">Performance (local)</h3>
|
||||
<div class="muted" style="margin-bottom:.35rem">Scroll the Step 5 list; this panel shows a rough FPS estimate and virtualization renders.</div>
|
||||
|
|
@ -436,6 +443,160 @@
|
|||
.catch(function(){ dfcMetricsEl.textContent = 'MDFC metrics unavailable'; });
|
||||
}
|
||||
loadDfcMetrics();
|
||||
var partnerSummaryEl = document.getElementById('partnerMetricsSummary');
|
||||
var partnerModesEl = document.getElementById('partnerMetricsModes');
|
||||
var partnerSourcesEl = document.getElementById('partnerColorSources');
|
||||
function escapeHtml(str){
|
||||
return String(str == null ? '' : str).replace(/[&<>"']/g, function(ch){
|
||||
return ({"&": "&", "<": "<", ">": ">", "\"": """, "'": "'"}[ch]) || ch;
|
||||
});
|
||||
}
|
||||
function labelForPartnerRole(role){
|
||||
var key = role == null ? '' : String(role).toLowerCase();
|
||||
var map = {
|
||||
'primary': 'Primary',
|
||||
'partner': 'Partner commander',
|
||||
'partner_with': 'Partner With',
|
||||
'background': 'Background',
|
||||
'companion': "Doctor's Companion",
|
||||
'doctor_companion': "Doctor's Companion",
|
||||
'doctor': 'Doctor',
|
||||
'secondary': 'Secondary',
|
||||
};
|
||||
if (map[key]) return map[key];
|
||||
if (!key) return '';
|
||||
return key.replace(/_/g, ' ').replace(/\b\w/g, function(ch){ return ch.toUpperCase(); });
|
||||
}
|
||||
function labelForPartnerMode(mode){
|
||||
var key = mode == null ? 'none' : String(mode).toLowerCase();
|
||||
var map = {
|
||||
'none': 'Single commander',
|
||||
'partner': 'Partner',
|
||||
'partner_with': 'Partner With',
|
||||
'background': 'Choose a Background',
|
||||
'doctor_companion': "Doctor & Companion",
|
||||
'doctor': 'Doctor',
|
||||
};
|
||||
return map[key] || labelForPartnerRole(key) || key;
|
||||
}
|
||||
function buildModeCountsHtml(modeCounts, total){
|
||||
var html = '<div><strong>Total pairings observed:</strong> ' + String(total || 0) + '</div>';
|
||||
var keys = Object.keys(modeCounts || {}).filter(function(k){ return Number(modeCounts[k] || 0) > 0; });
|
||||
if (keys.length){
|
||||
var parts = keys.sort().map(function(k){
|
||||
return labelForPartnerMode(k) + ': ' + String(modeCounts[k]);
|
||||
});
|
||||
html += '<div style="font-size:12px;">Mode breakdown: ' + parts.join(' · ') + '</div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
function renderPartnerMetrics(payload){
|
||||
if (!partnerSummaryEl) return;
|
||||
try{
|
||||
if (!payload || payload.ok !== true){
|
||||
partnerSummaryEl.textContent = 'Partner metrics unavailable';
|
||||
if (partnerModesEl) partnerModesEl.textContent = '';
|
||||
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
var metrics = payload.metrics || {};
|
||||
var total = Number(metrics.total_pairs || 0);
|
||||
var modeCounts = metrics.mode_counts || {};
|
||||
var last = metrics.last_summary || null;
|
||||
var updated = metrics.last_updated || '';
|
||||
if (!total || !last){
|
||||
partnerSummaryEl.textContent = 'No partner/background builds recorded yet.';
|
||||
if (partnerModesEl) partnerModesEl.innerHTML = buildModeCountsHtml(modeCounts, total);
|
||||
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
var primary = last.primary != null ? String(last.primary) : '';
|
||||
var secondary = last.secondary != null ? String(last.secondary) : '';
|
||||
if (!primary && Array.isArray(last.names) && last.names.length){ primary = String(last.names[0] || ''); }
|
||||
if (!secondary && Array.isArray(last.names) && last.names.length > 1){ secondary = String(last.names[1] || ''); }
|
||||
var header = '<div><strong>Latest pairing:</strong> ' + escapeHtml(primary || '—');
|
||||
if (secondary){ header += ' + ' + escapeHtml(secondary); }
|
||||
header += '</div>';
|
||||
header += '<div><strong>Mode:</strong> ' + escapeHtml(labelForPartnerMode(last.partner_mode)) + '</div>';
|
||||
var colorLabel = last.color_label != null ? String(last.color_label) : '';
|
||||
var colorCode = last.color_code != null ? String(last.color_code) : '';
|
||||
var colors = Array.isArray(last.color_identity) ? last.color_identity.filter(Boolean).map(String).join(' / ') : '';
|
||||
if (colorLabel || colorCode || colors){
|
||||
var labelText = colorLabel || colors || colorCode;
|
||||
var extra = (!colorLabel && colorCode && colorCode !== labelText) ? ' (' + escapeHtml(colorCode) + ')' : '';
|
||||
if (colorLabel && colorCode && colorLabel.indexOf(colorCode) === -1){ extra = ' (' + escapeHtml(colorCode) + ')'; }
|
||||
header += '<div><strong>Colors:</strong> ' + escapeHtml(labelText) + extra + '</div>';
|
||||
}
|
||||
if (updated){
|
||||
header += '<div style="font-size:11px; opacity:0.75;">Last updated: ' + escapeHtml(updated) + '</div>';
|
||||
}
|
||||
partnerSummaryEl.innerHTML = header;
|
||||
if (partnerModesEl){
|
||||
partnerModesEl.innerHTML = buildModeCountsHtml(modeCounts, total);
|
||||
}
|
||||
if (partnerSourcesEl){
|
||||
var sources = Array.isArray(last.color_sources) ? last.color_sources : [];
|
||||
if (!sources.length){
|
||||
partnerSourcesEl.innerHTML = '<div class="muted">No color source breakdown recorded.</div>';
|
||||
} else {
|
||||
var html = '<div><strong>Color sources</strong></div>';
|
||||
html += '<ul style="list-style:none; padding:0; margin:.35rem 0 0; display:grid; gap:.25rem;">';
|
||||
sources.forEach(function(entry){
|
||||
var color = entry && entry.color != null ? String(entry.color) : '?';
|
||||
var providers = Array.isArray(entry && entry.providers) ? entry.providers : [];
|
||||
var providerParts = providers.map(function(provider){
|
||||
var name = provider && provider.name != null ? String(provider.name) : 'Unknown';
|
||||
var roleLabel = labelForPartnerRole(provider && provider.role);
|
||||
if (roleLabel){
|
||||
return escapeHtml(name) + ' [' + escapeHtml(roleLabel) + ']';
|
||||
}
|
||||
return escapeHtml(name);
|
||||
});
|
||||
if (!providerParts.length){ providerParts.push('—'); }
|
||||
html += '<li class="muted"><span class="chip" style="display:inline-flex; align-items:center; gap:.25rem;"><span class="dot" style="background: var(--border);"></span> ' + escapeHtml(color) + '</span> ' + providerParts.join(', ') + '</li>';
|
||||
});
|
||||
html += '</ul>';
|
||||
var delta = last.color_delta || {};
|
||||
try{
|
||||
var deltaParts = [];
|
||||
var added = Array.isArray(delta.added) ? delta.added.filter(Boolean) : [];
|
||||
var removed = Array.isArray(delta.removed) ? delta.removed.filter(Boolean) : [];
|
||||
if (added.length){ deltaParts.push('Added ' + added.map(escapeHtml).join(', ')); }
|
||||
if (removed.length){ deltaParts.push('Removed ' + removed.map(escapeHtml).join(', ')); }
|
||||
if (deltaParts.length){
|
||||
html += '<div class="muted" style="font-size:12px; margin-top:.35rem;">' + deltaParts.join(' · ') + '</div>';
|
||||
}
|
||||
}catch(_){ }
|
||||
partnerSourcesEl.innerHTML = html;
|
||||
}
|
||||
}
|
||||
}catch(_){
|
||||
partnerSummaryEl.textContent = 'Partner metrics unavailable';
|
||||
if (partnerModesEl) partnerModesEl.textContent = '';
|
||||
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
|
||||
}
|
||||
}
|
||||
function loadPartnerMetrics(){
|
||||
if (!partnerSummaryEl) return;
|
||||
partnerSummaryEl.textContent = 'Loading partner metrics…';
|
||||
fetch('/status/partner_metrics', { cache: 'no-store' })
|
||||
.then(function(resp){
|
||||
if (resp.status === 404){
|
||||
partnerSummaryEl.textContent = 'Diagnostics disabled (partner metrics unavailable)';
|
||||
if (partnerModesEl) partnerModesEl.textContent = '';
|
||||
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
|
||||
return null;
|
||||
}
|
||||
return resp.json();
|
||||
})
|
||||
.then(function(data){ if (data) renderPartnerMetrics(data); })
|
||||
.catch(function(){
|
||||
partnerSummaryEl.textContent = 'Partner metrics unavailable';
|
||||
if (partnerModesEl) partnerModesEl.textContent = '';
|
||||
if (partnerSourcesEl) partnerSourcesEl.innerHTML = '';
|
||||
});
|
||||
}
|
||||
loadPartnerMetrics();
|
||||
// Theme status and reset
|
||||
try{
|
||||
var tEl = document.getElementById('themeSummary');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue