// storebound.dev: copy buttons, AI tool tabs, the hero demo (real CLI output) and the How it works stepper. No network, no storage. (() => { const copyText = async text => { try { await navigator.clipboard.writeText(text); return true; } catch {} const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); let ok = false; try { ok = document.execCommand('copy'); } catch {} ta.remove(); return ok; }; for (const btn of document.querySelectorAll('.copy')) { btn.addEventListener('click', async () => { const target = btn.dataset.copyTarget && document.getElementById(btn.dataset.copyTarget); // Drop "# comment" tails so the copied commands run as is. const text = btn.dataset.copy ?? (target ? target.innerText.split('\n').map(l => l.replace(/\s+#.*$/, '')).join('\n').trim() : ''); const ok = await copyText(text); btn.textContent = ok ? 'Copied' : 'Press Ctrl+C'; btn.classList.toggle('done', ok); setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('done'); }, 1600); }); } // Teaser: click to play (nothing loads before that); native controls take over afterwards. for (const root of document.querySelectorAll('[data-player]')) { const video = root.querySelector('video'); const btn = root.querySelector('.play'); video.controls = false; btn.hidden = false; btn.addEventListener('click', () => { btn.hidden = true; video.controls = true; video.play().catch(() => {}); video.focus(); }); } for (const root of document.querySelectorAll('[data-tabs]')) { const tabs = [...root.querySelectorAll('[role="tab"]')]; const select = (tab, focus) => { for (const t of tabs) { const on = t === tab; t.setAttribute('aria-selected', String(on)); t.tabIndex = on ? 0 : -1; document.getElementById(t.getAttribute('aria-controls')).hidden = !on; } if (focus) { tab.focus(); tab.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } }; tabs.forEach((t, i) => { t.addEventListener('click', () => select(t, false)); t.addEventListener('keydown', e => { const k = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key]; if (k === undefined) return; e.preventDefault(); select(tabs[(k + tabs.length) % tabs.length], true); }); }); select(tabs.find(t => t.getAttribute('aria-selected') === 'true') ?? tabs[0], false); } const reduce = matchMedia('(prefers-reduced-motion: reduce)'); // Hero demo: four scenes of real CLI output, each under a plain-language headline. Auto-cycles while the hero is on // screen and the page is visible, pauses on hover; a picked tab stops the cycle. Every scene holds its final frame // for HOLD_MS. Reduced motion: no animation, the tabs show each final frame. // Steps: [data-s] elements appear in order (data-o overrides document order); data-s="type" types a command, // a number is the pause after it in ms. data-flip="id ..." marks checklist rows fixed, data-pill sets the pill. const demo = document.querySelector('[data-demo]'); if (demo) { const tabs = [...demo.querySelectorAll('[role="tab"]')]; const panels = tabs.map(t => document.getElementById(t.getAttribute('aria-controls'))); const cwd = demo.querySelector('[data-cwd]'); const HOLD_MS = 3400, START_MS = 250, CHAR_MS = 30, AFTER_TYPE_MS = 220; const motion = !reduce.matches; let cur = 0, auto = motion, visible = false, hovered = false, run = 0; const shown = () => visible && !document.hidden; const live = () => shown() && !hovered; const sync = () => demo.classList.toggle('is-held', !live()); const STOP = {}; // a wait that only counts down while the demo is on screen (and, while cycling, not hovered); throws when a newer run started const wait = (ms, token) => new Promise((done, fail) => { let left = ms; const tick = () => { const step = Math.min(left, 40); setTimeout(() => { if (token !== run) return fail(STOP); if (auto ? live() : shown()) left -= step; if (left <= 0) done(); else tick(); }, step); }; if (token !== run) fail(STOP); else if (ms <= 0) done(); else tick(); }); const steps = p => { const els = [...p.querySelectorAll('[data-s]')]; return els.map((el, i) => ({ el, i, o: el.dataset.o ? Number(el.dataset.o) : i })).sort((a, b) => a.o - b.o || a.i - b.i).map(x => x.el); }; const stepMs = el => el.dataset.s === 'type' ? el.querySelector('span').textContent.length * CHAR_MS + AFTER_TYPE_MS : el.dataset.s === '' ? 22 : Number(el.dataset.s); const sceneMs = panels.map(p => START_MS + steps(p).reduce((n, el) => n + stepMs(el), 0) + HOLD_MS); const countUp = el => { const to = Number(el.dataset.count), t0 = performance.now(), dur = 700; const f = now => { const k = Math.min(1, (now - t0) / dur); el.textContent = String(Math.round(to * (1 - (1 - k) ** 3))); if (k < 1) requestAnimationFrame(f); }; requestAnimationFrame(f); }; const select = (i, focus) => { cur = i; tabs.forEach((t, k) => { const on = k === i; t.setAttribute('aria-selected', String(on)); t.tabIndex = on ? 0 : -1; panels[k].hidden = !on; }); cwd.textContent = panels[i].dataset.cwd; demo.style.setProperty('--sc-ms', sceneMs[i] + 'ms'); if (focus) tabs[i].focus(); }; // plays one scene from empty to its final frame const play = async (i, token) => { const p = panels[i]; const items = steps(p); for (const el of items) el.classList.remove('on', 'typing'); for (const el of p.querySelectorAll('.fixed')) el.classList.remove('fixed'); for (const c of p.querySelectorAll('[data-count]')) c.textContent = '0'; delete p.dataset.pill; p.classList.add('is-anim'); demo.classList.add('is-ready'); await wait(START_MS, token); for (const el of items) { if (el.dataset.s === 'type') { const n = el.querySelector('span').textContent.length; el.style.setProperty('--n', '0'); el.classList.add('typing', 'on'); for (let k = 1; k <= n; k++) { el.style.setProperty('--n', String(k)); await wait(CHAR_MS, token); } await wait(AFTER_TYPE_MS, token); el.classList.remove('typing'); } else { el.classList.add('on'); if (el.dataset.pill) p.dataset.pill = el.dataset.pill; for (const id of (el.dataset.flip ?? '').split(' ').filter(Boolean)) document.getElementById(id)?.classList.add('fixed'); const c = el.matches('[data-count]') ? el : el.querySelector('[data-count]'); if (c) countUp(c); await wait(stepMs(el), token); } } }; const restartBar = () => { demo.classList.remove('is-auto'); void demo.offsetWidth; if (auto) demo.classList.add('is-auto'); }; const cycle = async () => { const token = ++run; try { for (;;) { select(cur, false); restartBar(); await play(cur, token); await wait(HOLD_MS, token); cur = (cur + 1) % tabs.length; } } catch (e) { if (e !== STOP) throw e; } }; const pick = (i, focus) => { auto = false; demo.classList.remove('is-auto'); const token = ++run; select(i, focus); if (motion) play(i, token).catch(e => { if (e !== STOP) throw e; }); }; tabs.forEach((t, i) => { t.addEventListener('click', () => pick(i, false)); t.addEventListener('keydown', e => { const k = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key]; if (k === undefined) return; e.preventDefault(); pick((k + tabs.length) % tabs.length, true); }); }); if (motion) { demo.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') { hovered = true; sync(); } }); demo.addEventListener('pointerleave', () => { hovered = false; sync(); }); document.addEventListener('visibilitychange', sync); new IntersectionObserver(es => { visible = es[es.length - 1].isIntersecting; sync(); }, { threshold: 0.25 }).observe(demo); sync(); cycle(); } } // How it works: on wide screens a vertical tab list with one shared preview, auto-advancing while in view // until the visitor picks a step. On narrow screens a plain timeline with every step's preview inline. const steps = document.querySelector('[data-steps]'); if (steps) { const items = [...steps.children]; const heads = items.map(li => li.querySelector('.st-head')); const panels = items.map(li => li.querySelector('.st-panel')); const wide = matchMedia('(min-width: 1024px)'); const STEP_MS = 6500; steps.style.setProperty('--step-ms', STEP_MS + 'ms'); let cur = 0, auto = !reduce.matches, timer = 0, inView = false, held = false; const stopTimer = () => { clearTimeout(timer); timer = 0; }; const schedule = () => { stopTimer(); const run = wide.matches && auto && inView && !held && !document.hidden; steps.classList.toggle('is-auto', auto && wide.matches); steps.classList.toggle('is-paused', !run); if (run) timer = setTimeout(() => show((cur + 1) % heads.length, false, true), STEP_MS); }; const restartBar = () => { steps.classList.remove('is-auto'); void steps.offsetWidth; }; const show = (i, focus, animate) => { cur = i; heads.forEach((h, k) => { const on = k === i; h.setAttribute('aria-selected', String(on)); h.tabIndex = on ? 0 : -1; panels[k].hidden = !on; if (on && animate) { panels[k].classList.remove('enter'); void panels[k].offsetWidth; panels[k].classList.add('enter'); } }); if (focus) heads[i].focus(); restartBar(); schedule(); }; const resume = () => { if (wide.matches && auto) show(cur, false, false); }; const pick = (i, focus) => { auto = false; show(i, focus, true); }; heads.forEach((h, i) => { h.addEventListener('click', () => { if (wide.matches) pick(i, false); }); h.addEventListener('keydown', e => { if (!wide.matches) return; const k = { ArrowDown: i + 1, ArrowRight: i + 1, ArrowUp: i - 1, ArrowLeft: i - 1, Home: 0, End: heads.length - 1 }[e.key]; if (k === undefined) return; e.preventDefault(); pick((k + heads.length) % heads.length, true); }); }); for (const b of steps.querySelectorAll('[data-next]')) b.addEventListener('click', () => pick(Number(b.dataset.next), true)); steps.addEventListener('pointerenter', () => { held = true; schedule(); }); steps.addEventListener('pointerleave', () => { held = false; resume(); }); steps.addEventListener('focusin', () => { held = true; schedule(); }); steps.addEventListener('focusout', e => { if (!steps.contains(e.relatedTarget)) { held = false; resume(); } }); document.addEventListener('visibilitychange', schedule); new IntersectionObserver(es => { inView = es[es.length - 1].isIntersecting; resume(); }, { threshold: 0.35 }).observe(steps); const layout = () => { if (wide.matches) { steps.setAttribute('role', 'tablist'); steps.setAttribute('aria-orientation', 'vertical'); steps.setAttribute('aria-label', 'Steps'); items.forEach((li, k) => { li.setAttribute('role', 'presentation'); heads[k].setAttribute('role', 'tab'); heads[k].setAttribute('aria-controls', panels[k].id); panels[k].setAttribute('role', 'tabpanel'); panels[k].setAttribute('aria-labelledby', heads[k].id); panels[k].tabIndex = 0; }); for (const f of steps.querySelectorAll('.st-foot')) f.hidden = false; show(cur, false, false); } else { for (const f of steps.querySelectorAll('.st-foot')) f.hidden = true; stopTimer(); for (const a of ['role', 'aria-orientation', 'aria-label']) steps.removeAttribute(a); items.forEach((li, k) => { li.removeAttribute('role'); for (const a of ['role', 'aria-controls', 'aria-selected', 'tabindex']) heads[k].removeAttribute(a); for (const a of ['role', 'aria-labelledby', 'tabindex']) panels[k].removeAttribute(a); panels[k].hidden = false; }); steps.classList.remove('is-auto', 'is-paused'); } }; wide.addEventListener('change', layout); layout(); } })();