import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import type { Meta, StoryObj } from "@storybook/react" import { toStoryChoice } from "../argTypes.storyHelpers.ts" import { Header } from "../Header/Header.tsx" import { Main } from "../Main/Main.tsx" import { Rail } from "../Rail/Rail.tsx" import { HeaderSchemeToggle, OverflowingContent, PageContent, ParkedDrawer, RailDetail, RailNavigation, ScrollFiller, } from "../shell.storyHelpers.tsx" import { Shell } from "./Shell.tsx" /** * `full` last, because it is the escape hatch rather than a step: * everything before it is a `screen.*` token, and an app reaching * for it is saying "this page is a wall of tiles", not picking a * slightly larger number. */ const CONTENT_WIDTH_OPTIONS = [ "sm", "md", "lg", "xl", "2xl", "full", ] as const const meta = { title: "Components/Layout/Shell", component: Shell, parameters: { layout: "fullscreen" }, argTypes: { contentWidth: toStoryChoice(CONTENT_WIDTH_OPTIONS), }, args: { contentWidth: "lg", skipLinkLabel: "Skip to main content", }, } satisfies Meta export default meta type Story = StoryObj /** * **Template 1 — header + main.** The shape eight of the fleet's * ten hand-rolled shells are, and the whole of what most apps * need. Copy it verbatim; only the contents of `actions` and * `Main` differ per app. */ export const Playground: Story = { parameters: playgroundParameters, render: (shellProps) => (
} heading="Rip Deck" />
), } /** * **Template 2 — header + a start rail.** The rail is the app's * sections, so it is `landmark="navigation"` and gets a name an * agent can scope to. Below `md` it becomes the strip above the * content — the same element, not a second copy. */ export const WithStartRail: Story = { render: (shellProps) => (
} heading="Comic Ingest" />
), } /** * **Template 3 — header + both rails.** Sections on one side, * the selected thing's detail on the other. Two rails is where * naming stops being optional: two unnamed landmarks of the same * type are indistinguishable to a screen reader and to an agent. */ export const WithBothRails: Story = { render: (shellProps) => (
} heading="Rip Deck" />
), } /** * The one the tests point a 390px viewport at. * * Both rails, plus the two shapes of content that make a page * scroll sideways: an unbroken 130-character path, and a table * wider than the phone it is on. Plex Channels' narrow view * scrolls left and right today for the first reason; the second * is what an `overflow-wrap` alone cannot save. * * The third shape is `ParkedDrawer`, and it sits **beside** * `Main` rather than in it — an app's parked chrome is parked * against the frame, which is the arrangement `Shell`'s * `overflow-x: clip` exists for. Absolutely positioned children * of a grid container take no track, so it costs the layout * nothing. `Main.stories.tsx` renders the same drawer *inside* * `Main`, where the clip that answers it is `Main`'s own. * * `document.documentElement.scrollWidth <= clientWidth` at 390px * is the assertion, and it is in `Shell.test.tsx` rather than * here — a story is a demo. */ export const Responsive: Story = { render: (shellProps) => (
} heading="Transfers" />
), } /** * What a keyboard user meets first. Tab once: the skip link * appears at the top of the page and lands focus **inside** * `
`, past the header and the rail. * * None of the fleet's ten hand-rolled shells has one, so on every * one of those apps the first Tab starts a walk through the * header — every page, every navigation. */ export const Interactive: Story = { render: (shellProps) => (
} heading="Rip Deck" />
), } /** A long page leaves the header and rail in place. */ export const Scrollable: Story = { render: (shellProps) => (
} heading="Rip Deck" />
), }