mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 15:40:12 +01:00
502 lines
24 KiB
HTML
502 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
<section class="commander-page">
|
|
<header class="commander-hero">
|
|
<h2>Commanders</h2>
|
|
<p class="muted">Browse the catalog and jump straight into a build with your chosen leader.</p>
|
|
</header>
|
|
|
|
<form
|
|
id="commander-filter-form"
|
|
class="commander-filters"
|
|
action="/commanders"
|
|
method="get"
|
|
hx-get="/commanders"
|
|
hx-target="#commander-results"
|
|
hx-trigger="submit, change from:#commander-color, keyup changed delay:300ms from:#commander-search, keyup changed delay:300ms from:#commander-theme"
|
|
hx-include="#commander-filter-form"
|
|
hx-push-url="true"
|
|
hx-indicator="#commander-loading"
|
|
novalidate
|
|
>
|
|
<label>
|
|
<span class="filter-label">Commander name</span>
|
|
<input type="search" id="commander-search" name="q" value="{{ query }}" placeholder="Search commander names..." autocomplete="off" />
|
|
</label>
|
|
<div class="filter-field">
|
|
<label for="commander-theme" class="filter-label">Theme:</label>
|
|
<div class="autocomplete-container">
|
|
<input type="search" id="commander-theme" name="theme" value="{{ theme_query }}"
|
|
placeholder="Search themes..." autocomplete="off"
|
|
role="combobox"
|
|
aria-autocomplete="list"
|
|
aria-controls="theme-suggestions"
|
|
aria-expanded="false"
|
|
hx-get="/commanders/theme-autocomplete"
|
|
hx-trigger="keyup changed delay:300ms"
|
|
hx-target="#theme-suggestions"
|
|
hx-include="[name='theme']"
|
|
hx-swap="innerHTML" />
|
|
<div id="theme-suggestions" class="autocomplete-dropdown" role="listbox" aria-label="Theme suggestions"></div>
|
|
</div>
|
|
</div>
|
|
<label>
|
|
<span class="filter-label">Color identity</span>
|
|
<select id="commander-color" name="color">
|
|
<option value="">All colors</option>
|
|
{% for code, label in color_options %}
|
|
<option value="{{ code }}" {% if color == code %}selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
<input type="hidden" name="page" value="{{ page }}" />
|
|
<button type="submit" class="btn filter-submit">Apply</button>
|
|
</form>
|
|
|
|
<div
|
|
id="commander-results"
|
|
data-skeleton
|
|
data-skeleton-label=""
|
|
data-skeleton-delay="420"
|
|
data-skeleton-overlay="false"
|
|
aria-live="polite"
|
|
>
|
|
<div
|
|
id="commander-loading"
|
|
class="commander-loading"
|
|
role="status"
|
|
aria-live="polite"
|
|
data-skeleton-placeholder
|
|
>
|
|
<span class="sr-only">Loading commanders…</span>
|
|
<div class="commander-skeleton-list" aria-hidden="true">
|
|
{% for i in range(3) %}
|
|
<article class="commander-skeleton">
|
|
<div class="skeleton-thumb shimmer"></div>
|
|
<div class="skeleton-main">
|
|
<div class="skeleton-line skeleton-title shimmer"></div>
|
|
<div class="skeleton-line skeleton-meta shimmer"></div>
|
|
<div class="skeleton-chip-row">
|
|
<span class="skeleton-chip shimmer"></span>
|
|
<span class="skeleton-chip shimmer"></span>
|
|
<span class="skeleton-chip shimmer"></span>
|
|
</div>
|
|
<div class="skeleton-line skeleton-text shimmer"></div>
|
|
</div>
|
|
<div class="skeleton-cta shimmer"></div>
|
|
</article>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="commander-results-swap" data-skeleton-content>
|
|
{% include "commanders/list_fragment.html" %}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<style>
|
|
.commander-page { display:flex; flex-direction:column; gap:1.25rem; }
|
|
.commander-hero h2 { margin:0; font-size:1.75rem; }
|
|
.commander-hero p { margin:0; max-width:60ch; }
|
|
.commander-filters { display:flex; flex-wrap:wrap; gap:.75rem 1rem; align-items:flex-end; }
|
|
.commander-filters label { display:flex; flex-direction:column; gap:.35rem; min-width:220px; }
|
|
.filter-label { font-size:.85rem; color:var(--muted); letter-spacing:.03em; text-transform:uppercase; }
|
|
.commander-filters input,
|
|
.commander-filters select { background:var(--panel); color:var(--text); border:1px solid var(--border); border-radius:8px; padding:.45rem .6rem; min-height:2.4rem; }
|
|
.commander-filters input:focus,
|
|
.commander-filters select:focus { outline:2px solid var(--ring); outline-offset:2px; }
|
|
.filter-submit { height:2.4rem; align-self:flex-end; }
|
|
|
|
.commander-summary { font-size:.9rem; }
|
|
.commander-error { padding:.75rem .9rem; border:1px solid #f87171; background:rgba(248,113,113,.12); border-radius:10px; color:#fca5a5; }
|
|
.commander-empty { margin:1rem 0 0; }
|
|
.commander-list { display:flex; flex-direction:column; gap:1rem; margin-top:.5rem; }
|
|
|
|
.commander-row { display:flex; gap:1rem; padding:1rem; border:1px solid var(--border); border-radius:14px; background:var(--panel); align-items:stretch; }
|
|
.commander-thumb { width:160px; flex:0 0 auto; position:relative; }
|
|
.commander-thumb img { width:160px; height:auto; border-radius:10px; border:1px solid var(--border); background:#0b0d12; display:block; }
|
|
.commander-thumb-img.is-placeholder { animation: commander-thumb-pulse 1.2s ease-in-out infinite alternate; }
|
|
@keyframes commander-thumb-pulse {
|
|
from { filter:brightness(0.45); }
|
|
to { filter:brightness(0.65); }
|
|
}
|
|
.commander-main { flex:1 1 auto; display:flex; flex-direction:column; gap:.6rem; min-width:0; }
|
|
.commander-header { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .75rem; }
|
|
.commander-name { margin:0; font-size:1.25rem; }
|
|
.color-identity { display:flex; align-items:center; gap:.35rem; }
|
|
.commander-context { margin:0; font-size:.95rem; }
|
|
.commander-themes { display:flex; flex-wrap:wrap; gap:.45rem; width:100%; }
|
|
.commander-themes-empty { font-size:.85rem; }
|
|
.commander-theme-chip { display:inline-flex; align-items:center; justify-content:center; flex-wrap:wrap; gap:.2rem; padding:4px 12px; border-radius:9999px; border:1px solid var(--border); background:rgba(148,163,184,.15); font-size:.85rem; line-height:1.3; font-weight:600; letter-spacing:.03em; color:inherit; cursor:pointer; transition:background .18s ease, border-color .18s ease, transform .18s ease; appearance:none; font-family:inherit; white-space:normal; text-align:center; min-width:0; max-width:min(100%, 24ch); word-break:break-word; overflow-wrap: anywhere; }
|
|
.commander-theme-chip:focus-visible { outline:2px solid var(--ring); outline-offset:2px; }
|
|
.commander-theme-chip:hover { background:rgba(148,163,184,.25); border-color:rgba(148,163,184,.45); transform:translateY(-1px); }
|
|
.commander-partners { display:flex; flex-wrap:wrap; gap:.4rem; font-size:.85rem; }
|
|
.commander-partner-sep { opacity:.6; }
|
|
.commander-cta { margin-left:auto; display:flex; align-items:center; }
|
|
.commander-cta .btn { white-space:nowrap; }
|
|
.commander-pagination { display:flex; align-items:center; justify-content:space-between; gap:.75rem; margin-top:1rem; flex-wrap:wrap; }
|
|
.commander-summary + .commander-pagination { margin-top:.75rem; }
|
|
.commander-pagination .pagination-group { display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; }
|
|
.commander-pagination .commander-page-btn { display:inline-flex; align-items:center; justify-content:center; min-width:96px; }
|
|
.commander-pagination .commander-page-btn[disabled],
|
|
.commander-pagination .commander-page-btn.disabled { opacity:.55; cursor:default; pointer-events:none; }
|
|
.commander-pagination-status { font-size:.85rem; color:var(--muted); }
|
|
.theme-recommendations { display:flex; flex-wrap:wrap; align-items:center; gap:.6rem 1rem; margin-top:.75rem; }
|
|
.theme-recommendations-label { font-size:.8rem; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); }
|
|
.theme-recommendations-chips { display:flex; flex-wrap:wrap; gap:.4rem; }
|
|
.theme-suggestion-chip { display:inline-flex; align-items:center; gap:.25rem; padding:4px 10px; border-radius:9999px; border:1px solid var(--border); background:rgba(94,106,136,.18); font-size:.75rem; letter-spacing:.03em; color:inherit; cursor:pointer; transition:background .15s ease, border-color .15s ease, transform .15s ease; }
|
|
.theme-suggestion-chip:hover { background:rgba(94,106,136,.28); border-color:rgba(148,163,184,.45); transform:translateY(-1px); }
|
|
.theme-suggestion-chip:focus-visible { outline:2px solid var(--ring); outline-offset:2px; }
|
|
|
|
.commander-loading { margin-top:1rem; gap:1rem; }
|
|
.commander-results-swap { display:flex; flex-direction:column; }
|
|
.commander-skeleton-list { display:flex; flex-direction:column; gap:1rem; }
|
|
.commander-skeleton { display:flex; gap:1rem; padding:1rem; border:1px solid var(--border); border-radius:14px; background:var(--panel); align-items:stretch; }
|
|
.skeleton-thumb { width:160px; height:220px; border-radius:10px; }
|
|
.skeleton-main { flex:1 1 auto; display:flex; flex-direction:column; gap:.6rem; }
|
|
.skeleton-line { height:16px; border-radius:9999px; }
|
|
.skeleton-title { width:45%; height:22px; }
|
|
.skeleton-meta { width:30%; }
|
|
.skeleton-text { width:65%; }
|
|
.skeleton-chip-row { display:flex; gap:.5rem; flex-wrap:wrap; }
|
|
.skeleton-chip { width:90px; height:22px; border-radius:9999px; display:inline-block; }
|
|
.skeleton-cta { width:120px; height:42px; border-radius:9999px; }
|
|
.shimmer { background:linear-gradient(90deg, rgba(148,163,184,0.25) 25%, rgba(148,163,184,0.15) 37%, rgba(148,163,184,0.25) 63%); background-size:400% 100%; animation:commander-shimmer 1.4s ease-in-out infinite; }
|
|
@keyframes commander-shimmer {
|
|
0% { background-position:100% 0; }
|
|
100% { background-position:-100% 0; }
|
|
}
|
|
.commander-theme-dialog { position:fixed; inset:0; display:none; align-items:center; justify-content:center; padding:1.5rem; background:rgba(15,23,42,.55); backdrop-filter:blur(6px); z-index:1300; }
|
|
.commander-theme-dialog[data-open="true"] { display:flex; }
|
|
.commander-theme-dialog__panel { background:var(--panel); color:var(--text); border-radius:16px; width:min(92vw, 420px); border:1px solid rgba(148,163,184,.35); box-shadow:0 24px 60px rgba(15,23,42,.42); padding:1.5rem; display:flex; flex-direction:column; gap:1rem; }
|
|
.commander-theme-dialog__title { margin:0; font-size:1.45rem; line-height:1.2; }
|
|
.commander-theme-dialog__body { margin:0; font-size:1.2rem; line-height:1.65; white-space:normal; word-break:break-word; }
|
|
.commander-theme-dialog__close { align-self:flex-end; min-width:0; }
|
|
@media (max-width: 640px) {
|
|
.commander-theme-dialog__panel { width:min(94vw, 360px); padding:1.25rem; }
|
|
.commander-theme-dialog__title { font-size:1.3rem; }
|
|
.commander-theme-dialog__body { font-size:1.1rem; }
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.commander-row { flex-direction:column; }
|
|
.commander-thumb img { width:100%; max-width:280px; }
|
|
.commander-cta { margin-left:0; }
|
|
.commander-cta .btn { width:100%; justify-content:center; text-align:center; }
|
|
}
|
|
@media (max-width: 640px) {
|
|
.commander-filters { align-items:stretch; }
|
|
.filter-submit { width:100%; }
|
|
.commander-filters label { flex:1 1 100%; min-width:0; }
|
|
.commander-thumb { width:min(70vw, 220px); align-self:center; }
|
|
.commander-thumb img { width:100%; }
|
|
.skeleton-thumb { width:min(70vw, 220px); height:calc(min(70vw, 220px) * 1.4); }
|
|
}
|
|
|
|
/* Autocomplete dropdown styles */
|
|
.autocomplete-container { position:relative; width:100%; }
|
|
.autocomplete-dropdown { position:absolute; top:100%; left:0; right:0; z-index:1000; background:var(--panel); border:1px solid var(--border); border-radius:8px; margin-top:4px; max-height:280px; overflow-y:auto; box-shadow:0 4px 12px rgba(0,0,0,.25); display:none; }
|
|
.autocomplete-dropdown:not(:empty) { display:block; }
|
|
.autocomplete-item { padding:.5rem .75rem; cursor:pointer; border-bottom:1px solid var(--border); transition:background .15s ease; }
|
|
.autocomplete-item:last-child { border-bottom:none; }
|
|
.autocomplete-item:hover, .autocomplete-item:focus, .autocomplete-item.selected { background:rgba(148,163,184,.15); }
|
|
.autocomplete-item.selected { background:rgba(148,163,184,.25); border-left:3px solid var(--ring); padding-left:calc(.75rem - 3px); }
|
|
.autocomplete-item .tag-count { color:var(--muted); font-size:.85rem; float:right; }
|
|
.autocomplete-empty { padding:.75rem; text-align:center; color:var(--muted); font-size:.85rem; }
|
|
.autocomplete-error { padding:.75rem; text-align:center; color:#f87171; font-size:.85rem; }
|
|
</style>
|
|
<script>
|
|
(function(){
|
|
const form = document.getElementById('commander-filter-form');
|
|
if (!form) return;
|
|
const pageInput = form.querySelector('input[name="page"]');
|
|
if (!pageInput) return;
|
|
|
|
const resetPage = () => { pageInput.value = '1'; };
|
|
const setLastTrigger = (value) => { form.dataset.lastTrigger = value; };
|
|
const searchField = document.getElementById('commander-search');
|
|
const colorField = document.getElementById('commander-color');
|
|
const themeField = document.getElementById('commander-theme');
|
|
if (searchField) {
|
|
searchField.addEventListener('input', () => {
|
|
resetPage();
|
|
setLastTrigger('search');
|
|
});
|
|
}
|
|
if (colorField) {
|
|
colorField.addEventListener('change', () => {
|
|
resetPage();
|
|
setLastTrigger('color');
|
|
});
|
|
}
|
|
if (themeField) {
|
|
themeField.addEventListener('input', () => {
|
|
resetPage();
|
|
setLastTrigger('theme');
|
|
});
|
|
|
|
// Autocomplete dropdown handling
|
|
const autocompleteDropdown = document.getElementById('theme-suggestions');
|
|
if (autocompleteDropdown) {
|
|
let selectedIndex = -1;
|
|
|
|
// Helper to get all autocomplete items
|
|
const getItems = () => Array.from(autocompleteDropdown.querySelectorAll('.autocomplete-item'));
|
|
|
|
// Helper to select an item by index
|
|
const selectItem = (index) => {
|
|
const items = getItems();
|
|
items.forEach((item, i) => {
|
|
if (i === index) {
|
|
item.classList.add('selected');
|
|
item.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
} else {
|
|
item.classList.remove('selected');
|
|
}
|
|
});
|
|
selectedIndex = index;
|
|
};
|
|
|
|
// Helper to apply selected item
|
|
const applySelectedItem = () => {
|
|
const items = getItems();
|
|
const item = items[selectedIndex];
|
|
if (item && item.dataset.value) {
|
|
themeField.value = item.dataset.value;
|
|
autocompleteDropdown.innerHTML = '';
|
|
selectedIndex = -1;
|
|
themeField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
form.dispatchEvent(new Event('submit', { bubbles: true }));
|
|
}
|
|
};
|
|
|
|
// Reset selection when dropdown content changes
|
|
const observer = new MutationObserver(() => {
|
|
selectedIndex = -1;
|
|
getItems().forEach(item => item.classList.remove('selected'));
|
|
// Update aria-expanded based on dropdown content
|
|
const hasContent = autocompleteDropdown.children.length > 0;
|
|
themeField.setAttribute('aria-expanded', hasContent ? 'true' : 'false');
|
|
});
|
|
observer.observe(autocompleteDropdown, { childList: true });
|
|
|
|
// Click handler for autocomplete items
|
|
document.body.addEventListener('click', (e) => {
|
|
const item = e.target.closest('.autocomplete-item');
|
|
if (item && item.dataset.value) {
|
|
themeField.value = item.dataset.value;
|
|
autocompleteDropdown.innerHTML = '';
|
|
selectedIndex = -1;
|
|
themeField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
form.dispatchEvent(new Event('submit', { bubbles: true }));
|
|
}
|
|
});
|
|
|
|
// Close dropdown when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!e.target.closest('.autocomplete-container')) {
|
|
autocompleteDropdown.innerHTML = '';
|
|
selectedIndex = -1;
|
|
}
|
|
});
|
|
|
|
// Keyboard navigation
|
|
themeField.addEventListener('keydown', (e) => {
|
|
const items = getItems();
|
|
const hasItems = items.length > 0;
|
|
|
|
if (e.key === 'Escape') {
|
|
autocompleteDropdown.innerHTML = '';
|
|
selectedIndex = -1;
|
|
e.preventDefault();
|
|
} else if (e.key === 'ArrowDown' && hasItems) {
|
|
e.preventDefault();
|
|
const newIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : 0;
|
|
selectItem(newIndex);
|
|
} else if (e.key === 'ArrowUp' && hasItems) {
|
|
e.preventDefault();
|
|
const newIndex = selectedIndex > 0 ? selectedIndex - 1 : items.length - 1;
|
|
selectItem(newIndex);
|
|
} else if (e.key === 'Enter' && selectedIndex >= 0 && hasItems) {
|
|
e.preventDefault();
|
|
applySelectedItem();
|
|
}
|
|
});
|
|
|
|
// Mouse hover to highlight items
|
|
autocompleteDropdown.addEventListener('mouseover', (e) => {
|
|
const item = e.target.closest('.autocomplete-item');
|
|
if (item) {
|
|
const items = getItems();
|
|
const index = items.indexOf(item);
|
|
if (index >= 0) {
|
|
selectItem(index);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
form.addEventListener('submit', () => {
|
|
if (!form.dataset.lastTrigger) {
|
|
setLastTrigger('submit');
|
|
}
|
|
});
|
|
|
|
const coarseQuery = window.matchMedia('(pointer: coarse)');
|
|
const prefersThemeModal = () => (coarseQuery && coarseQuery.matches) || window.innerWidth <= 768;
|
|
|
|
let themeDialog;
|
|
let themeDialogTitle;
|
|
let themeDialogBody;
|
|
let themeDialogClose;
|
|
|
|
function closeThemeDialog() {
|
|
if (!themeDialog || themeDialog.dataset.open !== 'true') return;
|
|
themeDialog.dataset.open = 'false';
|
|
themeDialog.setAttribute('aria-hidden', 'true');
|
|
const invoker = themeDialog.__lastInvoker;
|
|
themeDialog.__lastInvoker = null;
|
|
if (invoker && typeof invoker.focus === 'function') {
|
|
try {
|
|
invoker.focus({ preventScroll: true });
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
|
|
const ensureThemeDialog = () => {
|
|
if (themeDialog) return themeDialog;
|
|
themeDialog = document.getElementById('commander-theme-dialog');
|
|
if (!themeDialog) {
|
|
themeDialog = document.createElement('div');
|
|
themeDialog.id = 'commander-theme-dialog';
|
|
themeDialog.className = 'commander-theme-dialog';
|
|
themeDialog.setAttribute('aria-hidden', 'true');
|
|
themeDialog.innerHTML = `
|
|
<div class="commander-theme-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="commander-theme-dialog-title">
|
|
<h3 class="commander-theme-dialog__title" id="commander-theme-dialog-title"></h3>
|
|
<p class="commander-theme-dialog__body"></p>
|
|
<button type="button" class="btn commander-theme-dialog__close">Close</button>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(themeDialog);
|
|
}
|
|
themeDialogTitle = themeDialog.querySelector('.commander-theme-dialog__title');
|
|
themeDialogBody = themeDialog.querySelector('.commander-theme-dialog__body');
|
|
themeDialogClose = themeDialog.querySelector('.commander-theme-dialog__close');
|
|
if (themeDialogClose && !themeDialogClose.__bound) {
|
|
themeDialogClose.__bound = true;
|
|
themeDialogClose.addEventListener('click', () => closeThemeDialog());
|
|
}
|
|
if (!themeDialog.__backdropBound) {
|
|
themeDialog.__backdropBound = true;
|
|
themeDialog.addEventListener('click', (evt) => {
|
|
if (evt.target === themeDialog) {
|
|
closeThemeDialog();
|
|
}
|
|
});
|
|
}
|
|
return themeDialog;
|
|
};
|
|
|
|
const openThemeDialog = (name, summary, invoker) => {
|
|
ensureThemeDialog();
|
|
if (!themeDialog) return;
|
|
themeDialog.setAttribute('aria-hidden', 'false');
|
|
themeDialog.dataset.open = 'true';
|
|
themeDialog.__lastInvoker = invoker || null;
|
|
if (themeDialogTitle) themeDialogTitle.textContent = name || 'Theme';
|
|
if (themeDialogBody) themeDialogBody.textContent = summary && summary.trim() ? summary : 'Summary unavailable.';
|
|
requestAnimationFrame(() => {
|
|
if (themeDialogClose) {
|
|
try {
|
|
themeDialogClose.focus({ preventScroll: true });
|
|
} catch (_) {}
|
|
}
|
|
});
|
|
};
|
|
|
|
const updatePageFromResults = (container) => {
|
|
if (!container) return;
|
|
const marker = container.querySelector('[data-current-page]');
|
|
if (marker) {
|
|
const current = marker.getAttribute('data-current-page');
|
|
if (current) pageInput.value = current;
|
|
}
|
|
};
|
|
|
|
document.body.addEventListener('htmx:afterSwap', (event) => {
|
|
const target = event.detail && event.detail.target;
|
|
if (!target || target.id !== 'commander-results') return;
|
|
updatePageFromResults(target);
|
|
// Intelligent scroll-to-top: only when triggered from bottom controls or when the summary/top controls are off-screen
|
|
const container = document.getElementById('commander-results');
|
|
const searchEl = document.getElementById('commander-search');
|
|
if (!container) return;
|
|
const lastTrigger = form.dataset.lastTrigger || '';
|
|
form.dataset.lastTrigger = '';
|
|
if (lastTrigger === 'search' || lastTrigger === 'theme') {
|
|
return;
|
|
}
|
|
const invoker = event.detail && event.detail.elt ? event.detail.elt : null;
|
|
const fromBottom = invoker && invoker.closest && invoker.closest('[data-bottom-controls]');
|
|
// If not from bottom, check whether the top of the results is already within view; if so, skip scroll
|
|
const rect = container.getBoundingClientRect();
|
|
const topInView = rect.top >= 0 && rect.top <= (window.innerHeight * 0.25);
|
|
// If we're below the top controls (content's top is above viewport) or the click came from the bottom controls,
|
|
// jump directly to the search input (no smooth animation) for fastest navigation.
|
|
if (fromBottom || rect.top < 0) {
|
|
requestAnimationFrame(() => {
|
|
if (searchEl) {
|
|
searchEl.scrollIntoView({ behavior: 'auto', block: 'start' });
|
|
try { searchEl.focus({ preventScroll: true }); } catch(_) { /* no-op */ }
|
|
} else {
|
|
window.scrollTo({ top: 0, behavior: 'auto' });
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
if (!topInView) {
|
|
requestAnimationFrame(() => {
|
|
container.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
});
|
|
}
|
|
});
|
|
|
|
updatePageFromResults(document.getElementById('commander-results'));
|
|
|
|
document.body.addEventListener('click', (event) => {
|
|
const suggestion = event.target && event.target.closest ? event.target.closest('[data-theme-suggestion]') : null;
|
|
if (suggestion) {
|
|
if (!themeField) return;
|
|
event.preventDefault();
|
|
const value = suggestion.getAttribute('data-theme-suggestion') || '';
|
|
themeField.value = value;
|
|
resetPage();
|
|
setLastTrigger('theme');
|
|
themeField.dispatchEvent(new Event('input', { bubbles: true }));
|
|
try {
|
|
form.requestSubmit();
|
|
} catch (_) {
|
|
form.submit();
|
|
}
|
|
return;
|
|
}
|
|
const chip = event.target && event.target.closest ? event.target.closest('.commander-theme-chip') : null;
|
|
if (chip) {
|
|
event.preventDefault();
|
|
const name = chip.getAttribute('data-theme-name') || chip.textContent.trim();
|
|
const summary = chip.getAttribute('data-theme-summary') || 'Summary unavailable.';
|
|
openThemeDialog(name, summary, chip);
|
|
return;
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape') {
|
|
closeThemeDialog();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|