import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import type { Meta, StoryObj } from "@storybook/react" import type { ReactNode } from "react" import { useState } from "react" import { Button } from "../Button/Button.tsx" import { StoryCell, StoryGrid, StorySection, } from "../board.storyHelpers.tsx" import { BulletedListIcon, ExternalIcon, InboxIcon, MenuIcon, MoonIcon, PlayIcon, SettingsIcon, } from "../icons.storyHelpers.tsx" import { RouterLinkProvider } from "../RouterLink/RouterLinkProvider.tsx" import type { RouterLinkProps } from "../RouterLink/routerLink.ts" import { Nav } from "./Nav.tsx" import type { NavRailItem } from "./navItems.ts" /** * A stand-in for react-router's ``, and it does the one thing * a plain anchor must not do here: `preventDefault()`. * * That is not a test convenience, it is what a router does — it * intercepts the plain left click and pushes history instead of * letting the browser leave the page. Without it these boards * genuinely navigate away. * * The `href` is untouched, so middle-click and ctrl-click still go * to the platform, exactly as they do in an app. */ const SoftRouterLink = ({ href, onClick, ...linkProps }: RouterLinkProps): ReactNode => ( { // The caller's handler first, then the interception — which is // react-router's own order in `Link`, and it is what lets a // folded panel see a click that has not been cancelled yet. onClick?.(event) event.preventDefault() }} /> ) /** * A fixed-width box, because `bar` is a measurement. A story that * sizes itself to its contents never folds at all. */ const Frame = ({ children, inlineSize, }: { children: ReactNode inlineSize: string }): ReactNode => (
{children}
) /** * **Invented, all of it.** The app this was built alongside is a * household's media tool and a published library must not carry its * shelves, so these are a fictional radio station's screens. The * shape is what matters and the shape is the same: five or six * destinations, two of them configuration, one of them leaving the * app for a source it does not own. */ const STATION_ITEMS: NavRailItem[] = [ { href: "/", icon: , key: "now", label: "On air", }, { href: "/tonight", icon: , key: "tonight", label: "Tonight", }, { href: "/library", icon: , key: "library", label: "Library", }, { href: "/incoming", icon: , key: "incoming", label: "Incoming", }, { href: "/settings", icon: , key: "settings", label: "Settings", }, ] /** * The same list with a destination that leaves the app, and one long * enough to prove the truncation rule. A rail that only looks right * with short labels has not been tested. */ const STATION_ITEMS_WITH_EXTERNAL: NavRailItem[] = [ ...STATION_ITEMS, { href: "https://example.com/catalogue", icon: , isExternal: true, key: "catalogue", label: "Publisher catalogue — every pressing", }, ] const meta = { component: Nav, decorators: [ (Story) => ( ), ], parameters: { layout: "padded" }, title: "Components/Layout/Nav", } satisfies Meta export default meta // `typeof Nav`, not `typeof meta`: `NavProps` is a union, and // Storybook resolves a `satisfies Meta<>` object with decorators // down to `never` args when the component's props do not intersect. type Story = StoryObj /** * A header row, which is what an app with a handful of destinations * wants: ai-usage, folio, mux-magic, portly-controllers. */ export const Playground: Story = { parameters: playgroundParameters, args: { activeHref: "/library", items: STATION_ITEMS, layout: "bar", menuIcon: , menuLabel: "Main menu", }, render: (navProps) => ( // Wide enough for all five, so there is **no menu button at // all** — a trigger that shows at every width is not a fold, it // is a nav somebody has permanently hidden.