StackDeck.tsx

3.7 KB · raw

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}