// /js/components — the whole Solid kit, on one page.
//
// It used to be three pages ("Components", "Forms", "AutoTable"), which is a split along
// the lines of the SOURCE FILES rather than along anything a reader wants: a person
// looking for a date picker does not know, and should not have to guess, whether it was
// filed under forms or under overlays. So: one page, one scroll, and a sidebar of groups
// that jumps you to the one you are after.
//
// The section ids come from componentGroups.ts, which is also what the sidebar renders —
// so the sidebar cannot offer a jump to a section that does not exist.

import { createSignal, For, JSXElement } from "solid-js";

import {
    ButtonUI,
    ButtonLink,
    ButtonLinkRed,
    BackLink,
    SegmentedButtons,
    BUTTON_COLOR_PRIMARY,
    BUTTON_COLOR_NEUTRAL,
    BUTTON_COLOR_WHITE,
    BUTTON_COLOR_LIGHT_NEUTRAL,
    BUTTON_COLOR_BLUE,
    BUTTON_COLOR_GREEN,
    BUTTON_COLOR_RED,
} from "@ui/Buttons";
import { Badge, BADGE_GREEN, BADGE_RED, BADGE_BLUE, BADGE_AMBER, BADGE_NEUTRAL, BADGE_MUTED } from "@ui/Badges";
import { AlertWhite, AlertGray, AlertBlue, AlertGreen, AlertRed, AlertYellow } from "@ui/Alerts";
import { EnvBadge } from "@ui/EnvBadge";
import { Card, BorderCard, BorderCutCornerCard, CardHeader, CardSubheader } from "@ui/Cards";
import { Divider, CodeBox, PageHeader, PageLink, Loader, Breadcrumbs, ManagerPageHeader } from "@ui/General";
import { Icon, IconInline, IconSuccess, IconError, IconContainer } from "@ui/Icons";
import {
    FormInput,
    FormLabel,
    FormSelect,
    FormTextarea,
    FormFieldset,
    FormFileInput,
    FormEmailInput,
    FormNumberInput,
    FormCurrencyInput,
    FormPercentInput,
    FormPhoneInput,
    FormZipCodeInput,
    FormCombobox,
    FormMultiSelect,
    FormMultiSelectTrigger,
    FormAsyncCombobox,
    FormStateSelector,
    FormTimezoneSelector,
    FormSignaturePad,
    US_STATES,
} from "@ui/Forms";
import { ToggleSwitch } from "@ui/ToggleSwitch";
import { isEmailValid } from "@ui/Validation";
import { Calendar } from "@ui/Calendar";
import { DatePicker, DateOfBirthPicker } from "@ui/DatePicker";
import { PrettyTable } from "@ui/PrettyTable";
import { CellGrid } from "@ui/CellGrid";
import AutoTable, {
    AutoTableColumn,
    AutoTableSearch,
    AutoTableFilterFields,
    TdLeft,
    TdRight,
    TdCenter,
    COL_POS_LEFT,
    COL_POS_RIGHT,
    COL_POS_CENTER,
    AUTOTABLE_SIZE_COMPACT,
} from "@ui/AutoTable";
import { Modal, ConfirmModal, WizardModal } from "@ui/Modal";
import { Tooltip } from "@ui/Tooltips";
import { Popover, PopoverTrigger, PopoverContent, HoverPopover, HoverPopoverTrigger, HoverPopoverContent } from "@ui/Popovers";
import { Menu, MenuTrigger, MenuContent, MenuItem, MenuDivider, Submenu } from "@ui/Menu";
import { ToastProvider, useToast } from "@ui/Toast";
import { TutorialProvider, StartTutorialButton, TutorialStep } from "@ui/Tutorial";
import { createRemoteFlash, RemoteUpdateFlash } from "@ui/RemoteUpdateFlash";
import { TabGroup } from "@ui/Tabs";
import { CrmTabGroup, CrmSubTabGroup } from "@ui/CrmTabs";
import { Accordion, SingleAccordion } from "@ui/Accordion";
import { SidebarNav } from "@ui/Sidebar";
import { FuzzyMatch } from "@ui/FuzzyMatch";
import { Chart, ChartSeries } from "@ui/Chart";
import { USHeatmap } from "@ui/USHeatmap";
import { ThemeToggle, useTheme } from "@ui/Theme";

import { Demo } from "../layout/Demo.tsx";

// ---- page scaffolding ----------------------------------------------------

// Section is one group. The id is what the sidebar jumps to and what the tour targets;
// scroll-mt-24 keeps the heading clear of the sticky nav once it gets there.
function Section(props: { id: string; title: string; children?: JSXElement }) {
    return (
        <section id={props.id} class="mt-12 scroll-mt-24">
            <h2 class="text-xl font-semibold tracking-tight text-ink">{props.title}</h2>
            {props.children}
        </section>
    );
}

function Prose(props: { children?: JSXElement }) {
    return <p class="mt-3 leading-relaxed text-ink-soft">{props.children}</p>;
}

// Panel is a live demo with no source alongside it — for the components whose usage is
// obvious from looking at them. Demo (with code) is for the ones where it is not.
function Panel(props: { title: string; children?: JSXElement }) {
    return (
        <div class="mt-4 rounded-default border border-line bg-surface shadow-xs">
            <div class="border-b border-line px-4 py-2">
                <span class="text-ss text-ink-muted">{props.title}</span>
            </div>
            <div class="p-4">{props.children}</div>
        </div>
    );
}

function Field(props: { label: string; children?: JSXElement }) {
    return (
        <div class="flex flex-col gap-1">
            <FormLabel>{props.label}</FormLabel>
            {props.children}
        </div>
    );
}

const TOUR_STEPS: TutorialStep[] = [
    { title: "Buttons", target: "#buttons", content: "The presentational end: props in, an element out." },
    { title: "Overlays", target: "#overlays", content: "Measured against the real viewport, portaled out of the tree." },
    { title: "Tables", target: "#tables", content: "Filter, sort, calculate, export — from a column list." },
    { title: "That's the tour", content: "Escape ends it. Arrow keys and Enter move between steps." },
];

export function Components() {
    // ToastProvider and TutorialProvider are the only two components in this kit that are
    // PROVIDERS: useToast() and useTutorial() throw outside them. Everything else —
    // modals, popovers, menus, the date picker — self-portals to document.body and needs
    // no host mounted anywhere.
    return (
        <ToastProvider position="bottom-right">
            <TutorialProvider steps={TOUR_STEPS}>
                <Body />
            </TutorialProvider>
        </ToastProvider>
    );
}

