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 = 248;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 <div class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"}`}>60 <For each={props.cards}>61 {(card, index) => {62 const active = () => card.id === props.active;63 const concealed = () => !deckOpen() && !active();64 const distance = createMemo(() => {65 const ai = activeIndex();66 const ci = index();67 if (ci === ai) return -1;68 if (ci > ai) return ci - ai - 1;69 return props.cards.length - ai + ci - 1;70 });71 const top = () => {72 if (active()) return 0;73 if (!deckOpen()) return FOCUS_H + 44 + distance() * 8;74 return EXPANDED_H - TAB_OVERLAP + distance() * (TAB_H - TAB_OVERLAP);75 };76 const zIndex = () => (active() ? 99 : 50 - distance());7778 return (79 <StackCard80 spec={card}81 active={active()}82 focused={!deckOpen() && active()}83 concealed={concealed()}84 top={top()}85 zIndex={zIndex()}86 height={active() ? (deckOpen() ? EXPANDED_H : FOCUS_H) : TAB_H + 4}87 onClick={() => {88 if (active()) return;89 props.onActiveChange(card.id);90 setDeckOpen(false);91 }}92 onScrollToTop={active() && !deckOpen() ? () => setDeckOpen(true) : undefined}93 >94 {props.bodies[card.id]}95 </StackCard>96 );97 }}98 </For>99 </div>100 </section>101 );102}