@charcuterie/ui 3.23.0 → 3.24.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/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/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 +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -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/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 +20 -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
|
@@ -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
|
+
}
|