function Body() {
    return (
        <div>
            <div class="border-b border-line pb-6">
                <p class="text-ss font-semibold uppercase tracking-widest text-primary">Kjøl JS Web</p>
                <h1 class="mt-2 text-3xl font-semibold tracking-tight text-ink">Components</h1>
                <p class="mt-3 leading-relaxed text-ink-muted">
                    Every component in the Solid kit, running. Not a screenshot of one anywhere: each block
                    below is the real component, imported from <code class="font-mono">@ui/*</code> and rendered
                    on this page. Use the sidebar to jump to a group.
                </p>
            </div>

            <Section id="using" title="Using a component">
                <Prose>
                    They are ordinary Solid components: named exports, props in, an element out. Two things
                    catch people out. Handlers keep their DOM names —{" "}
                    <code class="font-mono">onclick</code>, <code class="font-mono">oninput</code>,{" "}
                    <code class="font-mono">onchange</code>, never <code class="font-mono">onClick</code>. And
                    many props accept a signal <em>getter</em> as well as a value, so you can pass{" "}
                    <code class="font-mono">value={"{"}name{"}"}</code> rather than{" "}
                    <code class="font-mono">value={"{"}name(){"}"}</code> and let the component track it.
                </Prose>
                <div class="mt-4 flex gap-2">
                    <StartTutorialButton>Take the tour</StartTutorialButton>
                </div>
            </Section>

            <Buttons />
            <Badges />
            <CardsAndLayout />
            <Icons />
            <Forms />
            <Selects />
            <Toggles />
            <Dates />
            <Tables />
            <Overlays />
            <Feedback />
            <Navigation />
            <Search />
            <Charts />
            <Theming />
        </div>
    );
}

// ---- buttons -------------------------------------------------------------

function Buttons() {
    const [clicks, setClicks] = createSignal(0);
    const [span, setSpan] = createSignal("week");

    return (
        <Section id="buttons" title="Buttons">
            <Prose>
                Colour, outline, size and an optional icon. A button with an icon and no text gets square
                padding rather than the wide pill a text button gets — so an icon button is a square, not a
                lozenge with a picture rattling around in it.
            </Prose>

            <Panel title="Colours">
                <div class="flex flex-wrap items-center gap-2">
                    <ButtonUI color={BUTTON_COLOR_PRIMARY}>Primary</ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_GREEN}>Green</ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_RED}>Red</ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_BLUE}>Blue</ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_NEUTRAL}>Neutral</ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_WHITE}>White</ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_LIGHT_NEUTRAL}>Light</ButtonUI>
                </div>
            </Panel>

            <Panel title="Outline, size, icon, disabled">
                <div class="flex flex-wrap items-center gap-2">
                    <ButtonUI color={BUTTON_COLOR_PRIMARY} outline>Outline</ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_RED} outline>Danger</ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_GREEN} small icon="check">
                        <Icon icon="check" size={12} /> Small + icon
                    </ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_NEUTRAL} icon="plus">
                        <Icon icon="plus" size={14} />
                    </ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_PRIMARY} disabled>Disabled</ButtonUI>
                </div>
            </Panel>

            <Demo
                title="Click it"
                code={`<ButtonUI color={BUTTON_COLOR_PRIMARY} onclick={() => setClicks(clicks() + 1)}>
    Clicked {clicks()} times
</ButtonUI>

// onclick, not onClick. These are DOM handler names.`}
            >
                <div class="flex flex-wrap items-center gap-3">
                    <ButtonUI color={BUTTON_COLOR_PRIMARY} onclick={() => setClicks(clicks() + 1)}>
                        Clicked {clicks()} times
                    </ButtonUI>
                    <ButtonLink onclick={() => setClicks(0)}>Reset (a link that is a button)</ButtonLink>
                    <ButtonLinkRed onclick={() => setClicks(0)}>And a destructive one</ButtonLinkRed>
                    <BackLink href="#using" text="Back link" />
                </div>
            </Demo>

            <Panel title={"Segmented control — selected: " + span()}>
                <SegmentedButtons
                    class="max-w-xs"
                    options={[
                        { value: "day", label: "Day" },
                        { value: "week", label: "Week" },
                        { value: "month", label: "Month" },
                    ]}
                    value={span}
                    onchange={setSpan}
                />
            </Panel>
        </Section>
    );
}

// ---- badges & alerts -----------------------------------------------------

function Badges() {
    return (
        <Section id="badges" title="Badges & alerts">
            <Prose>
                A badge is a solid fill with white text, so it stays legible in both themes without a variant
                on it. An alert is the opposite: a wash of colour behind dark text, which is exactly the case
                a token cannot carry into dark mode — a red-50 tint is invisible on a near-black surface — so
                alerts are one of only two places in the kit with a <code class="font-mono">dark:</code> rule.
            </Prose>

            <Panel title="Badges">
                <div class="flex flex-wrap items-center gap-2">
                    <Badge color={BADGE_GREEN} pill>active</Badge>
                    <Badge color={BADGE_RED} pill>failed</Badge>
                    <Badge color={BADGE_BLUE}>info</Badge>
                    <Badge color={BADGE_AMBER} pill>pending</Badge>
                    <Badge color={BADGE_NEUTRAL}>default</Badge>
                    <Badge color={BADGE_MUTED}>muted</Badge>
                </div>
            </Panel>

            <Panel title="Alerts">
                <div class="space-y-3">
                    <AlertBlue header="Heads up">An informational message with a header.</AlertBlue>
                    <AlertGreen>A success alert, with no header.</AlertGreen>
                    <AlertYellow header="Warning">Something needs your attention.</AlertYellow>
                    <AlertRed header="Error">Something went wrong.</AlertRed>
                    <AlertGray>And a neutral one.</AlertGray>
                    <AlertWhite header="White">On the plain surface.</AlertWhite>
                </div>
            </Panel>

            <Panel title="Environment badge">
                {/* It is absolutely positioned (top-0 -right-2), so it needs a relative
                    ancestor to hang off — normally the logo. And it renders NOTHING in
                    production, which is the whole point of it: a badge that is always
                    there tells you nothing. */}
                <div class="relative inline-block rounded-default border border-line px-4 py-2">
                    <span class="text-sm font-semibold text-ink">kjol</span>
                    <EnvBadge />
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    Nothing beside the wordmark? Then this build is production — which is what it is telling
                    you.
                </p>
            </Panel>
        </Section>
    );
}

// ---- cards & layout ------------------------------------------------------

function CardsAndLayout() {
    return (
        <Section id="cards" title="Cards & layout">
            <Prose>
                The structural furniture: cards, headings, dividers, crumb trails, a spinner, a code box.
                Nothing here holds state — they are containers, and they take their children.
            </Prose>

            <Panel title="Cards">
                <div class="grid gap-4 sm:grid-cols-3">
                    <Card>
                        <CardHeader>Card</CardHeader>
                        <p class="text-sm text-ink-soft">Padded, and grows to fill its row.</p>
                    </Card>
                    <BorderCard>
                        <CardSubheader>BorderCard</CardSubheader>
                        <p class="text-sm text-ink-soft">A border instead of a shadow.</p>
                    </BorderCard>
                    <BorderCutCornerCard>
                        <CardSubheader>Cut corner</CardSubheader>
                        <p class="text-sm text-ink-soft">The same, with a clipped corner.</p>
                    </BorderCutCornerCard>
                </div>
            </Panel>

            <Panel title="Headers, dividers, crumbs">
                <Breadcrumbs
                    items={[
                        { url: "/", displayText: "kjol" },
                        { url: "/js", displayText: "Kjøl JS Web" },
                        { url: "/js/components", displayText: "Components" },
                    ]}
                />
                <PageHeader text="A page header" />
                <Divider />
                <ManagerPageHeader
                    title="With a description and an action"
                    description="The three-part page heading."
                    action={<ButtonUI color={BUTTON_COLOR_PRIMARY} small>New</ButtonUI>}
                />
            </Panel>

            <Panel title="Loader, code box, links">
                <div class="flex flex-wrap items-center gap-6">
                    <Loader />
                    <PageLink href="https://solidjs.com" newTab class="text-primary underline underline-offset-4">
                        An external link (new tab)
                    </PageLink>
                </div>
                <CodeBox class="mt-3" code={"go run ./server -build\ngo run ./server"} />
            </Panel>
        </Section>
    );
}

// ---- icons ---------------------------------------------------------------

const ICON_NAMES = [
    "check", "xmark", "plus", "pen-to-square", "trash-can", "download", "print", "calendar",
    "envelope", "globe", "user", "house", "table", "code", "star", "bell", "sun", "moon",
    "magnifying-glass", "circle-info", "circle-check", "circle-exclamation", "triangle-exclamation",
    "chevron-down", "bars", "copy", "sliders", "palette", "cog", "server", "bolt", "cube",
    "chart-column", "table-columns", "list-ol", "grip-vertical", "phone", "arrow-right",
];

function Icons() {
    return (
        <Section id="icons" title="Icons">
            <Prose>
                FontAwesome, tree-shaken. The bundler scans the source for the names actually referenced,
                pulls just those paths out of the SVG kit, and emits a registry containing only them —
                which is why this site ships a few dozen glyphs rather than FontAwesome's 41.5 MB kit.
            </Prose>
            <Prose>
                A name nothing in the source references is not bundled; a name the kit has never heard of
                renders an empty box and is reported by the bundler rather than shipping silently.
            </Prose>

            <Panel title="The registry">
                <div class="grid grid-cols-4 gap-2 sm:grid-cols-8">
                    <For each={ICON_NAMES}>
                        {(name) => (
                            <div class="flex flex-col items-center gap-1.5 rounded-default border border-line px-2 py-3">
                                <Icon icon={name} size={18} class="text-ink-soft" />
                                <span class="font-mono text-[10px] text-ink-faint">{name}</span>
                            </div>
                        )}
                    </For>
                </div>
            </Panel>

            <Panel title="Variants">
                <div class="flex flex-wrap items-center gap-5">
                    <IconContainer>
                        <Icon icon="check" size={16} />
                        <span class="text-sm text-ink-soft">IconContainer</span>
                    </IconContainer>
                    <IconSuccess icon="circle-check" size={18} />
                    <IconError icon="circle-exclamation" size={18} />
                    <span class="inline-flex items-center gap-1.5 text-sm text-ink-soft">
                        <IconInline icon="bolt" size={14} /> IconInline sits on the text baseline
                    </span>
                    <Icon icon="star" size={18} solid />
                    <Icon icon="star" size={18} solid={false} />
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    The last two are the same name in the solid and regular styles. Which one you get by
                    default is a CSS variable the app sets, read at runtime.
                </p>
            </Panel>
        </Section>
    );
}

