1import { createMemo, createSignal, For, Show, 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}1213const EXPANDED_H = 258;14const FOCUS_H = 420;15const TAB_H = 46;16const TAB_OVERLAP = 14;1718export function StackDeck(props: StackDeckProps) {19 const [deckOpen, setDeckOpen] = createSignal(true);2021 function handleBack() {22 if (!deckOpen()) {23 setDeckOpen(true);24 } else {25 props.onBack();26 }27 }2829 onMount(() => {30 const listener = () => handleBack();31 window.addEventListener("peach:settings-back", listener);32 onCleanup(() => window.removeEventListener("peach:settings-back", listener));33 });3435 const activeIndex = createMemo(() => {36 const idx = props.cards.findIndex((card) => card.id === props.active);37 return idx >= 0 ? idx : 0;38 });3940 return (41 <section class="stk-shell" aria-label="Settings">42 <div class="stk-mast">43 <button44 class="stk-back"45 onClick={handleBack}46 aria-label="Back"47 type="button"48 >49 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">50 <path d="M6 1L1 6L6 11" />51 </svg>52 </button>53 <div class="stk-mast-title">Settings</div>54 <div class="stk-mast-meta">55 {activeIndex() + 1} / {props.cards.length}56 </div>57 </div>5859 <div60 class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"}`}61 onWheel={(e) => {62 const cardEl = (e.target as HTMLElement).closest<HTMLElement>("[data-settings-card-id]");63 if (!cardEl) return;64 const isActive = cardEl.dataset.active === "true";65 if (!isActive && deckOpen() && e.deltaY > 3) {66 props.onActiveChange(cardEl.dataset.settingsCardId as CardId);67 setDeckOpen(false);68 } else if (isActive && !deckOpen() && e.deltaY < -3) {69 const body = cardEl.querySelector<HTMLElement>(".stk-card-body");70 if (!body || body.scrollTop === 0) setDeckOpen(true);71 }72 }}73 >74 <For each={props.cards}>75 {(card, index) => {76 const active = () => card.id === props.active;77 const concealed = () => !deckOpen() && !active();78 const distance = createMemo(() => {79 const ai = activeIndex();80 const ci = index();81 if (ci === ai) return -1;82 if (ci > ai) return ci - ai - 1;83 return props.cards.length - ai + ci - 1;84 });85 const top = () => {86 if (active()) return 0;87 if (!deckOpen()) return FOCUS_H + 44 + distance() * 8;88 return EXPANDED_H - TAB_OVERLAP + distance() * (TAB_H - TAB_OVERLAP);89 };90 const zIndex = () => (active() ? 99 : 50 - distance());9192 return (93 <StackCard94 spec={card}95 active={active()}96 focused={!deckOpen() && active()}97 concealed={concealed()}98 top={top()}99 zIndex={zIndex()}100 height={active() ? (deckOpen() ? EXPANDED_H : FOCUS_H) : TAB_H + 4}101 onClick={() => {102 if (active()) return;103 props.onActiveChange(card.id);104 setDeckOpen(false);105 }}106 >107 {props.bodies[card.id]}108 </StackCard>109 );110 }}111 </For>112 </div>113 </section>114 );115}