@charcuterie/ui 3.23.0 → 3.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/Board/Board.d.ts +6 -3
- package/dist/Board/Board.d.ts.map +1 -1
- package/dist/Board/Board.js +19 -1
- package/dist/Board/Board.js.map +1 -1
- package/dist/Board/BoardCard.d.ts +42 -7
- package/dist/Board/BoardCard.d.ts.map +1 -1
- package/dist/Board/BoardCard.js +54 -8
- package/dist/Board/BoardCard.js.map +1 -1
- package/dist/DeploymentUpdate/useDeploymentUpdate.d.ts +31 -0
- package/dist/DeploymentUpdate/useDeploymentUpdate.d.ts.map +1 -0
- package/dist/DeploymentUpdate/useDeploymentUpdate.js +94 -0
- package/dist/DeploymentUpdate/useDeploymentUpdate.js.map +1 -0
- package/dist/Nav/Nav.d.ts +167 -0
- package/dist/Nav/Nav.d.ts.map +1 -0
- package/dist/Nav/Nav.js +165 -0
- package/dist/Nav/Nav.js.map +1 -0
- package/dist/Nav/NavLink.d.ts +43 -0
- package/dist/Nav/NavLink.d.ts.map +1 -0
- package/dist/Nav/NavLink.js +111 -0
- package/dist/Nav/NavLink.js.map +1 -0
- package/dist/Nav/navItems.d.ts +67 -0
- package/dist/Nav/navItems.d.ts.map +1 -0
- package/dist/Nav/navItems.js +104 -0
- package/dist/Nav/navItems.js.map +1 -0
- package/dist/Nav/useNavLayout.d.ts +86 -0
- package/dist/Nav/useNavLayout.d.ts.map +1 -0
- package/dist/Nav/useNavLayout.js +114 -0
- package/dist/Nav/useNavLayout.js.map +1 -0
- package/dist/NavBar/NavBar.d.ts +37 -111
- package/dist/NavBar/NavBar.d.ts.map +1 -1
- package/dist/NavBar/NavBar.js +30 -110
- package/dist/NavBar/NavBar.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Board/Board.mdx +25 -2
- package/src/Board/Board.stories.tsx +52 -1
- package/src/Board/Board.test.tsx +82 -0
- package/src/Board/Board.tsx +24 -4
- package/src/Board/BoardCard.tsx +103 -21
- package/src/Board/boardMoveHandleContainer.test.ts +54 -0
- package/src/DeploymentUpdate/useDeploymentUpdate.ts +143 -0
- package/src/Nav/Nav.mdx +169 -0
- package/src/Nav/Nav.stories.tsx +402 -0
- package/src/Nav/Nav.test.tsx +338 -0
- package/src/Nav/Nav.tsx +406 -0
- package/src/Nav/NavLink.tsx +220 -0
- package/src/Nav/navItems.test.ts +163 -0
- package/src/Nav/navItems.ts +175 -0
- package/src/Nav/useNavLayout.ts +197 -0
- package/src/NavBar/NavBar.mdx +26 -0
- package/src/NavBar/NavBar.tsx +54 -240
- package/src/index.ts +29 -1
- package/src/sourceRules.test.ts +13 -1
- package/dist/NavBar/NavBarLink.d.ts +0 -43
- package/dist/NavBar/NavBarLink.d.ts.map +0 -1
- package/dist/NavBar/NavBarLink.js +0 -67
- package/dist/NavBar/NavBarLink.js.map +0 -1
- package/dist/NavBar/navBarCurrent.d.ts +0 -37
- package/dist/NavBar/navBarCurrent.d.ts.map +0 -1
- package/dist/NavBar/navBarCurrent.js +0 -48
- package/dist/NavBar/navBarCurrent.js.map +0 -1
- package/src/NavBar/NavBarLink.tsx +0 -139
- package/src/NavBar/navBarCurrent.test.ts +0 -81
- package/src/NavBar/navBarCurrent.ts +0 -64
package/src/Nav/Nav.mdx
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
import * as NavStories from "./Nav.stories";
|
|
4
|
+
|
|
5
|
+
<Meta of={NavStories} />
|
|
6
|
+
|
|
7
|
+
# Nav
|
|
8
|
+
|
|
9
|
+
The app's destinations. A header row, a side rail, a collapsed rail, a strip along the foot
|
|
10
|
+
of a phone, or a panel behind one control — the same list, drawn where it fits.
|
|
11
|
+
|
|
12
|
+
> **This replaces [`NavBar`](?path=/docs/components-layout-navbar--docs)**, which shipped the
|
|
13
|
+
> header row on its own and is deprecated as of this release. `bar` is `Nav`'s default
|
|
14
|
+
> layout and behaves identically, down to the measured whole-row fold — see *`bar` folds
|
|
15
|
+
> whole* below.
|
|
16
|
+
|
|
17
|
+
<Canvas of={NavStories.Default} />
|
|
18
|
+
|
|
19
|
+
## Props
|
|
20
|
+
|
|
21
|
+
<Controls />
|
|
22
|
+
|
|
23
|
+
## What was actually missing
|
|
24
|
+
|
|
25
|
+
`Shell` / `Header` / `Rail` / `Main` shipped the page frame on 2026-08-10 and left **the
|
|
26
|
+
rail empty**. Twelve UI repos, and exactly two have a `<nav>` at all: Docket hand-rolled a
|
|
27
|
+
row of `NavLink`s with the collapse rule in its own stylesheet, Folio hand-rolled another,
|
|
28
|
+
and the other ten have no destinations anywhere.
|
|
29
|
+
|
|
30
|
+
The two that exist already disagree. Docket *wraps* below its breakpoint, so its header
|
|
31
|
+
stands three lines tall on a phone — which is not a decision anybody made, it is what
|
|
32
|
+
`flex-wrap` does when nobody writes the other rule.
|
|
33
|
+
|
|
34
|
+
## Every item is a real link
|
|
35
|
+
|
|
36
|
+
<Canvas of={NavStories.AllVariants} />
|
|
37
|
+
|
|
38
|
+
`<a href>`, through the injected `RouterLinkProvider` — the same seam `TextLink` and
|
|
39
|
+
`ButtonLink` take, so an app injects its router once and every destination in the library
|
|
40
|
+
routes.
|
|
41
|
+
|
|
42
|
+
Not a `<button onClick={navigate}>`. That is what mux-magic and QueuePilot do today, it is
|
|
43
|
+
**identical in a screenshot**, and it loses middle-click, ctrl-click, "open in a new tab",
|
|
44
|
+
the status bar preview, and "copy link address". See
|
|
45
|
+
[buttons are actions, links are navigation](?path=/docs/guides-routing--docs).
|
|
46
|
+
|
|
47
|
+
An `isExternal` item is the exception the seam already knows: a plain anchor,
|
|
48
|
+
`target="_blank"`, `rel="noopener noreferrer"`. Pushing `https://…` onto a router's history
|
|
49
|
+
navigates the SPA to a route that does not exist.
|
|
50
|
+
|
|
51
|
+
## `icon`, and `NavRailItem`
|
|
52
|
+
|
|
53
|
+
`NavItem` keeps `icon` optional, because a `bar` never takes the label away — Docket's nine
|
|
54
|
+
destinations are words, and inventing nine glyphs to satisfy a type would be worse than the
|
|
55
|
+
problem.
|
|
56
|
+
|
|
57
|
+
A rail is the other case. `railIcons` is one `--control-height-lg` square and shows nothing
|
|
58
|
+
else, so an item with no glyph is a blank square that still navigates, and there is no visual
|
|
59
|
+
defect to notice because the row reads as padding. **Annotate a rail's array as
|
|
60
|
+
`NavRailItem[]`** — the same shape with `icon` made required — and the type checker catches
|
|
61
|
+
it before somebody drags a window narrow.
|
|
62
|
+
|
|
63
|
+
`items` is deliberately **not** discriminated on `layout`. The main call site passes
|
|
64
|
+
`layout={navLayout.layout}` straight from `useNavLayout`, which is a variable and not a
|
|
65
|
+
literal, so a union that split the item type by layout would force a `switch` at the one
|
|
66
|
+
place this component exists to make short.
|
|
67
|
+
|
|
68
|
+
The library still ships no icons — lucide (ISC) is the fleet recommendation, and a glyph in
|
|
69
|
+
a default renders as nothing where the font lacks it.
|
|
70
|
+
|
|
71
|
+
## `bar` folds whole, and it is measured
|
|
72
|
+
|
|
73
|
+
There is no `collapseAt` and no media query on this axis. The bar reads the width its links
|
|
74
|
+
actually have and folds when their sum stops fitting, so a longer label, a tenth destination,
|
|
75
|
+
a `data-density="kiosk"` or a reader at 175% browser zoom each move the fold on their own.
|
|
76
|
+
mux-magic collapses at a hardcoded `480px` and plex-channels at `760px`, and neither number
|
|
77
|
+
has a reason.
|
|
78
|
+
|
|
79
|
+
It folds **whole** rather than progressively, unlike `Toolbar`. `Toolbar`'s items are ranked;
|
|
80
|
+
a nav's order is the shape of the product — Docket's reads as the pipeline, Triage → Backlog
|
|
81
|
+
→ Phases → Board → Archives — and half of that behind a button is a rule no reader can learn.
|
|
82
|
+
|
|
83
|
+
⚠️ **A `bar` must not be sized by its own contents.** It measures its own box to decide what
|
|
84
|
+
to draw in it, so a container that shrink-wraps makes the answer its own input: put it in a
|
|
85
|
+
`minmax(0, 1fr)` grid track or a `flex-1 min-w-0` row.
|
|
86
|
+
|
|
87
|
+
## The current destination is announced
|
|
88
|
+
|
|
89
|
+
<Canvas of={NavStories.AllStates} />
|
|
90
|
+
|
|
91
|
+
`aria-current="page"`, and a background. One without the other is WCAG 1.4.1, and a
|
|
92
|
+
`font-weight` swap alone is exactly that failure.
|
|
93
|
+
|
|
94
|
+
Matching is two rules:
|
|
95
|
+
|
|
96
|
+
1. An exact path wins — `/board` on `/board`.
|
|
97
|
+
2. Otherwise the **longest path-segment prefix** wins. `/board` matches `/board/inProgress`
|
|
98
|
+
and does **not** match `/boardgames`, because `games` is not a new segment. Longest, so
|
|
99
|
+
an app with `/settings` and `/settings/labels` marks the deeper one.
|
|
100
|
+
|
|
101
|
+
The root is exempt from rule 2: `/` prefixes every path in the app, so a permissive root
|
|
102
|
+
would mark Home current on every screen and the state would carry no information.
|
|
103
|
+
|
|
104
|
+
Query and fragment are stripped first. A destination and a filtered view of it are the same
|
|
105
|
+
place — `/board?lane=todo` is still Board, which is the case that breaks first in a real app.
|
|
106
|
+
|
|
107
|
+
**And exactly one item is current.** `getIsCurrentHref` answers for a single link, so a nav
|
|
108
|
+
holding both `/settings` and `/settings/labels` gets `true` twice while the reader is on the
|
|
109
|
+
deeper one — which is what `NavBar` shipped. `resolveActiveKey` is the whole-list answer and
|
|
110
|
+
takes the **deepest** match, by path rather than by array position, so the product's own
|
|
111
|
+
order never decides which destination lights up.
|
|
112
|
+
|
|
113
|
+
Both are exported, so an app can assert its own table without mounting anything.
|
|
114
|
+
|
|
115
|
+
## Three widths, one rule — `useNavLayout`
|
|
116
|
+
|
|
117
|
+
<Canvas of={NavStories.Responsive} />
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
const nav = useNavLayout({ storageKey: "queuepilot-nav" })
|
|
121
|
+
|
|
122
|
+
// nav.layout: "rail" | "railIcons" | "menu"
|
|
123
|
+
// nav.isCollapsed, nav.mode, nav.setMode, nav.toggle
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
| Width | `layout` | What the app renders |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `>= 64rem` | `rail` | a `Rail` holding a `Nav` |
|
|
129
|
+
| `>= 48rem` | `railIcons` | the same `Rail`, narrower |
|
|
130
|
+
| below | `menu` | a trigger in the `Header` |
|
|
131
|
+
|
|
132
|
+
`toggle()` overrides the width and the choice is remembered under `storageKey`. The
|
|
133
|
+
override does **not** reach the narrow state — `expanded` on a 390px screen is a 212px rail
|
|
134
|
+
beside 178px of content — and the remembered value is kept rather than cleared, so rotating
|
|
135
|
+
a tablet back to landscape restores the rail the reader asked for.
|
|
136
|
+
|
|
137
|
+
**Why the app still places it.** The placement genuinely differs and the rule does not. The
|
|
138
|
+
`menu` state puts a control in the header, which is a different corner of `Shell`'s grid
|
|
139
|
+
from a side rail, and no amount of restyling moves an element across a grid. So the library
|
|
140
|
+
owns *which* state is right, at what width, with what override, remembered where; the app
|
|
141
|
+
owns two `if`s.
|
|
142
|
+
|
|
143
|
+
## The narrow panel is a `Popover`, not a `Menu`
|
|
144
|
+
|
|
145
|
+
<Canvas of={NavStories.Interactive} />
|
|
146
|
+
|
|
147
|
+
`Menu`'s items are `onSelect` callbacks on `<button>`s. That is right for what a menu
|
|
148
|
+
usually holds — undo, redo, a scheme cycle — and wrong for a destination, for every reason
|
|
149
|
+
in *Every item is a real link* above.
|
|
150
|
+
|
|
151
|
+
So `layout="menu"` is a `Popover`: anchored, dismissable, portalled to the body, and
|
|
152
|
+
`modal={false}` so the caret is not trapped. It is a disclosure, not a dialog somebody has
|
|
153
|
+
to answer. Escape and an outside press both close it.
|
|
154
|
+
|
|
155
|
+
Give the trigger a real name. A bare hamburger is a glyph, and a glyph is not a name — the
|
|
156
|
+
reason `IconButton` requires `label`.
|
|
157
|
+
|
|
158
|
+
## When to use which layout
|
|
159
|
+
|
|
160
|
+
| Layout | For |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `bar` | three to six destinations, and a grid that wants every pixel — ai-usage, folio, mux-magic, portly-controllers. Folds itself; needs no `useNavLayout`. |
|
|
163
|
+
| `rail` / `railIcons` | seven or more, or a pipeline you read down — Docket, mail-sifter, QueuePilot |
|
|
164
|
+
| `bottom` | three or four you switch between constantly, in the Narrow View only |
|
|
165
|
+
| `menu` | the Narrow View of a `rail` app |
|
|
166
|
+
|
|
167
|
+
`bottom` is deliberately not the default narrow answer. It costs a permanent strip of the
|
|
168
|
+
screen, and it earns that only when the destinations are few and the switching is constant.
|
|
169
|
+
An app you settle into for an evening wants the panel.
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { Button } from "../Button/Button.tsx"
|
|
5
|
+
import {
|
|
6
|
+
StoryCell,
|
|
7
|
+
StoryGrid,
|
|
8
|
+
StorySection,
|
|
9
|
+
} from "../board.storyHelpers.tsx"
|
|
10
|
+
import {
|
|
11
|
+
BulletedListIcon,
|
|
12
|
+
ExternalIcon,
|
|
13
|
+
InboxIcon,
|
|
14
|
+
MenuIcon,
|
|
15
|
+
MoonIcon,
|
|
16
|
+
PlayIcon,
|
|
17
|
+
SettingsIcon,
|
|
18
|
+
} from "../icons.storyHelpers.tsx"
|
|
19
|
+
import { RouterLinkProvider } from "../RouterLink/RouterLinkProvider.tsx"
|
|
20
|
+
import type { RouterLinkProps } from "../RouterLink/routerLink.ts"
|
|
21
|
+
import { Nav } from "./Nav.tsx"
|
|
22
|
+
import type { NavRailItem } from "./navItems.ts"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A stand-in for react-router's `<Link>`, and it does the one thing
|
|
26
|
+
* a plain anchor must not do here: `preventDefault()`.
|
|
27
|
+
*
|
|
28
|
+
* That is not a test convenience, it is what a router does — it
|
|
29
|
+
* intercepts the plain left click and pushes history instead of
|
|
30
|
+
* letting the browser leave the page. Without it these boards
|
|
31
|
+
* genuinely navigate away.
|
|
32
|
+
*
|
|
33
|
+
* The `href` is untouched, so middle-click and ctrl-click still go
|
|
34
|
+
* to the platform, exactly as they do in an app.
|
|
35
|
+
*/
|
|
36
|
+
const SoftRouterLink = ({
|
|
37
|
+
href,
|
|
38
|
+
onClick,
|
|
39
|
+
...linkProps
|
|
40
|
+
}: RouterLinkProps): ReactNode => (
|
|
41
|
+
<a
|
|
42
|
+
{...linkProps}
|
|
43
|
+
data-router="soft"
|
|
44
|
+
href={href}
|
|
45
|
+
onClick={(event) => {
|
|
46
|
+
// The caller's handler first, then the interception — which is
|
|
47
|
+
// react-router's own order in `Link`, and it is what lets a
|
|
48
|
+
// folded panel see a click that has not been cancelled yet.
|
|
49
|
+
onClick?.(event)
|
|
50
|
+
|
|
51
|
+
event.preventDefault()
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A fixed-width box, because `bar` is a measurement. A story that
|
|
58
|
+
* sizes itself to its contents never folds at all.
|
|
59
|
+
*/
|
|
60
|
+
const Frame = ({
|
|
61
|
+
children,
|
|
62
|
+
inlineSize,
|
|
63
|
+
}: {
|
|
64
|
+
children: ReactNode
|
|
65
|
+
inlineSize: string
|
|
66
|
+
}): ReactNode => (
|
|
67
|
+
<div
|
|
68
|
+
className="flex rounded-md border border-border-subtle bg-surface-raised p-2"
|
|
69
|
+
style={{ inlineSize }}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* **Invented, all of it.** The app this was built alongside is a
|
|
77
|
+
* household's media tool and a published library must not carry its
|
|
78
|
+
* shelves, so these are a fictional radio station's screens. The
|
|
79
|
+
* shape is what matters and the shape is the same: five or six
|
|
80
|
+
* destinations, two of them configuration, one of them leaving the
|
|
81
|
+
* app for a source it does not own.
|
|
82
|
+
*/
|
|
83
|
+
const STATION_ITEMS: NavRailItem[] = [
|
|
84
|
+
{
|
|
85
|
+
href: "/",
|
|
86
|
+
icon: <PlayIcon />,
|
|
87
|
+
key: "now",
|
|
88
|
+
label: "On air",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
href: "/tonight",
|
|
92
|
+
icon: <MoonIcon />,
|
|
93
|
+
key: "tonight",
|
|
94
|
+
label: "Tonight",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
href: "/library",
|
|
98
|
+
icon: <BulletedListIcon />,
|
|
99
|
+
key: "library",
|
|
100
|
+
label: "Library",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
href: "/incoming",
|
|
104
|
+
icon: <InboxIcon />,
|
|
105
|
+
key: "incoming",
|
|
106
|
+
label: "Incoming",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
href: "/settings",
|
|
110
|
+
icon: <SettingsIcon />,
|
|
111
|
+
key: "settings",
|
|
112
|
+
label: "Settings",
|
|
113
|
+
},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The same list with a destination that leaves the app, and one long
|
|
118
|
+
* enough to prove the truncation rule. A rail that only looks right
|
|
119
|
+
* with short labels has not been tested.
|
|
120
|
+
*/
|
|
121
|
+
const STATION_ITEMS_WITH_EXTERNAL: NavRailItem[] = [
|
|
122
|
+
...STATION_ITEMS,
|
|
123
|
+
{
|
|
124
|
+
href: "https://example.com/catalogue",
|
|
125
|
+
icon: <ExternalIcon />,
|
|
126
|
+
isExternal: true,
|
|
127
|
+
key: "catalogue",
|
|
128
|
+
label: "Publisher catalogue — every pressing",
|
|
129
|
+
},
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
const meta = {
|
|
133
|
+
component: Nav,
|
|
134
|
+
decorators: [
|
|
135
|
+
(Story) => (
|
|
136
|
+
<RouterLinkProvider link={SoftRouterLink}>
|
|
137
|
+
<Story />
|
|
138
|
+
</RouterLinkProvider>
|
|
139
|
+
),
|
|
140
|
+
],
|
|
141
|
+
parameters: { layout: "padded" },
|
|
142
|
+
title: "Components/Layout/Nav",
|
|
143
|
+
} satisfies Meta<typeof Nav>
|
|
144
|
+
|
|
145
|
+
export default meta
|
|
146
|
+
|
|
147
|
+
// `typeof Nav`, not `typeof meta`: `NavProps` is a union, and
|
|
148
|
+
// Storybook resolves a `satisfies Meta<>` object with decorators
|
|
149
|
+
// down to `never` args when the component's props do not intersect.
|
|
150
|
+
type Story = StoryObj<typeof Nav>
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A header row, which is what an app with a handful of destinations
|
|
154
|
+
* wants: ai-usage, folio, mux-magic, portly-controllers.
|
|
155
|
+
*/
|
|
156
|
+
export const Default: Story = {
|
|
157
|
+
args: {
|
|
158
|
+
activeHref: "/library",
|
|
159
|
+
items: STATION_ITEMS,
|
|
160
|
+
layout: "bar",
|
|
161
|
+
menuIcon: <MenuIcon />,
|
|
162
|
+
menuLabel: "Main menu",
|
|
163
|
+
},
|
|
164
|
+
render: (navProps) => (
|
|
165
|
+
// Wide enough for all five, so there is **no menu button at
|
|
166
|
+
// all** — a trigger that shows at every width is not a fold, it
|
|
167
|
+
// is a nav somebody has permanently hidden.
|
|
168
|
+
<Frame inlineSize="52rem">
|
|
169
|
+
<Nav {...navProps} />
|
|
170
|
+
</Frame>
|
|
171
|
+
),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The four placed layouts side by side. `menu` is not here — it is a
|
|
176
|
+
* panel that needs a trigger, and it has `Interactive` to itself.
|
|
177
|
+
*/
|
|
178
|
+
export const AllVariants: Story = {
|
|
179
|
+
args: { items: STATION_ITEMS },
|
|
180
|
+
render: (args) => (
|
|
181
|
+
<div className="flex flex-col gap-8">
|
|
182
|
+
<StorySection title="bar — a header row, with room">
|
|
183
|
+
<Frame inlineSize="52rem">
|
|
184
|
+
<Nav
|
|
185
|
+
{...args}
|
|
186
|
+
activeHref="/library"
|
|
187
|
+
layout="bar"
|
|
188
|
+
/>
|
|
189
|
+
</Frame>
|
|
190
|
+
</StorySection>
|
|
191
|
+
|
|
192
|
+
{/*
|
|
193
|
+
The same bar and the same five destinations, in a box that
|
|
194
|
+
cannot hold them. It folds WHOLE: `Toolbar` would leave two
|
|
195
|
+
in the row and put three behind the button, which splits the
|
|
196
|
+
product's own order across two places with no rule the
|
|
197
|
+
reader can learn.
|
|
198
|
+
*/}
|
|
199
|
+
<StorySection title="bar — the same five, folded">
|
|
200
|
+
<Frame inlineSize="14rem">
|
|
201
|
+
<Nav
|
|
202
|
+
{...args}
|
|
203
|
+
activeHref="/library"
|
|
204
|
+
label="Main, folded"
|
|
205
|
+
layout="bar"
|
|
206
|
+
menuIcon={<MenuIcon />}
|
|
207
|
+
menuLabel="Main menu"
|
|
208
|
+
/>
|
|
209
|
+
</Frame>
|
|
210
|
+
</StorySection>
|
|
211
|
+
|
|
212
|
+
{/*
|
|
213
|
+
The rail layouts take `NavRailItem[]`, so they are given the
|
|
214
|
+
fixture directly rather than through `args` — spreading the
|
|
215
|
+
story's args would widen `icon` back to optional and the
|
|
216
|
+
three cells below would stop type-checking, which is the
|
|
217
|
+
rule working rather than failing.
|
|
218
|
+
*/}
|
|
219
|
+
<StoryGrid columns={3}>
|
|
220
|
+
<StoryCell align="stretch" label="rail">
|
|
221
|
+
<Nav
|
|
222
|
+
activeHref="/library"
|
|
223
|
+
items={STATION_ITEMS}
|
|
224
|
+
label="Rail with labels"
|
|
225
|
+
layout="rail"
|
|
226
|
+
/>
|
|
227
|
+
</StoryCell>
|
|
228
|
+
|
|
229
|
+
<StoryCell align="stretch" label="railIcons">
|
|
230
|
+
<Nav
|
|
231
|
+
activeHref="/library"
|
|
232
|
+
items={STATION_ITEMS}
|
|
233
|
+
label="Rail without labels"
|
|
234
|
+
layout="railIcons"
|
|
235
|
+
/>
|
|
236
|
+
</StoryCell>
|
|
237
|
+
|
|
238
|
+
<StoryCell align="stretch" label="bottom">
|
|
239
|
+
<Nav
|
|
240
|
+
activeHref="/library"
|
|
241
|
+
items={STATION_ITEMS}
|
|
242
|
+
label="Bottom bar"
|
|
243
|
+
layout="bottom"
|
|
244
|
+
/>
|
|
245
|
+
</StoryCell>
|
|
246
|
+
</StoryGrid>
|
|
247
|
+
</div>
|
|
248
|
+
),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Every state one item can be in, and the two that are easy to get
|
|
253
|
+
* wrong are the last two.
|
|
254
|
+
*
|
|
255
|
+
* A **long label** must truncate rather than widen the column: a
|
|
256
|
+
* flex item's automatic minimum is its content's min-content width,
|
|
257
|
+
* so an untruncated destination name is the rail's floor.
|
|
258
|
+
*
|
|
259
|
+
* An **external** destination is never current, even while the app
|
|
260
|
+
* sits at a path that looks like it. It is somebody else's origin.
|
|
261
|
+
*/
|
|
262
|
+
export const AllStates: Story = {
|
|
263
|
+
args: { items: STATION_ITEMS },
|
|
264
|
+
render: () => (
|
|
265
|
+
<StoryGrid columns={2}>
|
|
266
|
+
<StoryCell align="stretch" label="nothing current">
|
|
267
|
+
<Nav
|
|
268
|
+
items={STATION_ITEMS}
|
|
269
|
+
label="Nothing current"
|
|
270
|
+
layout="rail"
|
|
271
|
+
/>
|
|
272
|
+
</StoryCell>
|
|
273
|
+
|
|
274
|
+
<StoryCell align="stretch" label="current, exact">
|
|
275
|
+
<Nav
|
|
276
|
+
activeHref="/library"
|
|
277
|
+
items={STATION_ITEMS}
|
|
278
|
+
label="Current exact"
|
|
279
|
+
layout="rail"
|
|
280
|
+
/>
|
|
281
|
+
</StoryCell>
|
|
282
|
+
|
|
283
|
+
<StoryCell
|
|
284
|
+
align="stretch"
|
|
285
|
+
label="current, one segment deeper"
|
|
286
|
+
>
|
|
287
|
+
<Nav
|
|
288
|
+
activeHref="/library/a-flock-of-seagulls?sort=year"
|
|
289
|
+
items={STATION_ITEMS}
|
|
290
|
+
label="Current deeper"
|
|
291
|
+
layout="rail"
|
|
292
|
+
/>
|
|
293
|
+
</StoryCell>
|
|
294
|
+
|
|
295
|
+
<StoryCell
|
|
296
|
+
align="stretch"
|
|
297
|
+
label="a long label, and one that leaves"
|
|
298
|
+
>
|
|
299
|
+
<Nav
|
|
300
|
+
activeHref="/library"
|
|
301
|
+
items={STATION_ITEMS_WITH_EXTERNAL}
|
|
302
|
+
label="Long and external"
|
|
303
|
+
layout="rail"
|
|
304
|
+
/>
|
|
305
|
+
</StoryCell>
|
|
306
|
+
</StoryGrid>
|
|
307
|
+
),
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The three widths a rail-shaped nav takes, drawn at the sizes that
|
|
312
|
+
* produce them rather than by resizing the window.
|
|
313
|
+
*
|
|
314
|
+
* `useNavLayout` is what picks between them in an app. Here they are
|
|
315
|
+
* placed by hand, because the point of the board is to show all
|
|
316
|
+
* three at once — which a hook keyed to the viewport cannot do.
|
|
317
|
+
*/
|
|
318
|
+
export const Responsive: Story = {
|
|
319
|
+
args: { items: STATION_ITEMS },
|
|
320
|
+
render: () => (
|
|
321
|
+
<div className="flex flex-wrap items-start gap-6">
|
|
322
|
+
{[
|
|
323
|
+
{
|
|
324
|
+
inlineSize: "13rem",
|
|
325
|
+
label: "≥ 64rem — rail",
|
|
326
|
+
layout: "rail" as const,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
inlineSize: "4rem",
|
|
330
|
+
label: "≥ 48rem — railIcons",
|
|
331
|
+
layout: "railIcons" as const,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
inlineSize: "22rem",
|
|
335
|
+
label: "< 48rem — bottom (or menu)",
|
|
336
|
+
layout: "bottom" as const,
|
|
337
|
+
},
|
|
338
|
+
].map((panel) => (
|
|
339
|
+
<div
|
|
340
|
+
className="flex flex-col gap-2"
|
|
341
|
+
key={panel.label}
|
|
342
|
+
style={{ inlineSize: panel.inlineSize }}
|
|
343
|
+
>
|
|
344
|
+
<span className="font-mono text-content-muted text-xs">
|
|
345
|
+
{panel.label}
|
|
346
|
+
</span>
|
|
347
|
+
|
|
348
|
+
<div className="rounded-md border border-border-subtle bg-surface-raised p-2">
|
|
349
|
+
<Nav
|
|
350
|
+
activeHref="/library"
|
|
351
|
+
items={STATION_ITEMS}
|
|
352
|
+
label={panel.label}
|
|
353
|
+
layout={panel.layout}
|
|
354
|
+
/>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
),
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The Narrow View, and the whole keyboard path: Tab to the trigger,
|
|
364
|
+
* Enter to open, Tab through real links, Escape to close.
|
|
365
|
+
*
|
|
366
|
+
* The trigger says **"Menu"** rather than carrying a bare hamburger,
|
|
367
|
+
* for the reason `IconButton` exists — a glyph is not a name.
|
|
368
|
+
*/
|
|
369
|
+
export const Interactive: Story = {
|
|
370
|
+
args: { items: STATION_ITEMS },
|
|
371
|
+
render: () => {
|
|
372
|
+
const NavMenuStory = () => {
|
|
373
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<Nav
|
|
377
|
+
activeHref="/library"
|
|
378
|
+
isVisible={isVisible}
|
|
379
|
+
items={STATION_ITEMS_WITH_EXTERNAL}
|
|
380
|
+
label="Main"
|
|
381
|
+
layout="menu"
|
|
382
|
+
onDismiss={() => {
|
|
383
|
+
setIsVisible(false)
|
|
384
|
+
}}
|
|
385
|
+
trigger={
|
|
386
|
+
<Button
|
|
387
|
+
appearance="outline"
|
|
388
|
+
intent="neutral"
|
|
389
|
+
onClick={() => {
|
|
390
|
+
setIsVisible((isOpen) => !isOpen)
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
Menu
|
|
394
|
+
</Button>
|
|
395
|
+
}
|
|
396
|
+
/>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return <NavMenuStory />
|
|
401
|
+
},
|
|
402
|
+
}
|