// ---- forms ---------------------------------------------------------------

function Forms() {
    const [name, setName] = createSignal("");
    const [email, setEmail] = createSignal("");
    const [notes, setNotes] = createSignal("");
    const [plan, setPlan] = createSignal("pro");

    // Derived, not stored — so it cannot fall out of step with the field it describes.
    // Blank is not "invalid", it is unfilled.
    const emailError = () => (email() && !isEmailValid(email()) ? "That is not an email address." : "");

    return (
        <Section id="forms" title="Forms & inputs">
            <Prose>
                Inputs are controlled: the value goes in as a prop, the change comes out as a callback, and
                the caller owns the state. The masked fields carry their behaviour inside the component — a
                currency field will not let you type a letter into it — which is the only reason it is the
                same in every app.
            </Prose>

            <Demo
                title="Text, email, textarea, select"
                code={`const emailError = () =>
    email() && !isEmailValid(email()) ? "That is not an email address." : "";

<FormEmailInput
    value={email()}
    oninput={(e) => setEmail(e.currentTarget.value)}
    error={emailError()}
    showIcon
/>`}
            >
                <div class="grid gap-4 sm:grid-cols-2">
                    <Field label="Name">
                        <FormInput
                            placeholder="Ada Lovelace"
                            value={name()}
                            oninput={(e) => setName(e.currentTarget.value)}
                        />
                    </Field>
                    <Field label="Email">
                        <FormEmailInput
                            placeholder="ada@example.com"
                            value={email()}
                            oninput={(e) => setEmail(e.currentTarget.value)}
                            error={emailError()}
                            showIcon
                        />
                    </Field>
                    <Field label="Plan">
                        <FormSelect value={plan()} onchange={(e) => setPlan(e.currentTarget.value)}>
                            <option value="free">Free</option>
                            <option value="pro">Pro</option>
                            <option value="enterprise">Enterprise</option>
                        </FormSelect>
                    </Field>
                    <Field label="Notes">
                        <FormTextarea
                            rows={3}
                            placeholder="Anything worth remembering…"
                            value={notes()}
                            oninput={(e) => setNotes(e.currentTarget.value)}
                        />
                    </Field>
                </div>
            </Demo>

            <Panel title="Masked inputs — try typing letters into them">
                <div class="grid gap-4 sm:grid-cols-3">
                    <Field label="Currency">
                        <FormCurrencyInput />
                    </Field>
                    <Field label="Percent">
                        <FormPercentInput />
                    </Field>
                    <Field label="Phone">
                        <FormPhoneInput />
                    </Field>
                    <Field label="Zip">
                        <FormZipCodeInput />
                    </Field>
                    <Field label="Whole number">
                        <FormNumberInput int unsigned placeholder="0" />
                    </Field>
                    <Field label="Attachment">
                        <FormFileInput />
                    </Field>
                </div>
            </Panel>

            <Panel title="Fieldset">
                <FormFieldset legend="Account">
                    <div class="grid gap-4 sm:grid-cols-2">
                        <Field label="Name">
                            <FormInput value={name()} oninput={(e) => setName(e.currentTarget.value)} />
                        </Field>
                        <Field label="Email (validated)">
                            <FormEmailInput
                                value={email()}
                                oninput={(e) => setEmail(e.currentTarget.value)}
                                error={emailError()}
                                showIcon
                            />
                        </Field>
                    </div>
                </FormFieldset>
            </Panel>
        </Section>
    );
}

// ---- selects & comboboxes ------------------------------------------------

const LANGUAGES = [
    { value: "go", label: "Go" },
    { value: "rust", label: "Rust" },
    { value: "ts", label: "TypeScript" },
    { value: "python", label: "Python" },
    { value: "kotlin", label: "Kotlin" },
    { value: "swift", label: "Swift" },
];

const PEOPLE = [
    { value: "ada@example.com", label: "Ada Lovelace" },
    { value: "alan@example.com", label: "Alan Turing" },
    { value: "grace@example.com", label: "Grace Hopper" },
    { value: "katherine@example.com", label: "Katherine Johnson" },
    { value: "margaret@example.com", label: "Margaret Hamilton" },
    { value: "radia@example.com", label: "Radia Perlman" },
];

function Selects() {
    const [one, setOne] = createSignal("");
    const [many, setMany] = createSignal<string[]>(["go"]);
    const [tags, setTags] = createSignal<string[]>(["go"]);
    const [state, setState] = createSignal("");
    const [tz, setTz] = createSignal("");
    const [picked, setPicked] = createSignal("");

    return (
        <Section id="selects" title="Selects & comboboxes">
            <Prose>
                A combobox picks one; a multi-select picks several, with checkboxes on the rows and removable
                pills in the field. Past three selections — or once the pills stop fitting — the field
                collapses to "N items selected" rather than growing until it wraps.
            </Prose>

            <Panel title="Combobox, multi-select, and the built-in selectors">
                <div class="grid gap-4 sm:grid-cols-2">
                    <Field label="Language (searchable, one)">
                        <FormCombobox
                            options={LANGUAGES}
                            value={one()}
                            onchange={setOne}
                            searchable
                            placeholder="Pick one"
                        />
                    </Field>
                    <Field label="Languages (several)">
                        <FormMultiSelect
                            options={LANGUAGES}
                            value={many}
                            onchange={setMany}
                            searchable
                            showSelectAll
                            placeholder="Pick a few"
                        />
                    </Field>
                    <Field label="State">
                        <FormCombobox options={US_STATES} value={state()} onchange={setState} searchable />
                    </Field>
                    <Field label="Timezone">
                        <FormTimezoneSelector value={tz()} onchange={(e: any) => setTz(e?.currentTarget?.value ?? e)} />
                    </Field>
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    one: <span class="font-mono text-ink">{one() || "—"}</span> · several:{" "}
                    <span class="font-mono text-ink">{many().join(", ") || "—"}</span>
                </p>
            </Panel>

            <Panel title={"Async combobox — picked: " + (picked() || "nothing yet")}>
                <div class="max-w-sm">
                    {/* The search is the CALLER's. The component knows how to debounce, order
                        and render; it knows nothing about where options come from. Here it is a
                        local array; in an app it would be a fetch. */}
                    <FormAsyncCombobox
                        options={[]}
                        placeholder="Search people…"
                        minChars={2}
                        onSearch={(q) =>
                            PEOPLE.filter((p) => p.label.toLowerCase().includes(q.toLowerCase()))
                        }
                        onSelect={(value, option) => setPicked(option.label + " <" + value + ">")}
                    />
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    Two characters before it asks; 200 ms after you stop typing. A response for a query you
                    have already typed past is DISCARDED rather than shown — which is the whole bug with
                    hand-rolled autocompletes.
                </p>
            </Panel>

            <Panel title="Multi-select behind your own trigger">
                <FormMultiSelectTrigger
                    trigger={
                        <ButtonUI color={BUTTON_COLOR_LIGHT_NEUTRAL} small>
                            <Icon icon="sliders" size={12} /> Tags ({tags().length})
                        </ButtonUI>
                    }
                    options={LANGUAGES}
                    value={tags}
                    onchange={setTags}
                    searchable
                    showSelectAll
                />
                <p class="mt-3 text-ss text-ink-muted">
                    Same selection model as the field above; only the thing you click on differs.
                </p>
            </Panel>
        </Section>
    );
}

// ---- toggles & signature -------------------------------------------------

function Toggles() {
    const [notify, setNotify] = createSignal(true);
    const [locked, setLocked] = createSignal(false);
    const [signed, setSigned] = createSignal("");

    return (
        <Section id="toggles" title="Toggles & signature">
            <Prose>
                A toggle is a checkbox that admits what it is — there is no{" "}
                <code class="font-mono">FormCheckbox</code> in this kit, and that is deliberate. The
                signature pad hands you back the drawing as an SVG string.
            </Prose>

            <Panel title="Toggles">
                <div class="flex flex-col gap-4">
                    <ToggleSwitch
                        checked={notify}
                        onchange={setNotify}
                        label="Email notifications"
                        description="At most one message a day."
                    />
                    <ToggleSwitch
                        checked={locked}
                        onchange={setLocked}
                        label="Locked"
                        description="This one is disabled."
                        disabled
                    />
                </div>
            </Panel>

            <Panel title={"Signature pad — " + signed().length + " bytes of SVG"}>
                <FormSignaturePad onchange={setSigned} />
                <p class="mt-3 text-ss text-ink-muted">
                    Draw in it. Clearing it emits an empty string, so "did they sign?" is just a length check.
                </p>
            </Panel>
        </Section>
    );
}

