import { createEffect, createMemo, createSignal, For, onMount, onCleanup } from "solid-js"; import { StackCard } from "./StackCard"; import type { CardId, StackBodies, StackCardSpec } from "./types"; interface StackDeckProps { active: CardId; onActiveChange: (id: CardId) => void; onBack: () => void; cards: readonly StackCardSpec[]; bodies: StackBodies; /** Extension version string (e.g. "3.6.38"), shown next to the card counter. */ version?: string; isFanning?: boolean; celebratingCardId?: CardId | null; isPro?: boolean; } const EXPANDED_H = 258; const DEFAULT_FOCUS_H = 462; const TAB_H = 46; const TAB_OVERLAP = 14; export function StackDeck(props: StackDeckProps) { const [deckOpen, setDeckOpen] = createSignal(true); const [focusHeight, setFocusHeight] = createSignal(DEFAULT_FOCUS_H); let focusedAt = 0; let deckEl: HTMLDivElement | undefined; function handleBack() { if (!deckOpen()) { setDeckOpen(true); } else { props.onBack(); } } createEffect(() => { if (props.isFanning) setDeckOpen(true); }); onMount(() => { const listener = () => handleBack(); window.addEventListener("peach:settings-back", listener); const updateFocusHeight = () => { if (!deckEl) return; setFocusHeight(deckEl.clientHeight || DEFAULT_FOCUS_H); }; updateFocusHeight(); const observer = typeof ResizeObserver === "undefined" ? undefined : new ResizeObserver(updateFocusHeight); if (deckEl) observer?.observe(deckEl); if (!observer) window.addEventListener("resize", updateFocusHeight); onCleanup(() => { window.removeEventListener("peach:settings-back", listener); observer?.disconnect(); if (!observer) window.removeEventListener("resize", updateFocusHeight); }); }); const activeIndex = createMemo(() => { const idx = props.cards.findIndex((card) => card.id === props.active); return idx >= 0 ? idx : 0; }); return (
Settings
{activeIndex() + 1} / {props.cards.length}
{ deckEl = el; }} class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"} ${props.isFanning ? "is-fanning" : ""} ${props.isPro ? "is-pro" : ""}`} onWheel={(e) => { const cardEl = (e.target as HTMLElement).closest("[data-settings-card-id]"); if (!cardEl || cardEl.dataset.active !== "true") return; if (deckOpen() && e.deltaY > 5) { // Scroll down on active card → unveil to focus mode setDeckOpen(false); focusedAt = Date.now(); } else if (!deckOpen() && e.deltaY < -5 && Date.now() - focusedAt > 300) { // Scroll up at top of focused card → minimize back to deck // 300ms guard prevents fighting right after unveil const body = cardEl.querySelector(".stk-card-body"); if (!body || body.scrollTop === 0) setDeckOpen(true); } }} > {(card, index) => { const active = () => card.id === props.active; const concealed = () => !deckOpen() && !active(); const distance = createMemo(() => { const ai = activeIndex(); const ci = index(); if (ci === ai) return -1; if (ci > ai) return ci - ai - 1; return props.cards.length - ai + ci - 1; }); const top = () => { if (active()) return 0; if (!deckOpen()) return focusHeight() + 44 + distance() * 8; return EXPANDED_H - TAB_OVERLAP + distance() * (TAB_H - TAB_OVERLAP); }; const zIndex = () => (active() ? 99 : 50 - distance()); return ( { if (active()) return; props.onActiveChange(card.id); setDeckOpen(false); }} height={active() ? (deckOpen() ? EXPANDED_H : focusHeight()) : TAB_H + 4} > {props.bodies[card.id]} ); }}
{props.version && v{props.version}}
); }