1import { createEffect, 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 isFanning?: boolean;14 celebratingCardId?: CardId | null;15 isPro?: boolean;16}1718const EXPANDED_H = 258;19const DEFAULT_FOCUS_H = 462;20const TAB_H = 46;21const TAB_OVERLAP = 14;2223export function StackDeck(props: StackDeckProps) {24 const [deckOpen, setDeckOpen] = createSignal(true);25 const [focusHeight, setFocusHeight] = createSignal(DEFAULT_FOCUS_H);26 let focusedAt = 0;27 let deckEl: HTMLDivElement | undefined;2829 function handleBack() {30 if (!deckOpen()) {31 setDeckOpen(true);32 } else {33 props.onBack();34 }35 }3637 createEffect(() => {38 if (props.isFanning) setDeckOpen(true);39 });4041 onMount(() => {42 const listener = () => handleBack();43 window.addEventListener("peach:settings-back", listener);4445 const updateFocusHeight = () => {46 if (!deckEl) return;47 setFocusHeight(deckEl.clientHeight || DEFAULT_FOCUS_H);48 };49 updateFocusHeight();50 const observer =51 typeof ResizeObserver === "undefined"52 ? undefined53 : new ResizeObserver(updateFocusHeight);54 if (deckEl) observer?.observe(deckEl);55 if (!observer) window.addEventListener("resize", updateFocusHeight);5657 onCleanup(() => {58 window.removeEventListener("peach:settings-back", listener);59 observer?.disconnect();60 if (!observer) window.removeEventListener("resize", updateFocusHeight);61 });62 });6364 const activeIndex = createMemo(() => {65 const idx = props.cards.findIndex((card) => card.id === props.active);66 return idx >= 0 ? idx : 0;67 });68 return (69 <section class="stk-shell" aria-label="Settings">70 <div class="stk-mast">71 <button72 class="stk-back"73 onClick={handleBack}74 aria-label="Back"75 type="button"76 >77 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">78 <path d="M6 1L1 6L6 11" />79 </svg>80 </button>81 <div class="stk-mast-title">Settings</div>82 <div class="stk-mast-meta">83 <span class="stk-mast-counter">{activeIndex() + 1} / {props.cards.length}</span>84 </div>85 </div>8687 <div88 ref={(el) => { deckEl = el; }}89 class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"} ${props.isFanning ? "is-fanning" : ""} ${props.isPro ? "is-pro" : ""}`}90 onWheel={(e) => {91 const cardEl = (e.target as HTMLElement).closest<HTMLElement>("[data-settings-card-id]");92 if (!cardEl || cardEl.dataset.active !== "true") return;93 if (deckOpen() && e.deltaY > 5) {94 95 setDeckOpen(false);96 focusedAt = Date.now();97 } else if (!deckOpen() && e.deltaY < -5 && Date.now() - focusedAt > 300) {98 99 100 const body = cardEl.querySelector<HTMLElement>(".stk-card-body");101 if (!body || body.scrollTop === 0) setDeckOpen(true);102 }103 }}104 >105 <For each={props.cards}>106 {(card, index) => {107 const active = () => card.id === props.active;108 const concealed = () => !deckOpen() && !active();109 const distance = createMemo(() => {110 const ai = activeIndex();111 const ci = index();112 if (ci === ai) return -1;113 if (ci > ai) return ci - ai - 1;114 return props.cards.length - ai + ci - 1;115 });116 const top = () => {117 if (active()) return 0;118 if (!deckOpen()) return focusHeight() + 44 + distance() * 8;119 return EXPANDED_H - TAB_OVERLAP + distance() * (TAB_H - TAB_OVERLAP);120 };121 const zIndex = () => (active() ? 99 : 50 - distance());122123 return (124 <StackCard125 spec={card}126 active={active()}127 focused={!deckOpen() && active()}128 concealed={concealed()}129 celebrating={!props.isFanning && props.celebratingCardId === card.id}130 top={top()}131 zIndex={zIndex()}132 onClick={() => {133 if (active()) return;134 props.onActiveChange(card.id);135 setDeckOpen(false);136 }}137 height={active() ? (deckOpen() ? EXPANDED_H : focusHeight()) : TAB_H + 4}138 >139 {props.bodies[card.id]}140 </StackCard>141 );142 }}143 </For>144 </div>145 {props.version && <span class="stk-version-corner">v{props.version}</span>}146 </section>147 );148}