// ---- dates ---------------------------------------------------------------

function Dates() {
    const [date, setDate] = createSignal("");
    const [dob, setDob] = createSignal("");
    const [day, setDay] = createSignal("");

    return (
        <Section id="dates" title="Dates">
            <Prose>
                The field is typeable, not merely clickable, and it parses loosely. A picker you can only
                click is a picker that is slower than the keyboard for everyone who already knows the date.
            </Prose>

            <Panel title={"Pickers — picked: " + (date() || "nothing")}>
                <div class="grid gap-4 sm:grid-cols-2">
                    <Field label="Date (portaled; flips near the bottom)">
                        <DatePicker value={date()} onchange={setDate} clearable placeholder="Pick a date" />
                    </Field>
                    <Field label="Date of birth (inline, month/year selects)">
                        <DateOfBirthPicker value={dob()} onchange={setDob} />
                    </Field>
                </div>
            </Panel>

            <Panel title={"The calendar on its own — selected: " + (day() || "none")}>
                <div class="max-w-xs">
                    <Calendar selected={day()} onSelect={setDay} />
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    The same grid the picker drops down, usable directly when you want it inline. Pass{" "}
                    <code class="font-mono">variant="month"</code> for the big version.
                </p>
            </Panel>
        </Section>
    );
}

// ---- tables --------------------------------------------------------------

interface Institution {
    name: string;
    state: string;
    term: string;
    rate: number;
    minimum: number;
    status: "open" | "closed" | "waitlist";
}

const ROWS: Institution[] = [
    { name: "First Meridian Bank", state: "CA", term: "90 day", rate: 4.85, minimum: 1000, status: "open" },
    { name: "Harborline Credit Union", state: "WA", term: "180 day", rate: 5.1, minimum: 2500, status: "open" },
    { name: "Cascade Federal", state: "OR", term: "1 year", rate: 5.35, minimum: 500, status: "waitlist" },
    { name: "Ironwood Savings", state: "IL", term: "90 day", rate: 4.6, minimum: 10000, status: "closed" },
    { name: "Great Lakes Trust", state: "MI", term: "2 year", rate: 5.55, minimum: 1000, status: "open" },
    { name: "Sunbelt National", state: "TX", term: "180 day", rate: 4.95, minimum: 5000, status: "open" },
    { name: "Granite State Bank", state: "NH", term: "1 year", rate: 5.2, minimum: 2000, status: "waitlist" },
    { name: "Pacific Crest", state: "CA", term: "5 year", rate: 5.75, minimum: 25000, status: "open" },
    { name: "Copper Ridge Bank", state: "AZ", term: "90 day", rate: 4.4, minimum: 1000, status: "closed" },
    { name: "Bayou Community", state: "LA", term: "1 year", rate: 5.05, minimum: 1500, status: "open" },
    { name: "Northern Pine FCU", state: "MN", term: "2 year", rate: 5.45, minimum: 500, status: "open" },
    { name: "Chesapeake First", state: "MD", term: "180 day", rate: 4.75, minimum: 3000, status: "waitlist" },
];

// The whole AutoTable is this list. Sorting, column ordering, hiding, resizing and CSV
// export are all driven from it — there is no per-column wiring anywhere else.
const COLUMNS: AutoTableColumn[] = [
    { displayName: "Institution", sortable: true, sortIdentifier: "name", displayPosition: COL_POS_LEFT },
    { displayName: "State", sortable: true, sortIdentifier: "state", displayPosition: COL_POS_CENTER, toggleable: true },
    { displayName: "Term", sortable: true, sortIdentifier: "term", displayPosition: COL_POS_LEFT },
    {
        displayName: "Rate", sortable: true, sortIdentifier: "rate", sortType: "numeric",
        displayPosition: COL_POS_RIGHT, csvValue: (i: Institution) => i.rate,
    },
    {
        displayName: "Minimum", sortable: true, sortIdentifier: "minimum", sortType: "money",
        displayPosition: COL_POS_RIGHT, toggleable: true, csvValue: (i: Institution) => i.minimum,
    },
    { displayName: "Status", sortable: true, sortIdentifier: "status", displayPosition: COL_POS_CENTER },
];

const money = (n: number) => "$" + n.toLocaleString("en-US");

function StatusBadge(props: { status: Institution["status"] }) {
    if (props.status === "open") return <Badge color={BADGE_GREEN} pill>open</Badge>;
    if (props.status === "closed") return <Badge color={BADGE_RED} pill>closed</Badge>;
    return <Badge color={BADGE_NEUTRAL} pill>waitlist</Badge>;
}

const GRID_SEED = [
    { id: "1", sku: "KJ-100", qty: 12, price: 4.5 },
    { id: "2", sku: "KJ-220", qty: 3, price: 18 },
    { id: "3", sku: "KJ-330", qty: 47, price: 1.25 },
];

function Tables() {
    const [gridRows, setGridRows] = createSignal(GRID_SEED.map((r) => ({ ...r })));
    const [sortKey, setSortKey] = createSignal<string | null>("sku");
    const [sortDesc, setSortDesc] = createSignal(false);

    return (
        <Section id="tables" title="Tables">
            <Prose>
                Three of them, and the difference is what the user is allowed to do. PrettyTable prints rows.
                CellGrid lets them be edited in place. AutoTable filters, sorts, pages, reorders, resizes and
                exports — configured with a column list and an array of rows.
            </Prose>

            <Panel title="PrettyTable — it prints rows, and that is all">
                <PrettyTable
                    columns={[
                        { displayName: "Name" },
                        { displayName: "Plan" },
                        { displayName: "Status", displayPosition: COL_POS_RIGHT },
                    ]}
                    options={{ hover: true, alternate: true, surroundingBorder: true, headerBorderY: true }}
                >
                    <tr>
                        <TdLeft>Ada Lovelace</TdLeft>
                        <TdLeft>Pro</TdLeft>
                        <TdRight><Badge color={BADGE_GREEN} pill>active</Badge></TdRight>
                    </tr>
                    <tr>
                        <TdLeft>Alan Turing</TdLeft>
                        <TdLeft>Free</TdLeft>
                        <TdRight><Badge color={BADGE_NEUTRAL} pill>trial</Badge></TdRight>
                    </tr>
                    <tr>
                        <TdLeft>Grace Hopper</TdLeft>
                        <TdLeft>Enterprise</TdLeft>
                        <TdRight><Badge color={BADGE_BLUE} pill>invited</Badge></TdRight>
                    </tr>
                </PrettyTable>
            </Panel>

            <Panel title="CellGrid — editable cells">
                <CellGrid
                    columns={[
                        { key: "sku", label: "SKU", sortKey: "sku" },
                        { key: "qty", label: "Qty", sortKey: "qty", sortType: "numeric", editable: true, inputMode: "numeric" },
                        { key: "price", label: "Price", sortKey: "price", sortType: "numeric", editable: true, inputMode: "decimal" },
                    ]}
                    rows={gridRows()}
                    initialRows={GRID_SEED}
                    idField="id"
                    onCellChange={(id, field, value) =>
                        setGridRows((rows) => rows.map((r) => (r.id === id ? { ...r, [field]: value } : r)))
                    }
                    sortKey={sortKey()}
                    setSortKey={setSortKey}
                    sortDesc={sortDesc()}
                    setSortDesc={setSortDesc}
                />
                <p class="mt-3 text-ss text-ink-muted">
                    Click a Qty or Price cell and type; Tab and the arrow keys move between editable cells.
                    The grid does not own the rows — it tells you which cell changed and hands the value back.
                </p>
            </Panel>

            <Prose>
                AutoTable is the big one. Sort by clicking a header, drag a header to reorder it, use the
                toolbar to hide a column or export what you are looking at. The column layout is keyed to
                localStorage, so it survives a reload.
            </Prose>

            <div class="mt-4">
                <AutoTable
                    data={ROWS}
                    columns={COLUMNS}
                    emptyMessage="No institutions match those filters."
                    options={{
                        size: AUTOTABLE_SIZE_COMPACT,
                        hover: true,
                        alternate: true,
                        surroundingBorder: true,
                        headerBorderY: true,
                        draggableColumns: true,
                        toggleColumns: true,
                        resizableColumns: true,
                        resetButton: true,
                        exportCSV: true,
                        exportFilename: "kjol-rates",
                        inlineToolbar: true,
                        columnOrderStorageKey: "kjolweb.js.table.order",
                        columnVisibilityStorageKey: "kjolweb.js.table.visible",
                        columnWidthStorageKey: "kjolweb.js.table.widths",
                    }}
                    searchFields={(ctx) => (
                        <AutoTableFilterFields>
                            <AutoTableSearch
                                label="Institution"
                                placeholder="Search by name…"
                                value={ctx.getSearchValue("name")}
                                onchange={(v) => ctx.setSearchValue("name", v)}
                            />
                            <AutoTableSearch
                                label="State"
                                placeholder="CA"
                                value={ctx.getSearchValue("state")}
                                onchange={(v) => ctx.setSearchValue("state", v)}
                            />
                        </AutoTableFilterFields>
                    )}
                    rowRenderer={(item: Institution) => (
                        <>
                            <TdLeft class="font-medium text-ink">{item.name}</TdLeft>
                            <TdCenter>{item.state}</TdCenter>
                            <TdLeft>{item.term}</TdLeft>
                            <TdRight class="font-mono">{item.rate.toFixed(2)}%</TdRight>
                            <TdRight class="font-mono">{money(item.minimum)}</TdRight>
                            <TdCenter><StatusBadge status={item.status} /></TdCenter>
                        </>
                    )}
                />
            </div>

            <AlertBlue header="Local rows, or a server" class="mt-6">
                This table is passed <code class="font-mono">data</code>. Give it{" "}
                <code class="font-mono">url</code> instead and the same column list drives a server-side
                query — the sort identifier becomes the sort key, the search fields become query parameters,
                and pagination is handled for you. Nothing else on the page changes.
            </AlertBlue>
        </Section>
    );
}

