Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< section >
2025-08-28 14:57:22 -07:00
{# Step phases removed #}
2025-10-06 09:17:59 -07:00
{% set partner_preview_payload = partner_preview if partner_preview else (combined_commander if combined_commander else None) %}
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< div class = "two-col two-col-left-rail" >
< aside class = "card-preview" data-card-name = "{{ commander.name }}" >
2025-09-24 13:57:23 -07:00
{# Strip synergy annotation for Scryfall search and image fuzzy param #}
{% set commander_base = (commander.name.split(' - Synergy (')[0] if ' - Synergy (' in commander.name else commander.name) %}
< a href = "https://scryfall.com/search?q={{ commander_base|urlencode }}" target = "_blank" rel = "noopener" >
2025-10-28 08:21:52 -07:00
< img src = "{{ commander_base|card_image('normal') }}" alt = "{{ commander.name }} card image" data-card-name = "{{ commander_base }}" / >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< / a >
< / aside >
2025-10-06 09:17:59 -07:00
{% 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 %}
2025-10-28 08:21:52 -07:00
{% set partner_image_url = partner_name_base|card_image('normal') %}
2025-10-06 09:17:59 -07:00
{% 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 %}
2025-10-28 08:21:52 -07:00
< img src = "{{ partner_name_base|card_image('normal') }}" alt = "{{ (partner_secondary_name or 'Selected card') ~ ' card image' }}"
2025-10-06 09:17:59 -07:00
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"
2025-10-28 08:21:52 -07:00
srcset="{{ partner_name_base|card_image('small') }} 160w, {{ partner_name_base|card_image('normal') }} 488w"
2025-10-06 09:17:59 -07:00
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 %}
2025-08-26 20:00:07 -07:00
< div class = "grow" data-skeleton >
2025-08-28 14:57:22 -07:00
< div hx-get = "/build/banner" hx-trigger = "load" > < / div >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< form hx-post = "/build/step2" hx-target = "#wizard" hx-swap = "innerHTML" >
< input type = "hidden" name = "commander" value = "{{ commander.name }}" / >
{% if error %}
< div style = "color:#a00; margin:.5rem 0;" > {{ error }}< / div >
{% endif %}
< fieldset >
< legend > Theme Tags< / legend >
{% if tags %}
2025-08-26 11:34:42 -07:00
< input type = "hidden" name = "primary_tag" id = "primary_tag" value = "{{ primary_tag or '' }}" / >
< input type = "hidden" name = "secondary_tag" id = "secondary_tag" value = "{{ secondary_tag or '' }}" / >
< input type = "hidden" name = "tertiary_tag" id = "tertiary_tag" value = "{{ tertiary_tag or '' }}" / >
< input type = "hidden" name = "tag_mode" id = "tag_mode" value = "{{ tag_mode or 'AND' }}" / >
< div class = "muted" style = "font-size:12px; margin-bottom:.35rem;" > Pick up to three themes. Toggle AND/OR to control how themes combine.< / div >
< div style = "display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; margin-bottom:.35rem;" >
< span class = "muted" style = "font-size:12px;" > Combine< / span >
< div role = "group" aria-label = "Combine mode" aria-describedby = "combine-help-tip" >
< label style = "margin-right:.35rem;" title = "AND prioritizes cards that match multiple of your themes (tighter synergy, smaller pool)." >
< input type = "radio" name = "combine_mode_radio" value = "AND" { % if ( tag_mode or ' AND ' ) = = ' AND ' % } checked { % endif % } / > AND
< / label >
< label title = "OR treats your themes as a union (broader pool, fills easier)." >
< input type = "radio" name = "combine_mode_radio" value = "OR" { % if tag_mode = = ' OR ' % } checked { % endif % } / > OR
< / label >
< / div >
< button type = "button" id = "reset-tags" class = "chip" style = "margin-left:.35rem;" > Reset themes< / button >
< span id = "tag-count" class = "muted" style = "font-size:12px;" > < / span >
< / 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 >
2025-10-06 09:17:59 -07:00
< 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 >
2025-08-26 11:34:42 -07:00
< / div >
< 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 '')) %}
< button type = "button" class = "chip{% if is_sel %} active{% endif %}" data-tag = "{{ t }}" aria-pressed = "{% if is_sel %}true{% else %}false{% endif %}" > {{ t }}< / button >
{% endfor %}
< / div >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
{% else %}
< p > No theme tags available for this commander.< / p >
{% endif %}
< / fieldset >
2025-10-06 09:17:59 -07:00
{% set partner_id_prefix = 'step2' %}
{% set partner_scope = 'step2' %}
{% include "build/_partner_controls.html" %}
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< fieldset >
< legend > Budget/Power Bracket< / legend >
< div style = "display:grid; gap:.5rem;" >
{% for b in brackets %}
2025-09-03 18:00:06 -07:00
{% if not gc_commander or b.level >= 3 %}
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< label style = "display:flex; gap:.5rem; align-items:flex-start;" >
< input type = "radio" name = "bracket" value = "{{ b.level }}" { % if ( selected_bracket is defined and selected_bracket = = b . level ) or ( selected_bracket is not defined and loop . first ) % } checked { % endif % } / >
< span > < strong > {{ b.name }}< / strong > — < small > {{ b.desc }}< / small > < / span >
< / label >
2025-09-03 18:00:06 -07:00
{% endif %}
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
{% endfor %}
< / div >
< div class = "muted" style = "margin-top:.35rem; font-size:.9em;" >
Note: This guides deck creation and relaxes/raises constraints, but it is not a guarantee the final deck strictly fits that bracket.
< / div >
< / fieldset >
< div style = "margin-top:1rem;" >
2025-08-26 20:00:07 -07:00
< button type = "submit" class = "btn-continue" data-action = "continue" > Continue to Ideals< / button >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< / div >
< / form >
< div style = "margin-top:.5rem;" >
2025-08-28 14:57:22 -07:00
< form hx-post = "/build/reset-all" hx-target = "#wizard" hx-swap = "innerHTML" style = "display:inline; margin:0;" >
Web UI: setup progress + logs folding, Finished Decks library, commander search UX (debounce, keyboard, highlights, color chips), ranking fixes (first-word priority, substring include), optional auto-select; setup start reliability (POST+GET), force runs, status with percent/ETA/timestamps; stepwise builder with added stage reporting and sidecar summaries; keyboard grid wrap-around; restrict commander search to eligible rows
2025-08-26 09:48:25 -07:00
< button type = "submit" > Start over< / button >
< / form >
< / div >
< / div >
< / div >
< / section >
2025-08-26 11:34:42 -07:00
< script >
(function(){
var chipHost = document.getElementById('tag-chip-list');
2025-10-06 09:17:59 -07:00
var recoBlock = document.getElementById('tag-reco-block');
2025-08-26 11:34:42 -07:00
var recoHost = document.getElementById('tag-reco-list');
var resetBtn = document.getElementById('reset-tags');
var primary = document.getElementById('primary_tag');
var secondary = document.getElementById('secondary_tag');
var tertiary = document.getElementById('tertiary_tag');
var tagMode = document.getElementById('tag_mode');
var countEl = document.getElementById('tag-count');
var orderEl = document.getElementById('tag-order');
2025-10-06 09:17:59 -07:00
var whyBtn = document.getElementById('reco-why');
var whyPanel = document.getElementById('reco-why-panel');
2025-08-26 11:34:42 -07:00
var commander = '{{ commander.name|e }}';
2025-08-28 14:57:22 -07:00
var clearPersisted = '{{ (clear_persisted|default(false)) and "1" or "0" }}' === '1';
2025-08-26 11:34:42 -07:00
if (!chipHost) return;
2025-10-06 09:17:59 -07:00
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';
2025-08-26 11:34:42 -07:00
function storageKey(suffix){ return 'step2-' + (commander || 'unknown') + '-' + suffix; }
2025-10-06 09:17:59 -07:00
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 [];
}
2025-08-26 11:34:42 -07:00
function getSelected(){
var arr = [];
if (primary & & primary.value) arr.push(primary.value);
if (secondary & & secondary.value) arr.push(secondary.value);
if (tertiary & & tertiary.value) arr.push(tertiary.value);
return arr;
}
function setSelected(arr){
arr = Array.from(new Set(arr || [])).filter(Boolean).slice(0,3);
if (primary) primary.value = arr[0] || '';
if (secondary) secondary.value = arr[1] || '';
if (tertiary) tertiary.value = arr[2] || '';
updateCount();
persist();
updateOrderUI();
}
function toggleTag(t){
var cur = getSelected();
var idx = cur.indexOf(t);
if (idx >= 0) { cur.splice(idx, 1); }
else {
if (cur.length >= 3) { cur = cur.slice(1); }
cur.push(t);
}
setSelected(cur);
updateChipsState();
}
function updateCount(){
try { if (countEl) countEl.textContent = getSelected().length + ' / 3 selected'; } catch(_){}
}
function persist(){
try {
localStorage.setItem(storageKey('tags'), JSON.stringify(getSelected()));
if (tagMode) localStorage.setItem(storageKey('mode'), tagMode.value || 'AND');
} catch(_){}
}
function loadPersisted(){
try {
2025-08-28 14:57:22 -07:00
// If this page load follows a fresh commander confirmation, wipe persisted values.
if (clearPersisted){
try {
localStorage.removeItem(storageKey('tags'));
localStorage.removeItem(storageKey('mode'));
} catch(_){ }
}
2025-08-26 11:34:42 -07:00
var savedTags = JSON.parse(localStorage.getItem(storageKey('tags')) || '[]');
var savedMode = localStorage.getItem(storageKey('mode')) || (tagMode & & tagMode.value) || 'AND';
if ((!primary.value & & !secondary.value & & !tertiary.value) & & Array.isArray(savedTags) & & savedTags.length){ setSelected(savedTags); }
if (tagMode) { tagMode.value = (savedMode === 'OR' ? 'OR' : 'AND'); }
// sync radios
syncModeRadios();
} catch(_){}
}
function syncModeRadios(){
try {
var radios = document.querySelectorAll('input[name="combine_mode_radio"]');
Array.prototype.forEach.call(radios, function(r){ r.checked = (r.value === (tagMode & & tagMode.value || 'AND')); });
} catch(_){}
}
function updateChipsState(){
var sel = getSelected();
function applyToContainer(container){
if (!container) return;
var chips = Array.prototype.slice.call(container.querySelectorAll('button.chip'));
chips.forEach(function(btn){
var t = btn.dataset.tag || '';
var active = sel.indexOf(t) >= 0;
btn.classList.toggle('active', active);
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
// update numeric badge for order
var old = btn.querySelector('sup.tag-order');
if (old) { try { old.remove(); } catch(_){} }
if (active){
var idx = sel.indexOf(t);
if (idx >= 0){
var sup = document.createElement('sup');
sup.className = 'tag-order';
sup.style.marginLeft = '.25rem';
sup.style.opacity = '.75';
sup.textContent = String(idx + 1);
btn.appendChild(sup);
}
}
});
}
applyToContainer(chipHost);
applyToContainer(recoHost);
updateCount();
updateOrderUI();
updateSelectAllState();
}
function updateOrderUI(){
if (!orderEl) return;
var sel = getSelected();
if (!sel.length){ orderEl.textContent = ''; return; }
try {
var parts = sel.map(function(t, i){ return (i+1) + '. ' + t; });
orderEl.textContent = 'Selected order: ' + parts.join(' • ');
} catch(_){ orderEl.textContent = ''; }
}
// bind mode radios
Array.prototype.forEach.call(document.querySelectorAll('input[name="combine_mode_radio"]'), function(r){
r.addEventListener('change', function(){ if (tagMode) { tagMode.value = r.value; persist(); } });
});
if (resetBtn) resetBtn.addEventListener('click', function(){ setSelected([]); updateChipsState(); });
2025-10-06 09:17:59 -07:00
chipHost.addEventListener('click', function(e){
var btn = e.target.closest('button.chip');
if (!btn || !chipHost.contains(btn)) return;
2025-08-26 11:34:42 -07:00
var t = btn.dataset.tag || '';
2025-10-06 09:17:59 -07:00
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){
2025-08-26 11:34:42 -07:00
var next = (e.key === 'ArrowRight') ? chips[Math.min(ix+1, chips.length-1)] : chips[Math.max(ix-1, 0)];
2025-10-06 09:17:59 -07:00
if (next & & next.focus){
try { next.focus(); } catch(_){ }
}
2025-08-26 11:34:42 -07:00
}
2025-10-06 09:17:59 -07:00
}
2025-08-26 11:34:42 -07:00
});
if (recoHost){
2025-10-06 09:17:59 -07:00
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();
2025-08-26 11:34:42 -07:00
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); });
2025-10-06 09:17:59 -07:00
combined = combined.slice(-3);
2025-08-26 11:34:42 -07:00
setSelected(combined);
updateChipsState();
updateSelectAllState();
} catch(_){ }
2025-10-06 09:17:59 -07:00
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 {
2025-08-26 11:34:42 -07:00
var isOpen = whyBtn.getAttribute('aria-expanded') === 'true';
2025-10-06 09:17:59 -07:00
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);
2025-08-26 11:34:42 -07:00
}
}
2025-10-06 09:17:59 -07:00
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();
}
2025-08-26 11:34:42 -07:00
function updateSelectAllState(){
try {
2025-10-06 09:17:59 -07:00
var selAllBtn = getSelectAllBtn();
if (!selAllBtn) return;
2025-08-26 11:34:42 -07:00
var sel = getSelected();
2025-10-06 09:17:59 -07:00
var host = getRecoHost();
var recs = host ? Array.prototype.slice.call(host.querySelectorAll('button.chip-reco')).map(function(b){ return b.dataset.tag || ''; }).filter(Boolean) : [];
2025-08-26 11:34:42 -07:00
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;
2025-10-06 09:17:59 -07:00
selAllBtn.disabled = disable;
selAllBtn.setAttribute('aria-disabled', disable ? 'true' : 'false');
2025-08-26 11:34:42 -07:00
if (disable){
2025-10-06 09:17:59 -07:00
selAllBtn.title = atCap ? 'Already have 3 themes selected' : 'All recommended already selected';
2025-08-26 11:34:42 -07:00
} else {
2025-10-06 09:17:59 -07:00
selAllBtn.title = 'Add recommended up to 3';
2025-08-26 11:34:42 -07:00
}
} catch(_){ }
}
2025-10-06 09:17:59 -07:00
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([]);
}
2025-08-26 11:34:42 -07:00
// initial: set from template-selected values, then maybe load persisted if none
updateChipsState();
loadPersisted();
updateChipsState();
})();
< / script >