diff --git a/static/app.js b/static/app.js index cd77cfd..c6168dd 100644 --- a/static/app.js +++ b/static/app.js @@ -6163,3 +6163,60 @@ loadSettings().then(() => { elLoadKey(); })(); loadVoiceLibrary().then(renderIntegrationSnippets).catch(e => status('Voice library load failed: ' + e.message)); + +// ── Collapsible cards ────────────────────────────────────────────────────── + +(function initCollapsibleCards() { + const LS_KEY = 'card-collapse-v1'; + const load = () => { try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); } catch { return {}; } }; + const save = (k, v) => { const s = load(); s[k] = v; try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch {} }; + const slug = t => t.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 48); + + // Sync existing
elements inside .card to localStorage + document.querySelectorAll('.card > details').forEach(det => { + const sum = det.querySelector('summary'); + if (!sum) return; + const key = 'det-' + slug(sum.textContent); + const st = load(); + if (st[key] !== undefined) det.open = st[key]; + else st[key] = det.open; // persist current default + det.addEventListener('toggle', () => save(key, det.open)); + }); + + // Add collapse toggle to every .card that has a direct h2 child + document.querySelectorAll('.card > h2').forEach(h2 => { + const card = h2.parentElement; + if (!card || card.dataset.colInit) return; + card.dataset.colInit = '1'; + + const key = 'card-' + slug(h2.textContent); + const open = load()[key] !== false; // default: open + + // Prepend a rotating chevron inside the h2 + const chev = document.createElement('span'); + chev.className = 'card-chev'; + chev.setAttribute('aria-hidden', 'true'); + h2.prepend(chev); + h2.classList.add('card-collapse-h2'); + + // Wrap every element after h2 in a single body div + const body = document.createElement('div'); + body.className = 'card-col-body'; + let sib = h2.nextElementSibling; + while (sib) { const nx = sib.nextElementSibling; body.appendChild(sib); sib = nx; } + card.appendChild(body); + + const apply = (isOpen) => { + body.hidden = !isOpen; + card.classList.toggle('card-col-closed', !isOpen); + }; + + apply(open); + + h2.addEventListener('click', () => { + const nowOpen = body.hidden; // hidden → about to open + apply(nowOpen); + save(key, nowOpen); + }); + }); +})(); diff --git a/static/style.css b/static/style.css index 9e36e3a..d747395 100644 --- a/static/style.css +++ b/static/style.css @@ -161,6 +161,18 @@ body { text-transform: uppercase; letter-spacing: .09em; } +/* ── Collapsible cards ───────────────────────────────────────────────────── */ +.card-collapse-h2 { cursor: pointer; user-select: none; display: flex; align-items: center; gap: 7px; } +.card-collapse-h2:hover { opacity: .8; } +.card-chev { display: inline-block; flex-shrink: 0; width: 16px; height: 16px; border-radius: 4px; background: var(--panel); border: 1px solid var(--border); color: var(--accent); font-size: 10px; line-height: 16px; text-align: center; transition: transform .2s; } +.card-chev::before { content: '▾'; } +.card-col-closed .card-chev { transform: rotate(-90deg); } +/* Sync
inside cards to the same chevron look */ +.card > details > summary { list-style: none; cursor: pointer; user-select: none; display: flex; align-items: center; gap: 7px; } +.card > details > summary::marker, .card > details > summary::-webkit-details-marker { display: none; } +.card > details > summary::before { content: '▾'; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 16px; height: 16px; border-radius: 4px; background: var(--panel); border: 1px solid var(--border); color: var(--accent); font-size: 10px; transition: transform .2s; } +.card > details:not([open]) > summary::before { transform: rotate(-90deg); } + /* ── Drop zone ──────────────────────────────────────────────────────────── */ #drop-zone { border: 2px dashed var(--border); border-radius: var(--radius);