// ---- overlays ------------------------------------------------------------

function Overlays() {
    const toast = useToast();

    const [modalOpen, setModalOpen] = createSignal(false);
    const [nestedOpen, setNestedOpen] = createSignal(false);
    const [confirmOpen, setConfirmOpen] = createSignal(false);
    const [wizardOpen, setWizardOpen] = createSignal(false);
    const [deleted, setDeleted] = createSignal(false);
    const [wizardName, setWizardName] = createSignal("");

    return (
        <Section id="overlays" title="Overlays">
            <Prose>
                Tooltips, popovers, menus and modals — every one measured against the real viewport. A
                floating panel portals itself to <code class="font-mono">document.body</code>, positions from
                its trigger's bounding box, and flips or shifts when it would otherwise run off the screen.
            </Prose>
            <Prose>
                None of them needs a host or a portal root mounted anywhere. That is the difference from a
                modal library that makes you remember a <code class="font-mono">&lt;div id="portal"&gt;</code>{" "}
                at the bottom of your index.html.
            </Prose>

            <Panel title="Tooltips — hover and focus">
                <div class="flex flex-wrap items-center gap-3">
                    <Tooltip content="Above — the default">
                        <ButtonUI color={BUTTON_COLOR_LIGHT_NEUTRAL}>Top</ButtonUI>
                    </Tooltip>
                    <Tooltip content="To the right, unless it would run off the edge" placement="right">
                        <ButtonUI color={BUTTON_COLOR_LIGHT_NEUTRAL}>Right</ButtonUI>
                    </Tooltip>
                    <Tooltip content="No open delay" delay={0}>
                        <ButtonUI color={BUTTON_COLOR_LIGHT_NEUTRAL}>Instant</ButtonUI>
                    </Tooltip>
                    <Tooltip content="Shown on FOCUS, not hover — tab to the field" trigger="focus">
                        <FormInput placeholder="Focus me" />
                    </Tooltip>
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    A tooltip that only answers to a mouse is a tooltip a keyboard user cannot read.
                </p>
            </Panel>

            <Panel title="Popovers — click, alignment, and a hover bridge">
                <div class="flex flex-wrap items-center gap-3">
                    <Popover>
                        <PopoverTrigger>
                            <ButtonUI color={BUTTON_COLOR_PRIMARY}>Click me</ButtonUI>
                        </PopoverTrigger>
                        <PopoverContent class="w-64">
                            <p class="text-sm text-ink-soft">
                                Click outside, or press Escape. Only the TOPMOST floating panel closes per press.
                            </p>
                        </PopoverContent>
                    </Popover>

                    <Popover placement="bottom-end">
                        <PopoverTrigger>
                            <ButtonUI color={BUTTON_COLOR_LIGHT_NEUTRAL}>Aligned to my right edge</ButtonUI>
                        </PopoverTrigger>
                        <PopoverContent class="w-56">
                            <p class="text-sm text-ink-soft">Placement bottom-end.</p>
                        </PopoverContent>
                    </Popover>

                    <HoverPopover placement="top" hoverCloseDelay={300}>
                        <HoverPopoverTrigger>
                            <ButtonUI color={BUTTON_COLOR_BLUE} outline>Hover, then reach the panel</ButtonUI>
                        </HoverPopoverTrigger>
                        <HoverPopoverContent class="w-64">
                            <p class="text-sm text-ink-soft">
                                Move the cursor across the gap and onto this panel — it stays open. Select this
                                text to prove it. Without that grace period the panel would close in the dead
                                space between the two, which is what happens once a panel is portaled and CSS
                                :hover no longer reaches it.
                            </p>
                        </HoverPopoverContent>
                    </HoverPopover>
                </div>
            </Panel>

            <Panel title="Menus & submenus">
                <div class="flex flex-wrap items-center gap-3">
                    <Menu>
                        <MenuTrigger>
                            <ButtonUI color={BUTTON_COLOR_WHITE}>
                                Actions <Icon icon="chevron-down" size={11} />
                            </ButtonUI>
                        </MenuTrigger>
                        <MenuContent>
                            <MenuItem icon="check" onclick={() => toast.success("Profile opened")}>Profile</MenuItem>
                            <MenuItem onclick={() => toast.info("Settings opened")}>Settings</MenuItem>
                            <Submenu trigger="More" icon="copy">
                                <MenuItem onclick={() => toast.info("Archived")}>Archive</MenuItem>
                                <MenuItem onclick={() => toast.warning("Duplicated")}>Duplicate</MenuItem>
                            </Submenu>
                            <MenuDivider />
                            <MenuItem closeOnClick={false} onclick={() => toast.generic("Menu stayed open")}>
                                Stay open (closeOnClick={"{false}"})
                            </MenuItem>
                            <MenuItem onclick={() => toast.error("Signed out")}>Sign out</MenuItem>
                        </MenuContent>
                    </Menu>

                    <Menu openOnHover>
                        <MenuTrigger>
                            <ButtonUI color={BUTTON_COLOR_LIGHT_NEUTRAL}>Opens on hover</ButtonUI>
                        </MenuTrigger>
                        <MenuContent>
                            <MenuItem>One</MenuItem>
                            <MenuItem>Two</MenuItem>
                        </MenuContent>
                    </Menu>
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    The items raise toasts, which is how you can see that an item really does close its own
                    menu — and that the one marked closeOnClick={"{false}"} does not.
                </p>
            </Panel>

            <Panel title={"Modals — deleted: " + deleted()}>
                <div class="flex flex-wrap items-center gap-3">
                    <ButtonUI color={BUTTON_COLOR_PRIMARY} onclick={() => setModalOpen(true)}>Open modal</ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_RED} outline onclick={() => setConfirmOpen(true)}>
                        Delete something…
                    </ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_BLUE} outline onclick={() => setWizardOpen(true)}>
                        Open wizard
                    </ButtonUI>
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    Open the modal, then the nested one inside it, and press Escape twice: modals unwind ONE
                    LAYER per press rather than all at once.
                </p>

                <Modal
                    isOpen={modalOpen}
                    onClose={() => setModalOpen(false)}
                    header={<h3 class="text-lg font-semibold">A modal</h3>}
                    footer={
                        <ButtonUI color={BUTTON_COLOR_NEUTRAL} onclick={() => setModalOpen(false)}>Close</ButtonUI>
                    }
                >
                    <p class="text-sm leading-relaxed text-ink-soft">
                        Portaled to <code class="font-mono">document.body</code>, so no ancestor's{" "}
                        <code class="font-mono">overflow: hidden</code> or transform can clip it — the two
                        things that silently clip a floating panel.
                    </p>
                    <div class="mt-4">
                        <ButtonUI color={BUTTON_COLOR_BLUE} outline onclick={() => setNestedOpen(true)}>
                            Open a nested modal
                        </ButtonUI>
                    </div>
                </Modal>

                <Modal
                    isOpen={nestedOpen}
                    onClose={() => setNestedOpen(false)}
                    size="small"
                    header={<h3 class="text-lg font-semibold">Nested</h3>}
                >
                    <p class="text-sm text-ink-soft">Escape closes THIS one first, not the one behind it.</p>
                </Modal>

                <ConfirmModal
                    isOpen={confirmOpen}
                    onClose={() => setConfirmOpen(false)}
                    onConfirm={() => {
                        setDeleted(true);
                        toast.error("Row deleted");
                    }}
                    title="Delete row"
                    message="This cannot be undone. (Nothing is actually deleted — this is a docs page.)"
                    confirmText="Delete"
                />

                <WizardModal
                    isOpen={wizardOpen}
                    onClose={() => setWizardOpen(false)}
                    onComplete={() => toast.success("Wizard complete: " + (wizardName() || "nobody"))}
                    title="Set up your account"
                    finishText="Finish"
                    steps={[
                        {
                            title: "Your name",
                            // Each step gets its own context: setCanContinue gates THIS step's
                            // Next button, which one shared boolean could not express.
                            content: (ctx) => {
                                ctx.setCanContinue(wizardName() !== "");
                                return (
                                    <Field label="Name (required to continue)">
                                        <FormInput
                                            value={wizardName()}
                                            placeholder="Ada Lovelace"
                                            oninput={(e) => setWizardName(e.currentTarget.value)}
                                        />
                                    </Field>
                                );
                            },
                        },
                        {
                            title: "Confirm",
                            content: (ctx) => {
                                ctx.setCanContinue(true);
                                return (
                                    <p class="text-sm text-ink-soft">
                                        All set for {wizardName() || "nobody"}. Finish to close.
                                    </p>
                                );
                            },
                        },
                    ]}
                />
            </Panel>
        </Section>
    );
}

