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);20 let focusedAt = 0;2122 function handleBack() {23 if (!deckOpen()) {24 setDeckOpen(true);25 } else {26 props.onBack();27 }28 }2930 onMount(() => {31 const listener = () => handleBack();32 window.addEventListener("peach:settings-back", listener);33 onCleanup(() => window.removeEventListener("peach:settings-back", listener));34 });3536 const activeIndex = createMemo(() => {37 const idx = props.cards.findIndex((card) => card.id === props.active);38 return idx >= 0 ? idx : 0;39 });4041 return (42 <section class="stk-shell" aria-label="Settings">43 <div class="stk-mast">44 <button45 class="stk-back"46 onClick={handleBack}47 aria-label="Back"48 type="button"49 >50 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">51 <path d="M6 1L1 6L6 11" />52 </svg>53 </button>54 <div class="stk-mast-title">Settings</div>55 <div class="stk-mast-meta">56 {activeIndex() + 1} / {props.cards.length}57 </div>58 </div>5960 <div61 class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"}`}62 onWheel={(e) => {63 const cardEl = (e.target as HTMLElement).closest<HTMLElement>("[data-settings-card-id]");64 if (!cardEl || cardEl.dataset.active !== "true") return;65 if (deckOpen() && e.deltaY > 5) {66 67 setDeckOpen(false);68 focusedAt = Date.now();69 } else if (!deckOpen() && e.deltaY < -5 && Date.now() - focusedAt > 300) {70 71 72 const body = cardEl.querySelector<HTMLElement>(".stk-card-body");73 if (!body || body.scrollTop === 0) setDeckOpen(true);74 }75 }}76 >77 <For each={props.cards}>78 {(card, index) => {79 const active = () => card.id === props.active;80 const concealed = () => !deckOpen() && !active();81 const distance = createMemo(() => {82 const ai = activeIndex();83 const ci = index();84 if (ci === ai) return -1;85 if (ci > ai) return ci - ai - 1;86 return props.cards.length - ai + ci - 1;87 });88 const top = () => {89 if (active()) return 0;90 if (!deckOpen()) return FOCUS_H + 44 + distance() * 8;91 return EXPANDED_H - TAB_OVERLAP + distance() * (TAB_H - TAB_OVERLAP);92 };93 const zIndex = () => (active() ? 99 : 50 - distance());9495 return (96 <StackCard97 spec={card}98 active={active()}99 focused={!deckOpen() && active()}100 concealed={concealed()}101 top={top()}102 zIndex={zIndex()}103 height={active() ? (deckOpen() ? EXPANDED_H : FOCUS_H) : TAB_H + 4}104 onClick={() => {105 if (active()) return;106 props.onActiveChange(card.id);107 setDeckOpen(false);108 }}109 >110 {props.bodies[card.id]}111 </StackCard>112 );113 }}114 </For>115 </div>116 </section>117 );118}