1import { createMemo, createSignal, For, onMount, onCleanup } from "solid-js";2import { StackCard } from "./StackCard";3import type { CardId, StackBodies, StackCardSpec } from "./types";45interface StackDeckProps {6 active: CardId;7 onActiveChange: (id: CardId) => void;8 onBack: () => void;9 cards: readonly StackCardSpec[];10 bodies: StackBodies;11 12 version?: string;13}1415const EXPANDED_H = 258;16const FOCUS_H = 420;17const TAB_H = 46;18const TAB_OVERLAP = 14;1920export function StackDeck(props: StackDeckProps) {21 const [deckOpen, setDeckOpen] = createSignal(true);22 let focusedAt = 0;2324 function handleBack() {25 if (!deckOpen()) {26 setDeckOpen(true);27 } else {28 props.onBack();29 }30 }3132 onMount(() => {33 const listener = () => handleBack();34 window.addEventListener("peach:settings-back", listener);35 onCleanup(() => window.removeEventListener("peach:settings-back", listener));36 });3738 const activeIndex = createMemo(() => {39 const idx = props.cards.findIndex((card) => card.id === props.active);40 return idx >= 0 ? idx : 0;41 });4243 return (44 <section class="stk-shell" aria-label="Settings">45 <div class="stk-mast">46 <button47 class="stk-back"48 onClick={handleBack}49 aria-label="Back"50 type="button"51 >52 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">53 <path d="M6 1L1 6L6 11" />54 </svg>55 </button>56 <div class="stk-mast-title">Settings</div>57 <div class="stk-mast-meta">58 <span class="stk-mast-counter">{activeIndex() + 1} / {props.cards.length}</span>59 {props.version && <span class="stk-mast-version">v{props.version}</span>}60 </div>61 </div>6263 <div64 class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"}`}65 onWheel={(e) => {66 const cardEl = (e.target as HTMLElement).closest<HTMLElement>("[data-settings-card-id]");67 if (!cardEl || cardEl.dataset.active !== "true") return;68 if (deckOpen() && e.deltaY > 5) {69 70 setDeckOpen(false);71 focusedAt = Date.now();72 } else if (!deckOpen() && e.deltaY < -5 && Date.now() - focusedAt > 300) {73 74 75 const body = cardEl.querySelector<HTMLElement>(".stk-card-body");76 if (!body || body.scrollTop === 0) setDeckOpen(true);77 }78 }}79 >80 <For each={props.cards}>81 {(card, index) => {82 const active = () => card.id === props.active;83 const concealed = () => !deckOpen() && !active();84 const distance = createMemo(() => {85 const ai = activeIndex();86 const ci = index();87 if (ci === ai) return -1;88 if (ci > ai) return ci - ai - 1;89 return props.cards.length - ai + ci - 1;90 });91 const top = () => {92 if (active()) return 0;93 if (!deckOpen()) return FOCUS_H + 44 + distance() * 8;94 return EXPANDED_H - TAB_OVERLAP + distance() * (TAB_H - TAB_OVERLAP);95 };96 const zIndex = () => (active() ? 99 : 50 - distance());9798 return (99 <StackCard100 spec={card}101 active={active()}102 focused={!deckOpen() && active()}103 concealed={concealed()}104 top={top()}105 zIndex={zIndex()}106 height={active() ? (deckOpen() ? EXPANDED_H : FOCUS_H) : TAB_H + 4}107 onClick={() => {108 if (active()) return;109 props.onActiveChange(card.id);110 setDeckOpen(false);111 }}112 >113 {props.bodies[card.id]}114 </StackCard>115 );116 }}117 </For>118 </div>119 </section>120 );121}