// ---- feedback ------------------------------------------------------------

function Feedback() {
    const toast = useToast();
    const flash = createRemoteFlash(2000);

    return (
        <Section id="feedback" title="Toasts & tours">
            <Prose>
                Toasts dismiss themselves after five seconds, with a bar counting down. A sticky one (
                <code class="font-mono">duration: null</code>) waits for the user instead.
            </Prose>

            <Panel title="Push, dismiss, and a sticky one">
                <div class="flex flex-wrap items-center gap-2">
                    <ButtonUI color={BUTTON_COLOR_GREEN} small onclick={() => toast.success("Saved.")}>
                        Success
                    </ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_RED} small onclick={() => toast.error("Something went wrong.")}>
                        Error
                    </ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_BLUE} small onclick={() => toast.info("Just so you know.")}>
                        Info
                    </ButtonUI>
                    <ButtonUI
                        color={BUTTON_COLOR_LIGHT_NEUTRAL}
                        small
                        onclick={() =>
                            toast.addToast({
                                message: "This one waits for you to dismiss it.",
                                type: "warning",
                                duration: null,
                            })
                        }
                    >
                        Sticky (no timer)
                    </ButtonUI>
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    <code class="font-mono">useToast()</code> throws outside a{" "}
                    <code class="font-mono">&lt;ToastProvider&gt;</code> — which is one of only two providers
                    this kit has.
                </p>
            </Panel>

            <Panel title="The guided tour">
                <div class="flex flex-wrap items-center gap-3">
                    <StartTutorialButton>Take the tour</StartTutorialButton>
                    <span class="text-ss text-ink-muted">
                        It dims the page, cuts a hole around each target, and animates the spotlight from one
                        to the next. Targets are CSS SELECTORS — the same section ids the sidebar jumps to.
                    </span>
                </div>
            </Panel>

            <Panel title="Remote update flash">
                <div class="flex flex-wrap items-center gap-3">
                    <ButtonUI color={BUTTON_COLOR_LIGHT_NEUTRAL} small onclick={flash.fire}>
                        Something changed elsewhere
                    </ButtonUI>
                    <RemoteUpdateFlash when={flash.visible()} />
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    A brief acknowledgement that data you are looking at was changed by somebody else. It is
                    not a toast: it belongs next to the thing that moved, not in the corner.
                </p>
            </Panel>
        </Section>
    );
}

// ---- navigation ----------------------------------------------------------

function Navigation() {
    const [side, setSide] = createSignal("buttons");

    const panel = (s: string) => <p class="pt-3 text-sm text-ink-soft">{s}</p>;

    return (
        <Section id="navigation" title="Tabs & navigation">
            <Prose>
                Tabs, an accordion, and a sidebar of jump links. Tabs can persist their active index to
                localStorage with <code class="font-mono">storageKey</code>, and sync it across tabs of the
                browser via storage events — which is either delightful or alarming, so it is opt-in.
            </Prose>

            <Panel title="Tabs">
                <TabGroup
                    items={[
                        { title: "Overview", content: panel("The overview panel.") },
                        { title: "Details", content: panel("The details panel.") },
                        { title: "Activity", badge: 3, content: panel("The activity panel (3 new).") },
                    ]}
                />
            </Panel>

            <Panel title="CRM tabs — the same thing, wearing a different suit">
                <div class="flex flex-col gap-4">
                    <CrmTabGroup
                        items={[
                            { title: "Contacts", badge: 12, content: panel("A contacts list would go here.") },
                            { title: "Deals", badge: 3, content: panel("And the deals.") },
                            { title: "Notes", content: panel("And the notes.") },
                        ]}
                    />
                    <CrmSubTabGroup
                        items={[
                            { title: "All", content: panel("The sub-tab strip: smaller, for nesting inside a tab.") },
                            { title: "Mine", content: panel("Mine.") },
                        ]}
                    />
                </div>
            </Panel>

            <Panel title="Accordion — one open at a time, or several">
                <div class="grid gap-6 lg:grid-cols-2">
                    <div class="flex flex-col gap-2">
                        <p class="text-ss font-semibold uppercase tracking-widest text-ink-faint">
                            SingleAccordion
                        </p>
                        <SingleAccordion
                            items={[
                                { title: "What is Kjøl JS Web?", content: panel("The Solid kit, built by a Go toolchain.") },
                                { title: "Is there a Node build?", content: panel("No. TSX → Solid → esbuild, all in Go.") },
                                { title: "How is it styled?", content: panel("Tailwind v4, compiled by kjol/tw.") },
                            ]}
                        />
                    </div>
                    <div class="flex flex-col gap-2">
                        <p class="text-ss font-semibold uppercase tracking-widest text-ink-faint">
                            Accordion (several at once)
                        </p>
                        <Accordion
                            items={[
                                { title: "First", content: panel("Open me.") },
                                { title: "Second", content: panel("And me, at the same time.") },
                                { title: "Third", content: panel("And me.") },
                            ]}
                        />
                    </div>
                </div>
            </Panel>

            <Panel title={"Sidebar nav — clicked: " + side()}>
                <div class="max-w-xs">
                    <SidebarNav
                        items={[
                            { id: "buttons", label: "Buttons", icon: <Icon icon="check" size={14} /> },
                            { id: "forms", label: "Forms", icon: <Icon icon="pen-to-square" size={14} /> },
                            {
                                id: "tables",
                                label: "Tables",
                                icon: <Icon icon="table" size={14} />,
                                children: [
                                    { id: "tables", label: "AutoTable" },
                                    { id: "overlays", label: "Overlays" },
                                ],
                            },
                        ]}
                        onItemClick={setSide}
                    />
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    It scrolls the element whose <code class="font-mono">id</code> matches the item into view —
                    so these really do jump, because those sections really do exist on this page.
                </p>
            </Panel>
        </Section>
    );
}

// ---- fuzzy search --------------------------------------------------------

