StackDeck.tsx

3.0 KB · raw

1import { createMemo, createSignal, For, Show } 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);20  const activeIndex = createMemo(() => {21    const idx = props.cards.findIndex((card) => card.id === props.active);22    return idx >= 0 ? idx : 0;23  });2425  return (26    <section class="stk-shell" aria-label="Settings">27      <div class="stk-mast">28        <button29          class="stk-back"30          onClick={props.onBack}31          aria-label="Back"32          type="button"33        >34          <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">35            <path d="M6 1L1 6L6 11" />36          </svg>37        </button>38        <div class="stk-mast-title">Settings</div>39        <div class="stk-mast-meta">40          {activeIndex() + 1} / {props.cards.length}41        </div>42      </div>4344      <div class={`stk-deck ${deckOpen() ? "is-open" : "is-focused"}`}>45        <For each={props.cards}>46          {(card, index) => {47            const active = () => card.id === props.active;48            const concealed = () => !deckOpen() && !active();49            const distance = createMemo(() => {50              const ai = activeIndex();51              const ci = index();52              if (ci === ai) return -1;53              if (ci > ai) return ci - ai - 1;54              return props.cards.length - ai + ci - 1;55            });56            const top = () => {57              if (active()) return 0;58              if (!deckOpen()) return FOCUS_H + 44 + distance() * 8;59              return EXPANDED_H + distance() * (TAB_H - TAB_OVERLAP);60            };61            const zIndex = () => (active() ? 99 : 50 - distance());6263            return (64              <StackCard65                spec={card}66                active={active()}67                focused={!deckOpen() && active()}68                concealed={concealed()}69                top={top()}70                zIndex={zIndex()}71                height={active() ? (deckOpen() ? EXPANDED_H : FOCUS_H) : TAB_H + 4}72                onClick={() => {73                  if (active()) return;74                  props.onActiveChange(card.id);75                  setDeckOpen(false);76                }}77              >78                {props.bodies[card.id]}79              </StackCard>80            );81          }}82        </For>83        <Show when={!deckOpen()}>84          <button85            class="stk-tray-handle"86            type="button"87            aria-label="Show settings sections"88            onClick={() => setDeckOpen(true)}89          >90            <span>sections</span>91          </button>92        </Show>93      </div>94    </section>95  );96}