@charcuterie/ui 3.21.0 → 3.23.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 +22 -0
- package/dist/Board/useBoardDrag.d.ts +15 -1
- package/dist/Board/useBoardDrag.d.ts.map +1 -1
- package/dist/Board/useBoardDrag.js +3 -3
- package/dist/Board/useBoardDrag.js.map +1 -1
- package/dist/DropRail/DropRail.d.ts +109 -0
- package/dist/DropRail/DropRail.d.ts.map +1 -0
- package/dist/DropRail/DropRail.js +277 -0
- package/dist/DropRail/DropRail.js.map +1 -0
- package/dist/NavBar/NavBar.d.ts +130 -0
- package/dist/NavBar/NavBar.d.ts.map +1 -0
- package/dist/NavBar/NavBar.js +114 -0
- package/dist/NavBar/NavBar.js.map +1 -0
- package/dist/NavBar/NavBarLink.d.ts +43 -0
- package/dist/NavBar/NavBarLink.d.ts.map +1 -0
- package/dist/NavBar/NavBarLink.js +67 -0
- package/dist/NavBar/NavBarLink.js.map +1 -0
- package/dist/NavBar/navBarCurrent.d.ts +37 -0
- package/dist/NavBar/navBarCurrent.d.ts.map +1 -0
- package/dist/NavBar/navBarCurrent.js +48 -0
- package/dist/NavBar/navBarCurrent.js.map +1 -0
- package/dist/ReorderList/ReorderList.d.ts +131 -0
- package/dist/ReorderList/ReorderList.d.ts.map +1 -0
- package/dist/ReorderList/ReorderList.js +154 -0
- package/dist/ReorderList/ReorderList.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Board/useBoardDrag.ts +17 -2
- package/src/DropRail/DropRail.mdx +90 -0
- package/src/DropRail/DropRail.stories.tsx +328 -0
- package/src/DropRail/DropRail.test.tsx +154 -0
- package/src/DropRail/DropRail.tsx +496 -0
- package/src/NavBar/NavBar.mdx +157 -0
- package/src/NavBar/NavBar.stories.tsx +350 -0
- package/src/NavBar/NavBar.test.tsx +344 -0
- package/src/NavBar/NavBar.tsx +271 -0
- package/src/NavBar/NavBarLink.tsx +139 -0
- package/src/NavBar/navBarCurrent.test.ts +81 -0
- package/src/NavBar/navBarCurrent.ts +64 -0
- package/src/ReorderList/ReorderList.mdx +83 -0
- package/src/ReorderList/ReorderList.stories.tsx +149 -0
- package/src/ReorderList/ReorderList.test.tsx +190 -0
- package/src/ReorderList/ReorderList.tsx +331 -0
- package/src/icons.storyHelpers.tsx +12 -0
- package/src/index.ts +17 -0
- package/src/sourceRules.test.ts +37 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
import * as NavBarStories from "./NavBar.stories";
|
|
4
|
+
|
|
5
|
+
<Meta of={NavBarStories} />
|
|
6
|
+
|
|
7
|
+
# NavBar
|
|
8
|
+
|
|
9
|
+
The app's destinations as real links, in one line — folding into a single menu button,
|
|
10
|
+
**whole**, when that line does not fit.
|
|
11
|
+
|
|
12
|
+
<Canvas of={NavBarStories.Default} />
|
|
13
|
+
|
|
14
|
+
## Props
|
|
15
|
+
|
|
16
|
+
<Controls />
|
|
17
|
+
|
|
18
|
+
## It folds whole, and that is the decision
|
|
19
|
+
|
|
20
|
+
`Toolbar` collapses **progressively**: items drop off the end one at a time, because its
|
|
21
|
+
items are ranked — the two things you do to a running job matter more than the two you do
|
|
22
|
+
to the deck.
|
|
23
|
+
|
|
24
|
+
A nav is not ranked. Its order is the shape of the product. Docket's reads as the pipeline
|
|
25
|
+
work moves along — Triage → Backlog → Phases → Lanes → Board → Archives — and a
|
|
26
|
+
half-collapsed bar splits that shape across two places with no rule a reader can learn.
|
|
27
|
+
"Some of the app is up here and the rest is behind a button" is worse than either whole
|
|
28
|
+
answer.
|
|
29
|
+
|
|
30
|
+
So the bar shows **every** destination or **none**, and the menu is the complete list in
|
|
31
|
+
the product's own order.
|
|
32
|
+
|
|
33
|
+
<Canvas of={NavBarStories.AllVariants} />
|
|
34
|
+
|
|
35
|
+
## Why this is not a folded `Toolbar`
|
|
36
|
+
|
|
37
|
+
`Toolbar` already solves collapse-into-an-overflow, and this component **reuses its
|
|
38
|
+
measurement** — `useToolbarOverflow`, the same tape measure — rather than growing a second
|
|
39
|
+
one.
|
|
40
|
+
|
|
41
|
+
What it cannot reuse is the rendering. A `ToolbarAction` is `{ label, onSelect }`, and a
|
|
42
|
+
collapsed one becomes a `MenuItem`, which is also `{ label, onSelect }`. A nav folded into
|
|
43
|
+
one is a row of `<button>`s:
|
|
44
|
+
|
|
45
|
+
| | `<button onClick={navigate}>` | `<a href>` |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| Middle-click, ctrl-click | nothing | opens a tab |
|
|
48
|
+
| "Open link in new tab" | absent from the menu | there |
|
|
49
|
+
| "Copy link address" | absent | there |
|
|
50
|
+
| Status bar on hover | blank | the destination |
|
|
51
|
+
| Visited styling, the back button's idea of history | none | the platform's |
|
|
52
|
+
|
|
53
|
+
`BuildingAnApp.mdx` names `<button onClick={navigate}>` as the fleet's most common
|
|
54
|
+
navigation defect. A collapse mechanism that introduced it below a certain width would be
|
|
55
|
+
that defect on a timer — and it screenshots identically, which is how it would survive
|
|
56
|
+
review.
|
|
57
|
+
|
|
58
|
+
The panel is a `Popover` (`role="dialog"`) rather than a `Menu` for the same reason:
|
|
59
|
+
`role="menu"` permits only the `menuitem` family, and a `menuitem` is something you **do**,
|
|
60
|
+
not somewhere you **go**.
|
|
61
|
+
|
|
62
|
+
## Measured, not breakpointed
|
|
63
|
+
|
|
64
|
+
There is no `collapseAt` prop and no media query. The bar reads the width its links
|
|
65
|
+
actually have and folds when their sum stops fitting — so a longer label, a tenth
|
|
66
|
+
destination, `data-density="kiosk"`, or a reader at 175% browser zoom each move the fold on
|
|
67
|
+
their own.
|
|
68
|
+
|
|
69
|
+
mux-magic collapses at a hardcoded `480px` and plex-channels at `760px`. Neither number has
|
|
70
|
+
a reason, and both are wrong the first time anything about the content changes.
|
|
71
|
+
|
|
72
|
+
<Canvas of={NavBarStories.Responsive} />
|
|
73
|
+
|
|
74
|
+
## ⚠️ The bar must not be sized by its own contents
|
|
75
|
+
|
|
76
|
+
It measures its own box to decide what to draw in it, so a container that shrink-wraps
|
|
77
|
+
makes the answer its own input: fewer links → a narrower box → fewer links.
|
|
78
|
+
|
|
79
|
+
The root carries `min-w-0 flex-1`, which is the whole fix inside a flex row. A grid track is
|
|
80
|
+
the caller's to declare:
|
|
81
|
+
|
|
82
|
+
```css
|
|
83
|
+
grid-template-columns: auto minmax(0, 1fr) auto; /* brand, nav, actions */
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`auto` on that middle track is the bug — it floors at the nav's min-content width, which is
|
|
87
|
+
the sum of every link.
|
|
88
|
+
|
|
89
|
+
## Which destination is current
|
|
90
|
+
|
|
91
|
+
The library is router-agnostic (see `RouterLinkProvider`), so it cannot ask a router which
|
|
92
|
+
link is active. The app hands it one string:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<NavBar currentHref={useLocation().pathname} items={DESTINATIONS} label="Main" />
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`getIsCurrentHref` decides the rest, and it is exported so an app can ask the same question
|
|
99
|
+
elsewhere:
|
|
100
|
+
|
|
101
|
+
- **A parent path is current for its children.** `/tasks` is current on `/tasks/41`,
|
|
102
|
+
because a nav item names a section and a section does not stop being the one you are in
|
|
103
|
+
when you open something inside it. The match is on whole segments, so `/task` is not
|
|
104
|
+
current for `/tasks`.
|
|
105
|
+
- **`/` is exact.** react-router's `<NavLink to="/">` is active on *every* route unless the
|
|
106
|
+
caller remembers `end`, which is the footgun behind every "why is Home always
|
|
107
|
+
highlighted" bug. There is no prop to change it here: a nav with two current items has
|
|
108
|
+
stopped saying anything.
|
|
109
|
+
- **The query string and the fragment are not part of the answer.**
|
|
110
|
+
`/backlog?focus=search` is still the Backlog.
|
|
111
|
+
|
|
112
|
+
The current destination gets `aria-current="page"` as well as the fill, because a highlight
|
|
113
|
+
is invisible to a screen reader and to anyone who cannot separate the two surfaces.
|
|
114
|
+
|
|
115
|
+
## Mounted exactly once
|
|
116
|
+
|
|
117
|
+
A destination is in the bar or in the panel, never both.
|
|
118
|
+
|
|
119
|
+
The fleet's habit is to render the whole nav twice and hide one copy with `hidden md:flex`
|
|
120
|
+
— mux-magic's `PageHeader` across two ~55-line blocks, mail-sifter's `TriageQueue` again.
|
|
121
|
+
Every link is then in the DOM at every width, so an agent driving the page finds two of
|
|
122
|
+
each and cannot tell which one a human can see, the two copies drift, and every test has to
|
|
123
|
+
be defensively scoped.
|
|
124
|
+
|
|
125
|
+
<Canvas of={NavBarStories.AllStates} />
|
|
126
|
+
|
|
127
|
+
## A fold control belongs at an edge
|
|
128
|
+
|
|
129
|
+
`menuAlign="end"` puts the trigger beside the header's own actions rather than leaving it
|
|
130
|
+
hugging the wordmark with the whole track empty after it. It reaches **only** the folded
|
|
131
|
+
row, which holds exactly one item — so where the links sit when the bar is whole is not a
|
|
132
|
+
prop, and cannot be made inconsistent with the rest of the header.
|
|
133
|
+
|
|
134
|
+
## The trigger's glyph is the app's
|
|
135
|
+
|
|
136
|
+
The library ships no icons, so `menuIcon` has no default — U+2630 renders as nothing where
|
|
137
|
+
the font lacks it, which includes this sandbox's chromium and the kiosk image. With no icon
|
|
138
|
+
the trigger is a text button reading `menuLabel`, which works everywhere.
|
|
139
|
+
|
|
140
|
+
`menuLabel` names the trigger **and** the panel it opens: a `role="dialog"` takes no
|
|
141
|
+
accessible name from its content.
|
|
142
|
+
|
|
143
|
+
## Keyboard
|
|
144
|
+
|
|
145
|
+
Every link in the bar is its own tab stop, which is what a `<nav>` is — this is not a
|
|
146
|
+
`role="toolbar"`, and there is no roving focus to learn.
|
|
147
|
+
|
|
148
|
+
Folded, it is one tab stop: Enter or Space opens the panel, focus moves into it, Escape
|
|
149
|
+
closes it and hands focus back to the trigger. Choosing a destination closes it too —
|
|
150
|
+
unless the click was modified, because a ctrl-click leaves the reader exactly where they
|
|
151
|
+
were and shutting the menu they are still reading is how people stop ctrl-clicking.
|
|
152
|
+
|
|
153
|
+
<Canvas of={NavBarStories.Interactive} />
|
|
154
|
+
|
|
155
|
+
## In a shell
|
|
156
|
+
|
|
157
|
+
<Canvas of={NavBarStories.InAppShell} />
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
controlSizeArgType,
|
|
6
|
+
placementArgType,
|
|
7
|
+
} from "../argTypes.storyHelpers.ts"
|
|
8
|
+
import { Button } from "../Button/Button.tsx"
|
|
9
|
+
import {
|
|
10
|
+
ContainerBoard,
|
|
11
|
+
StoryCell,
|
|
12
|
+
StoryGrid,
|
|
13
|
+
} from "../board.storyHelpers.tsx"
|
|
14
|
+
import { Header } from "../Header/Header.tsx"
|
|
15
|
+
import { MenuIcon } from "../icons.storyHelpers.tsx"
|
|
16
|
+
import { Main } from "../Main/Main.tsx"
|
|
17
|
+
import { RouterLinkProvider } from "../RouterLink/RouterLinkProvider.tsx"
|
|
18
|
+
import type { RouterLinkProps } from "../RouterLink/routerLink.ts"
|
|
19
|
+
import { Shell } from "../Shell/Shell.tsx"
|
|
20
|
+
import { PageContent } from "../shell.storyHelpers.tsx"
|
|
21
|
+
import type { NavBarItem } from "./NavBar.tsx"
|
|
22
|
+
import { NavBar } from "./NavBar.tsx"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A stand-in for react-router's `<Link>`, and it does the one thing
|
|
26
|
+
* the plain `SoftRouterLink` in the other link boards does not:
|
|
27
|
+
* `preventDefault()`.
|
|
28
|
+
*
|
|
29
|
+
* That is not a test convenience, it is what a router does — it
|
|
30
|
+
* intercepts the plain left click and pushes history instead of
|
|
31
|
+
* letting the browser leave the page. Without it these boards
|
|
32
|
+
* genuinely navigate: the first run of `NavBar.test.tsx` clicked
|
|
33
|
+
* "Phases" and took the whole vitest iframe to `/phases`, which is
|
|
34
|
+
* the most direct proof available that these rows are real anchors
|
|
35
|
+
* and not `<button>`s.
|
|
36
|
+
*
|
|
37
|
+
* The `href` is untouched, so middle-click and ctrl-click still go
|
|
38
|
+
* to the platform — exactly as they do in an app.
|
|
39
|
+
*/
|
|
40
|
+
const SoftRouterLink = ({
|
|
41
|
+
href,
|
|
42
|
+
onClick,
|
|
43
|
+
...linkProps
|
|
44
|
+
}: RouterLinkProps): ReactNode => (
|
|
45
|
+
<a
|
|
46
|
+
{...linkProps}
|
|
47
|
+
data-router="soft"
|
|
48
|
+
href={href}
|
|
49
|
+
onClick={(event) => {
|
|
50
|
+
// The caller's handler first, then the interception — which
|
|
51
|
+
// is react-router's own order in `Link`, and it is what lets
|
|
52
|
+
// `NavBar` see a click that has not been cancelled yet.
|
|
53
|
+
onClick?.(event)
|
|
54
|
+
|
|
55
|
+
event.preventDefault()
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Docket's nine, in the product's own order — the nav that outgrew
|
|
62
|
+
* a single line and is the reason this component exists. The order
|
|
63
|
+
* is the pipeline work moves along, which is exactly why it may not
|
|
64
|
+
* be split across a bar and a menu.
|
|
65
|
+
*/
|
|
66
|
+
const DESTINATIONS: NavBarItem[] = [
|
|
67
|
+
{ href: "/triage", label: "Triage" },
|
|
68
|
+
{ href: "/backlog", label: "Backlog" },
|
|
69
|
+
{ href: "/phases", label: "Phases" },
|
|
70
|
+
{ href: "/lanes", label: "Lanes" },
|
|
71
|
+
{ href: "/board", label: "Board" },
|
|
72
|
+
{ href: "/archives", label: "Archives" },
|
|
73
|
+
{ href: "/tonight", label: "Tonight" },
|
|
74
|
+
{ href: "/chats", label: "Chats" },
|
|
75
|
+
{ href: "/settings", label: "Settings" },
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
const FEW_DESTINATIONS = DESTINATIONS.slice(0, 3)
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A fixed-width box, because the whole component is a measurement.
|
|
82
|
+
* A `layout: "centered"` story would size itself to its contents
|
|
83
|
+
* and never fold at all.
|
|
84
|
+
*/
|
|
85
|
+
const Frame = ({
|
|
86
|
+
children,
|
|
87
|
+
inlineSize,
|
|
88
|
+
}: {
|
|
89
|
+
children: ReactNode
|
|
90
|
+
inlineSize: string
|
|
91
|
+
}): ReactNode => (
|
|
92
|
+
<div
|
|
93
|
+
className="flex rounded-md border border-border-subtle bg-surface-raised p-2"
|
|
94
|
+
style={{ inlineSize }}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const meta = {
|
|
101
|
+
title: "Components/Layout/NavBar",
|
|
102
|
+
component: NavBar,
|
|
103
|
+
decorators: [
|
|
104
|
+
(Story) => (
|
|
105
|
+
<RouterLinkProvider link={SoftRouterLink}>
|
|
106
|
+
<Story />
|
|
107
|
+
</RouterLinkProvider>
|
|
108
|
+
),
|
|
109
|
+
],
|
|
110
|
+
parameters: { layout: "padded" },
|
|
111
|
+
argTypes: {
|
|
112
|
+
menuPlacement: placementArgType,
|
|
113
|
+
size: controlSizeArgType,
|
|
114
|
+
},
|
|
115
|
+
args: {
|
|
116
|
+
currentHref: "/board",
|
|
117
|
+
items: DESTINATIONS,
|
|
118
|
+
label: "Main",
|
|
119
|
+
menuIcon: <MenuIcon />,
|
|
120
|
+
menuLabel: "Main menu",
|
|
121
|
+
size: "sm",
|
|
122
|
+
},
|
|
123
|
+
} satisfies Meta<typeof NavBar>
|
|
124
|
+
|
|
125
|
+
export default meta
|
|
126
|
+
|
|
127
|
+
type Story = StoryObj<typeof meta>
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Wide enough for all nine, so there is **no menu button at all** —
|
|
131
|
+
* a trigger that shows at every width is not a fold, it is a nav
|
|
132
|
+
* you have permanently hidden.
|
|
133
|
+
*/
|
|
134
|
+
export const Default: Story = {
|
|
135
|
+
render: (navBarProps) => (
|
|
136
|
+
<Frame inlineSize="52rem">
|
|
137
|
+
<NavBar {...navBarProps} />
|
|
138
|
+
</Frame>
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The two states, and there is deliberately no third.
|
|
144
|
+
*
|
|
145
|
+
* `Toolbar` drops items one at a time from the end because its items
|
|
146
|
+
* are **ranked**. A nav's order is the product's shape, not a
|
|
147
|
+
* priority list, so half of it in the bar and half behind a button
|
|
148
|
+
* is a rule no reader can learn.
|
|
149
|
+
*/
|
|
150
|
+
export const AllVariants: Story = {
|
|
151
|
+
render: ({ currentHref, size }) => (
|
|
152
|
+
<StoryGrid columns={1}>
|
|
153
|
+
<StoryCell label="It fits — every destination is a link">
|
|
154
|
+
<Frame inlineSize="52rem">
|
|
155
|
+
<NavBar
|
|
156
|
+
currentHref={currentHref}
|
|
157
|
+
items={DESTINATIONS}
|
|
158
|
+
label="Main, wide"
|
|
159
|
+
menuIcon={<MenuIcon />}
|
|
160
|
+
menuLabel="Main menu"
|
|
161
|
+
size={size}
|
|
162
|
+
/>
|
|
163
|
+
</Frame>
|
|
164
|
+
</StoryCell>
|
|
165
|
+
|
|
166
|
+
<StoryCell label="It does not fit — all nine fold together">
|
|
167
|
+
<Frame inlineSize="18rem">
|
|
168
|
+
<NavBar
|
|
169
|
+
currentHref={currentHref}
|
|
170
|
+
items={DESTINATIONS}
|
|
171
|
+
label="Main, narrow"
|
|
172
|
+
menuIcon={<MenuIcon />}
|
|
173
|
+
menuLabel="Main menu"
|
|
174
|
+
size={size}
|
|
175
|
+
/>
|
|
176
|
+
</Frame>
|
|
177
|
+
</StoryCell>
|
|
178
|
+
|
|
179
|
+
<StoryCell
|
|
180
|
+
label={
|
|
181
|
+
'Folded, menuAlign="end" — the trigger joins the header\'s own actions'
|
|
182
|
+
}
|
|
183
|
+
>
|
|
184
|
+
<Frame inlineSize="18rem">
|
|
185
|
+
<NavBar
|
|
186
|
+
currentHref={currentHref}
|
|
187
|
+
items={DESTINATIONS}
|
|
188
|
+
label="Main, trigger at the end"
|
|
189
|
+
menuAlign="end"
|
|
190
|
+
menuIcon={<MenuIcon />}
|
|
191
|
+
menuLabel="Main menu"
|
|
192
|
+
size={size}
|
|
193
|
+
/>
|
|
194
|
+
</Frame>
|
|
195
|
+
</StoryCell>
|
|
196
|
+
|
|
197
|
+
<StoryCell label="No icon — the trigger is its own label">
|
|
198
|
+
<Frame inlineSize="18rem">
|
|
199
|
+
<NavBar
|
|
200
|
+
currentHref={currentHref}
|
|
201
|
+
items={DESTINATIONS}
|
|
202
|
+
label="Main, worded trigger"
|
|
203
|
+
menuLabel="Menu"
|
|
204
|
+
size={size}
|
|
205
|
+
/>
|
|
206
|
+
</Frame>
|
|
207
|
+
</StoryCell>
|
|
208
|
+
</StoryGrid>
|
|
209
|
+
),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Folded and open is the state nobody screenshots, and it is the
|
|
214
|
+
* one that matters: the panel is the **complete** list, in the
|
|
215
|
+
* product's order, with the current destination marked in it.
|
|
216
|
+
*/
|
|
217
|
+
export const AllStates: Story = {
|
|
218
|
+
render: ({ size }) => (
|
|
219
|
+
<StoryGrid columns={1}>
|
|
220
|
+
<StoryCell label="Current destination, in the bar">
|
|
221
|
+
<Frame inlineSize="52rem">
|
|
222
|
+
<NavBar
|
|
223
|
+
currentHref="/board"
|
|
224
|
+
items={DESTINATIONS}
|
|
225
|
+
label="Main, current in bar"
|
|
226
|
+
menuIcon={<MenuIcon />}
|
|
227
|
+
menuLabel="Main menu"
|
|
228
|
+
size={size}
|
|
229
|
+
/>
|
|
230
|
+
</Frame>
|
|
231
|
+
</StoryCell>
|
|
232
|
+
|
|
233
|
+
<StoryCell label="On a route no destination owns — nothing is current">
|
|
234
|
+
<Frame inlineSize="52rem">
|
|
235
|
+
<NavBar
|
|
236
|
+
currentHref="/tasks/41"
|
|
237
|
+
items={FEW_DESTINATIONS}
|
|
238
|
+
label="Main, nothing current"
|
|
239
|
+
menuIcon={<MenuIcon />}
|
|
240
|
+
menuLabel="Main menu"
|
|
241
|
+
size={size}
|
|
242
|
+
/>
|
|
243
|
+
</Frame>
|
|
244
|
+
</StoryCell>
|
|
245
|
+
|
|
246
|
+
<StoryCell label="Folded, menu open — the complete list">
|
|
247
|
+
<Frame inlineSize="18rem">
|
|
248
|
+
<NavBar
|
|
249
|
+
currentHref="/board"
|
|
250
|
+
isMenuVisible
|
|
251
|
+
items={DESTINATIONS}
|
|
252
|
+
label="Main, folded and open"
|
|
253
|
+
menuIcon={<MenuIcon />}
|
|
254
|
+
menuLabel="Main menu"
|
|
255
|
+
size={size}
|
|
256
|
+
/>
|
|
257
|
+
</Frame>
|
|
258
|
+
</StoryCell>
|
|
259
|
+
</StoryGrid>
|
|
260
|
+
),
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Measured rather than breakpointed. The same nine destinations at
|
|
265
|
+
* three container widths, with no pixel number anywhere in the
|
|
266
|
+
* component — so a longer label, a tenth destination or a denser
|
|
267
|
+
* theme moves the fold on its own.
|
|
268
|
+
*/
|
|
269
|
+
export const Responsive: Story = {
|
|
270
|
+
render: ({ currentHref, size }) => (
|
|
271
|
+
<ContainerBoard>
|
|
272
|
+
{(width) => (
|
|
273
|
+
<Frame inlineSize="100%">
|
|
274
|
+
<NavBar
|
|
275
|
+
currentHref={currentHref}
|
|
276
|
+
items={DESTINATIONS}
|
|
277
|
+
label={`Main at ${width}`}
|
|
278
|
+
menuIcon={<MenuIcon />}
|
|
279
|
+
menuLabel="Main menu"
|
|
280
|
+
size={size}
|
|
281
|
+
/>
|
|
282
|
+
</Frame>
|
|
283
|
+
)}
|
|
284
|
+
</ContainerBoard>
|
|
285
|
+
),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Tab to the trigger, Enter to open, and focus moves into the
|
|
290
|
+
* panel; Escape closes it and hands focus back. Every row is a real
|
|
291
|
+
* `<a href>`, so ctrl-click and middle-click still open a tab —
|
|
292
|
+
* which is the entire reason this is not a `Menu`.
|
|
293
|
+
*/
|
|
294
|
+
export const Interactive: Story = {
|
|
295
|
+
render: ({ currentHref, size }) => (
|
|
296
|
+
<Frame inlineSize="18rem">
|
|
297
|
+
<NavBar
|
|
298
|
+
currentHref={currentHref}
|
|
299
|
+
items={DESTINATIONS}
|
|
300
|
+
label="Main, keyboard"
|
|
301
|
+
menuIcon={<MenuIcon />}
|
|
302
|
+
menuLabel="Main menu"
|
|
303
|
+
size={size}
|
|
304
|
+
/>
|
|
305
|
+
</Frame>
|
|
306
|
+
),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Where it actually goes: `Shell` → `Header`, with the wordmark
|
|
311
|
+
* before it and the page's own action after it.
|
|
312
|
+
*
|
|
313
|
+
* The header row is a flex line, so the bar's `flex-1 min-w-0` is
|
|
314
|
+
* what gives it a box sized by the row rather than by its own
|
|
315
|
+
* links — the one arrangement rule the component cannot enforce
|
|
316
|
+
* from the inside.
|
|
317
|
+
*/
|
|
318
|
+
const AppShellHarness = (): ReactNode => (
|
|
319
|
+
<Shell>
|
|
320
|
+
<Header
|
|
321
|
+
actions={
|
|
322
|
+
<Button intent="accent" size="sm">
|
|
323
|
+
Add Task
|
|
324
|
+
</Button>
|
|
325
|
+
}
|
|
326
|
+
heading="Docket"
|
|
327
|
+
>
|
|
328
|
+
<NavBar
|
|
329
|
+
currentHref="/board"
|
|
330
|
+
items={DESTINATIONS}
|
|
331
|
+
label="Main"
|
|
332
|
+
// The header's own action is at the far edge, so the fold
|
|
333
|
+
// control joins it rather than hugging the wordmark with
|
|
334
|
+
// the whole track empty after it.
|
|
335
|
+
menuAlign="end"
|
|
336
|
+
menuIcon={<MenuIcon />}
|
|
337
|
+
menuLabel="Main menu"
|
|
338
|
+
/>
|
|
339
|
+
</Header>
|
|
340
|
+
|
|
341
|
+
<Main>
|
|
342
|
+
<PageContent />
|
|
343
|
+
</Main>
|
|
344
|
+
</Shell>
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
export const InAppShell: Story = {
|
|
348
|
+
parameters: { layout: "fullscreen" },
|
|
349
|
+
render: () => <AppShellHarness />,
|
|
350
|
+
}
|