const KIT_NAMES = [
    "AutoTable", "Accordion", "Alert", "Badge", "ButtonUI", "Calendar", "CellGrid", "FormCombobox",
    "DatePicker", "FormInput", "Menu", "Modal", "FormMultiSelect", "Popover", "PrettyTable",
    "SegmentedButtons", "FormSignaturePad", "TabGroup", "ThemeToggle", "ToastProvider", "ToggleSwitch",
    "Tooltip", "TutorialProvider", "SidebarNav", "Chart", "FuzzyMatch",
];

// Both "and" and "&" spellings appear here on purpose: with andAmpersand on, typing
// either finds both, and the exact spelling ranks above the substituted one.
// "Standard" / "Brand" hold an "and" that is NOT the whole word — left untouched.
const AND_AMP_NAMES = [
    "First Bank & Trust",
    "First Bank and Trust Company",
    "Smith & Wesson Financial",
    "Johnson and Johnson Federal CU",
    "Highland Savings & Loan",
    "Standard Chartered",
    "Brand Mortgage Group",
    "AT&T Employees CU",
];

function Search() {
    const [hit, setHit] = createSignal("");

    return (
        <Section id="search" title="Fuzzy search">
            <Prose>
                Subsequence matching with a typo tolerance, scored so the best hit sorts first, and the
                matched characters highlighted in the result. "atbl" finds AutoTable; so does "autotbale",
                which is what you actually typed.
            </Prose>

            <Panel title={"Type into it — picked: " + (hit() || "nothing")}>
                <div class="max-w-md">
                    <FuzzyMatch
                        options={KIT_NAMES}
                        placeholder="Search the kit…"
                        maxResults={6}
                        showScores
                        onSelect={(value) => setHit(value)}
                    />
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    The scorer is headless too: <code class="font-mono">rankFuzzyMatches</code> and{" "}
                    <code class="font-mono">fuzzySegments</code> give you the ranking and the highlight runs,
                    and you render them however you like.
                </p>
            </Panel>

            <Prose>
                Pass <code class="font-mono">andAmpersand</code> and the word "and" and the symbol "&amp;"
                match each other, so "First Bank and Trust" also finds "First Bank &amp; Trust". The exact
                spelling still wins — the substituted form is a penalized extra pass, not a free swap — and a
                stray "and" inside "Standard" or "Brand" is left alone.
            </Prose>

            <Panel title="andAmpersand — type “first bank and trust”, or “smith & wesson”">
                <div class="max-w-md">
                    <FuzzyMatch
                        options={AND_AMP_NAMES}
                        andAmpersand
                        placeholder="Search bank names…"
                        maxResults={6}
                        showScores
                    />
                </div>
            </Panel>
        </Section>
    );
}

// ---- charts --------------------------------------------------------------

const CHART_DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];

// A made-up per-state metric for the choropleth, and a few cities (lat/lng) to drop on
// top of it — Anchorage and Honolulu included, to land on albersUsa's AK/HI insets.
const US_SIGNUPS: Record<string, number> = {
    CA: 4820, TX: 3910, NY: 3120, FL: 2870, IL: 1740, PA: 1610, OH: 1490, GA: 1450,
    NC: 1360, MI: 1280, WA: 1230, AZ: 1180, MA: 1120, VA: 1090, CO: 980, TN: 940,
    NJ: 910, OR: 720, MN: 690, WI: 610, MO: 560, MD: 540, IN: 520, NV: 480,
    UT: 430, AL: 390, SC: 360, KY: 310, LA: 300, OK: 280, CT: 260, IA: 210,
    KS: 180, AK: 140, HI: 160, ME: 120, MT: 90, WY: 60, ND: 70, SD: 80,
};
const US_CITIES = [
    { label: "Seattle", lat: 47.6062, lng: -122.3321, value: 1230 },
    { label: "San Francisco", lat: 37.7749, lng: -122.4194, value: 2110 },
    { label: "Denver", lat: 39.7392, lng: -104.9903, value: 980 },
    { label: "Chicago", lat: 41.8781, lng: -87.6298, value: 1740 },
    { label: "New York", lat: 40.7128, lng: -74.006, value: 3120 },
    { label: "Miami", lat: 25.7617, lng: -80.1918, value: 1460 },
    { label: "Anchorage", lat: 61.2181, lng: -149.9003, value: 140 },
    { label: "Honolulu", lat: 21.3069, lng: -157.8583, value: 160 },
];

function Charts() {
    const [seed, setSeed] = createSignal(0);
    const [threeD, setThreeD] = createSignal(false);
    const [depth, setDepth] = createSignal(16);
    const [tilt, setTilt] = createSignal(0.6);

    // Two series over the same week. seed() reshuffles them so you can watch the SVG
    // move — no teardown, no new instance, just the marks that changed.
    const shuffle = (base: number[]) =>
        base.map((v) => (seed() === 0 ? v : Math.max(4, (v * 7 + seed() * 13) % 80)));
    const requests = (): ChartSeries => ({ name: "Requests", data: shuffle([42, 17, 63, 28, 55, 9, 71]) });
    const errors = (): ChartSeries => ({ name: "Errors", data: shuffle([8, 3, 12, 6, 9, 2, 14]) });
    const pieData = (): ChartSeries => ({ name: "Traffic", data: shuffle([40, 25, 20, 15, 8]) });
    const pieLabels = ["Direct", "Search", "Social", "Email", "Referral"];
    const regions = ["East", "Central", "Mountain", "Pacific"];

    return (
        <Section id="charts" title="Charts">
            <Prose>
                <code class="font-mono">@ui/Chart</code> draws SVG — no charting library, nothing vendored, no{" "}
                <code class="font-mono">&lt;canvas&gt;</code>. Because the picture is JSX it is already reactive:
                change <code class="font-mono">series</code> and Solid re-renders the marks that moved. There is
                no <code class="font-mono">update()</code> to call.
            </Prose>
            <Prose>
                Being SVG buys two things a canvas cannot. The marks name the theme tokens
                (<code class="font-mono">var(--color-chart-1)</code> …), so the palette inverts for dark mode with
                the rest of the site — a canvas paints pixels and cannot read a CSS variable. And every mark is a
                real element, so hovering shows a crosshair and one tooltip listing every series at that point.
            </Prose>

            <Panel title="Grouped bars, a smooth area, a two-line series, and a donut — from one data set">
                <div class="grid gap-6 lg:grid-cols-12">
                    <div class="lg:col-span-7">
                        <Chart kind="bar" title="Requests & errors this week" labels={CHART_DAYS} series={[requests(), errors()]} height={260} threeD={threeD()} />
                    </div>
                    <div class="lg:col-span-5">
                        <Chart kind="donut" title="Traffic by source" labels={pieLabels} series={[pieData()]} height={260} threeD={threeD()} />
                    </div>
                    <div class="lg:col-span-7">
                        <Chart kind="area" curve="smooth" title="Requests, smoothed" labels={CHART_DAYS} series={[requests()]} height={220} threeD={threeD()} />
                    </div>
                    <div class="lg:col-span-5">
                        <Chart kind="line" title="Requests & errors" labels={CHART_DAYS} series={[requests(), errors()]} height={220} threeD={threeD()} />
                    </div>
                </div>
                <div class="mt-4 flex items-center gap-3">
                    <ButtonUI color={BUTTON_COLOR_PRIMARY} small onclick={() => setSeed(seed() + 1)}>
                        New data
                    </ButtonUI>
                    <ButtonUI color={BUTTON_COLOR_NEUTRAL} small onclick={() => setThreeD(!threeD())}>
                        {threeD() ? "Flat" : "3D"}
                    </ButtonUI>
                    <span class="text-ss text-ink-muted">
                        Each chart carries a <code class="font-mono">title</code>; a multi-series chart also draws a
                        legend. Click a legend key — say <em>Errors</em> or a donut slice — to hide it, and the scale
                        and marks recompute from what's left. The <code class="font-mono">threeD</code> prop gives
                        every plot a 3D grid — bars extrude, a line or area draws flat over it, the donut tilts.
                    </span>
                </div>
            </Panel>

            <Panel title="3D grid — bars extrude into it, lines draw flat on the front">
                <div class="grid items-center gap-6 lg:grid-cols-12">
                    <div class="lg:col-span-4">
                        <Chart kind="bar" title="Bars" labels={CHART_DAYS}
                            series={[requests()]} height={240} threeD depth={depth()} tilt={tilt()} />
                    </div>
                    <div class="lg:col-span-4">
                        <Chart kind="line" title="Lines" labels={CHART_DAYS}
                            series={[requests(), errors()]} height={240} threeD depth={depth()} tilt={tilt()} />
                    </div>
                    <div class="flex flex-col justify-center gap-5 lg:col-span-4">
                        <label class="flex flex-col gap-1.5">
                            <span class="flex items-center justify-between text-ss text-ink-soft">
                                <code class="font-mono">depth</code>
                                <span class="font-mono tabular-nums text-ink-muted">{depth()}px</span>
                            </span>
                            <input type="range" min="0" max="40" step="1" value={depth()}
                                class="w-full accent-accent"
                                oninput={(e) => setDepth(+e.currentTarget.value)} />
                        </label>
                        <label class="flex flex-col gap-1.5">
                            <span class="flex items-center justify-between text-ss text-ink-soft">
                                <code class="font-mono">tilt</code>
                                <span class="font-mono tabular-nums text-ink-muted">{tilt().toFixed(2)}</span>
                            </span>
                            <input type="range" min="0" max="1" step="0.05" value={tilt()}
                                class="w-full accent-accent"
                                oninput={(e) => setTilt(+e.currentTarget.value)} />
                        </label>
                    </div>
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    The same <code class="font-mono">depth</code> and <code class="font-mono">tilt</code> give the grid
                    itself perspective: a floor recedes from the value-0 baseline into a back wall. Bars extrude into
                    that space; a line or area stays flat on the front plane, drawn over the grid and read against the
                    front axis — the stroke is <em>not</em> itself extruded, since depth on a hairline reads as noise.{" "}
                    <code class="font-mono">tilt</code> runs 1 (head-on, near-flat) to 0 (bird's-eye);{" "}
                    <code class="font-mono">depth</code> is the sweep length in px.
                </p>
            </Panel>

            <Prose>
                One <code class="font-mono">kind</code> prop picks the form —{" "}
                <code class="font-mono">"line" | "area" | "bar" | "pie" | "donut"</code> — and{" "}
                <code class="font-mono">stacked</code>, <code class="font-mono">horizontal</code>,{" "}
                <code class="font-mono">curve</code>, <code class="font-mono">threeD</code>{" "}
                (with <code class="font-mono">depth</code> / <code class="font-mono">tilt</code>),{" "}
                <code class="font-mono">title</code>, <code class="font-mono">palette</code> and{" "}
                <code class="font-mono">valueFormat</code> refine it. The legend it draws for a multi-series or
                pie/donut chart is interactive — clicking a key toggles that series or slice. The width is measured
                from the container, so a chart fills whatever column you give it.
            </Prose>

            <Panel title="Horizontal bars, and stacked — the same kind, transposed and layered">
                <div class="grid gap-6 lg:grid-cols-2">
                    <Chart kind="bar" horizontal labels={CHART_DAYS} series={[requests(), errors()]} height={260} />
                    <Chart kind="bar" stacked labels={regions}
                        series={[
                            { name: "Requests", data: shuffle([42, 55, 28, 63]) },
                            { name: "Errors", data: shuffle([8, 9, 6, 12]) },
                            { name: "Retries", data: shuffle([5, 7, 3, 9]) },
                        ]} height={260} />
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    <code class="font-mono">horizontal</code> runs the categories down the y-axis;{" "}
                    <code class="font-mono">stacked</code> layers the series with a 2px surface gap between segments.
                </p>
            </Panel>

            <Panel title="US heatmap — a value per state, with proportional lat/lng points on top">
                <USHeatmap data={US_SIGNUPS} points={US_CITIES} proportional valueFormat={(v) => v.toLocaleString("en-US")} />
                <p class="mt-3 text-ss text-ink-muted">
                    <code class="font-mono">@ui/USHeatmap</code> shades each state on a themed sequential ramp and
                    projects <code class="font-mono">points</code> (latitude/longitude) with a dependency-free
                    albersUsa port — so Anchorage and Honolulu land on the Alaska and Hawaii insets. With{" "}
                    <code class="font-mono">proportional</code>, each dot's area scales with its value. Hover a
                    state or a point.
                </p>
            </Panel>
        </Section>
    );
}

// ---- theming -------------------------------------------------------------

const TOKENS: { swatch: string; name: string; role: string }[] = [
    { swatch: "bg-surface", name: "surface", role: "the page" },
    { swatch: "bg-surface-muted", name: "surface-muted", role: "a recessed strip" },
    { swatch: "bg-surface-raised", name: "surface-raised", role: "a panel, a hover" },
    { swatch: "bg-surface-strong", name: "surface-strong", role: "a track, a divider fill" },
    { swatch: "bg-line", name: "line", role: "an ordinary border" },
    { swatch: "bg-line-strong", name: "line-strong", role: "a border that has to be seen" },
    { swatch: "bg-ink", name: "ink", role: "body text, headings" },
    { swatch: "bg-ink-soft", name: "ink-soft", role: "secondary text" },
    { swatch: "bg-ink-muted", name: "ink-muted", role: "captions, labels" },
    { swatch: "bg-ink-faint", name: "ink-faint", role: "placeholders, disabled" },
];

function Theming() {
    const { isDark, mode } = useTheme();

    return (
        <Section id="theming" title="Theming">
            <Prose>
                No component in this kit names a colour. They say <code class="font-mono">bg-surface</code>,{" "}
                <code class="font-mono">text-ink</code>, <code class="font-mono">border-line</code> — and what
                those mean is decided in one place. Dark mode re-points a dozen CSS variables and not one
                component knows it happened.
            </Prose>

            <Demo
                title="The switch"
                code={`/* jsruntime/styles/theme.css */
@custom-variant dark (&:where(.dark, .dark *));

@theme {
    --color-surface: #ffffff;
    --color-ink:     #171717;
}

.dark {
    --color-surface: #101013;   /* not black: black makes every border vanish */
    --color-ink:     #f2f2f3;
}`}
            >
                <div class="flex flex-wrap items-center gap-4">
                    <ThemeToggle />
                    <div class="text-sm text-ink-soft">
                        currently <span class="font-mono text-ink">{isDark() ? "dark" : "light"}</span>, because
                        you asked for <span class="font-mono text-ink">{mode()}</span>
                    </div>
                </div>
                <p class="mt-4 text-sm leading-relaxed text-ink-muted">
                    Press it. Every component on this page moves — none of them were told. Your choice is
                    remembered, and it is the same choice Kjøl Wasm Web reads: both layers of this site share
                    one <code class="font-mono">kjol-theme</code> key, so the theme survives crossing between
                    two front-ends that share no code at all.
                </p>
            </Demo>

            <Panel title="The contract — each swatch is drawn WITH the token it names">
                <div class="overflow-hidden rounded-default border border-line">
                    <For each={TOKENS}>
                        {(t, i) => (
                            <div class={"flex items-center gap-4 px-4 py-2.5 " + (i() > 0 ? "border-t border-line" : "")}>
                                <span class={"h-7 w-7 shrink-0 rounded border border-line-strong " + t.swatch} />
                                <code class="w-40 shrink-0 font-mono text-[13px] text-ink">{t.name}</code>
                                <span class="text-sm text-ink-muted">{t.role}</span>
                            </div>
                        )}
                    </For>
                </div>
                <p class="mt-3 text-ss text-ink-muted">
                    The swatch class is written out in full in the source, not built as{" "}
                    <code class="font-mono">"bg-" + name</code>. Tailwind finds the classes it must compile by
                    scanning the source for literal strings — a concatenation is invisible to it, and every
                    swatch here would come out colourless.
                </p>
            </Panel>

            <div class="mt-6 grid gap-4 sm:grid-cols-2">
                <Card>
                    <CardHeader>Coloured tints</CardHeader>
                    <p class="text-sm leading-relaxed text-ink-soft">
                        A <code class="font-mono">red-50</code> wash is invisible on a near-black surface. An
                        alert's tint has to become a deep, transparent one — a different colour, not a
                        different value of the same one. One of only two places a{" "}
                        <code class="font-mono">dark:</code> variant survives.
                    </p>
                </Card>
                <Card>
                    <CardHeader>Fills that invert</CardHeader>
                    <p class="text-sm leading-relaxed text-ink-soft">
                        The neutral button is dark on a light page and light on a dark one, so its label must
                        invert with it — <code class="font-mono">text-white</code> would disappear the moment
                        the fill went pale. Hence three tokens, not one. The other place.
                    </p>
                </Card>
            </div>
        </Section>
    );
}
