@charcuterie/ui 3.22.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.
Files changed (76) hide show
  1. package/README.md +11 -0
  2. package/dist/Board/Board.d.ts +6 -3
  3. package/dist/Board/Board.d.ts.map +1 -1
  4. package/dist/Board/Board.js +19 -1
  5. package/dist/Board/Board.js.map +1 -1
  6. package/dist/Board/BoardCard.d.ts +42 -7
  7. package/dist/Board/BoardCard.d.ts.map +1 -1
  8. package/dist/Board/BoardCard.js +54 -8
  9. package/dist/Board/BoardCard.js.map +1 -1
  10. package/dist/Board/useBoardDrag.d.ts +15 -1
  11. package/dist/Board/useBoardDrag.d.ts.map +1 -1
  12. package/dist/Board/useBoardDrag.js +3 -3
  13. package/dist/Board/useBoardDrag.js.map +1 -1
  14. package/dist/Nav/Nav.d.ts +167 -0
  15. package/dist/Nav/Nav.d.ts.map +1 -0
  16. package/dist/Nav/Nav.js +165 -0
  17. package/dist/Nav/Nav.js.map +1 -0
  18. package/dist/Nav/NavLink.d.ts +43 -0
  19. package/dist/Nav/NavLink.d.ts.map +1 -0
  20. package/dist/Nav/NavLink.js +111 -0
  21. package/dist/Nav/NavLink.js.map +1 -0
  22. package/dist/Nav/navItems.d.ts +67 -0
  23. package/dist/Nav/navItems.d.ts.map +1 -0
  24. package/dist/Nav/navItems.js +104 -0
  25. package/dist/Nav/navItems.js.map +1 -0
  26. package/dist/Nav/useNavLayout.d.ts +86 -0
  27. package/dist/Nav/useNavLayout.d.ts.map +1 -0
  28. package/dist/Nav/useNavLayout.js +114 -0
  29. package/dist/Nav/useNavLayout.js.map +1 -0
  30. package/dist/NavBar/NavBar.d.ts +37 -111
  31. package/dist/NavBar/NavBar.d.ts.map +1 -1
  32. package/dist/NavBar/NavBar.js +30 -110
  33. package/dist/NavBar/NavBar.js.map +1 -1
  34. package/dist/ReorderList/ReorderList.d.ts +131 -0
  35. package/dist/ReorderList/ReorderList.d.ts.map +1 -0
  36. package/dist/ReorderList/ReorderList.js +154 -0
  37. package/dist/ReorderList/ReorderList.js.map +1 -0
  38. package/dist/index.d.ts +8 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -1
  41. package/dist/index.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/Board/Board.mdx +25 -2
  44. package/src/Board/Board.stories.tsx +52 -1
  45. package/src/Board/Board.test.tsx +82 -0
  46. package/src/Board/Board.tsx +24 -4
  47. package/src/Board/BoardCard.tsx +103 -21
  48. package/src/Board/boardMoveHandleContainer.test.ts +54 -0
  49. package/src/Board/useBoardDrag.ts +17 -2
  50. package/src/Nav/Nav.mdx +169 -0
  51. package/src/Nav/Nav.stories.tsx +402 -0
  52. package/src/Nav/Nav.test.tsx +338 -0
  53. package/src/Nav/Nav.tsx +406 -0
  54. package/src/Nav/NavLink.tsx +220 -0
  55. package/src/Nav/navItems.test.ts +163 -0
  56. package/src/Nav/navItems.ts +175 -0
  57. package/src/Nav/useNavLayout.ts +197 -0
  58. package/src/NavBar/NavBar.mdx +26 -0
  59. package/src/NavBar/NavBar.tsx +54 -240
  60. package/src/ReorderList/ReorderList.mdx +83 -0
  61. package/src/ReorderList/ReorderList.stories.tsx +149 -0
  62. package/src/ReorderList/ReorderList.test.tsx +190 -0
  63. package/src/ReorderList/ReorderList.tsx +331 -0
  64. package/src/index.ts +26 -1
  65. package/src/sourceRules.test.ts +26 -1
  66. package/dist/NavBar/NavBarLink.d.ts +0 -43
  67. package/dist/NavBar/NavBarLink.d.ts.map +0 -1
  68. package/dist/NavBar/NavBarLink.js +0 -67
  69. package/dist/NavBar/NavBarLink.js.map +0 -1
  70. package/dist/NavBar/navBarCurrent.d.ts +0 -37
  71. package/dist/NavBar/navBarCurrent.d.ts.map +0 -1
  72. package/dist/NavBar/navBarCurrent.js +0 -48
  73. package/dist/NavBar/navBarCurrent.js.map +0 -1
  74. package/src/NavBar/NavBarLink.tsx +0 -139
  75. package/src/NavBar/navBarCurrent.test.ts +0 -81
  76. package/src/NavBar/navBarCurrent.ts +0 -64
@@ -0,0 +1,190 @@
1
+ import { composeStories } from "@storybook/react"
2
+ import {
3
+ expect,
4
+ userEvent,
5
+ type within,
6
+ } from "storybook/test"
7
+ import { test } from "vitest"
8
+
9
+ import { expectNoAxeViolations } from "../expectNoAxeViolations.testHelpers.ts"
10
+ import { mountStory } from "../mountStory.testHelpers.ts"
11
+ import * as stories from "./ReorderList.stories.tsx"
12
+
13
+ const { Default, SingleItem } = composeStories(stories)
14
+
15
+ /**
16
+ * The rows come back in the DOM order the list draws them, which is
17
+ * the order under test. Reading the labels rather than the positions
18
+ * is deliberate: the printed number is derived from the index, so
19
+ * asserting on it would pass even if the rows had not moved.
20
+ */
21
+ const getRowLabels = (canvas: ReturnType<typeof within>) =>
22
+ canvas
23
+ .getAllByRole("listitem")
24
+ .map((row: HTMLElement) => row.textContent ?? "")
25
+ .filter((text: string) => text !== "")
26
+
27
+ test("moves a row later with the button, and says where it went", async () => {
28
+ const { canvas } = await mountStory(Default)
29
+
30
+ await userEvent.click(
31
+ canvas.getByRole("button", {
32
+ name: "Move Measure the alcove later",
33
+ }),
34
+ )
35
+
36
+ expect(getRowLabels(canvas)[0]).toContain(
37
+ "Cut the shelf to width",
38
+ )
39
+
40
+ expect(getRowLabels(canvas)[1]).toContain(
41
+ "Measure the alcove",
42
+ )
43
+
44
+ /*
45
+ * The POSITION, not just the fact of a move. "Moved" leaves a
46
+ * screen-reader user unable to tell the top of a list from the
47
+ * bottom of thirty.
48
+ */
49
+ expect(
50
+ canvas.getByRole("status", {
51
+ name: "Shelf build activity",
52
+ }),
53
+ ).toHaveTextContent(
54
+ "Moved Measure the alcove to position 2 of 5.",
55
+ )
56
+ })
57
+
58
+ test("moves a row earlier, and the ends stay disabled", async () => {
59
+ const { canvas } = await mountStory(Default)
60
+
61
+ expect(
62
+ canvas.getByRole("button", {
63
+ name: "Move Measure the alcove earlier",
64
+ }),
65
+ ).toBeDisabled()
66
+
67
+ expect(
68
+ canvas.getByRole("button", {
69
+ name: "Move Check it is level later",
70
+ }),
71
+ ).toBeDisabled()
72
+
73
+ await userEvent.click(
74
+ canvas.getByRole("button", {
75
+ name: "Move Sand the cut edge earlier",
76
+ }),
77
+ )
78
+
79
+ expect(getRowLabels(canvas)[1]).toContain(
80
+ "Sand the cut edge",
81
+ )
82
+ })
83
+
84
+ /**
85
+ * The handle is a pointer enhancement, so it must never be the only
86
+ * way to move a row — and on a list that cannot be reordered it must
87
+ * not advertise a gesture that does nothing.
88
+ */
89
+ test("a one-row list offers no drag handle", async () => {
90
+ const { canvas } = await mountStory(SingleItem)
91
+
92
+ expect(canvas.queryByTitle("Drag to reorder")).toBeNull()
93
+ })
94
+
95
+ test("the handle takes the gesture away from the browser's scroll", async () => {
96
+ const { canvas } = await mountStory(Default)
97
+
98
+ const grip = canvas.getAllByTitle("Drag to reorder")[0]
99
+
100
+ /*
101
+ * `touch-action: none` is the whole difference between a drag
102
+ * that works on a desktop and one that also works on the tablet
103
+ * this component was built for. Without it the browser claims
104
+ * the gesture for a scroll and the pointermove stream stops one
105
+ * frame in.
106
+ */
107
+ expect(grip).toHaveStyle({ touchAction: "none" })
108
+ })
109
+
110
+ /**
111
+ * The gesture itself, driven as a real pointer rather than as
112
+ * dispatched `PointerEvent`s.
113
+ *
114
+ * A hand-built event is not enough here, and the difference is not
115
+ * pedantry: `setPointerCapture` rejects an id that matches no
116
+ * ACTIVE pointer, so a synthetic sequence dies inside the handler
117
+ * before a single listener is registered — and the test then
118
+ * passes or fails for a reason the component does not have.
119
+ */
120
+ test("drags a row past the one below it", async () => {
121
+ const { canvas } = await mountStory(Default)
122
+
123
+ const grip = canvas.getAllByTitle("Drag to reorder")[0]
124
+
125
+ const secondRow = canvas.getAllByRole("listitem")[1]
126
+
127
+ if (!grip || !secondRow) {
128
+ throw new Error("The story drew no rows to drag.")
129
+ }
130
+
131
+ const to = secondRow.getBoundingClientRect()
132
+
133
+ await userEvent.pointer([
134
+ { keys: "[MouseLeft>]", target: grip },
135
+ {
136
+ coords: {
137
+ x: to.x + to.width / 2,
138
+ y: to.y + to.height - 2,
139
+ },
140
+ },
141
+ { keys: "[/MouseLeft]" },
142
+ ])
143
+
144
+ expect(getRowLabels(canvas)[0]).toContain(
145
+ "Cut the shelf to width",
146
+ )
147
+
148
+ expect(getRowLabels(canvas)[1]).toContain(
149
+ "Measure the alcove",
150
+ )
151
+ })
152
+
153
+ /**
154
+ * A press that goes nowhere must move nothing.
155
+ *
156
+ * The movement threshold is the reason the position can be both a
157
+ * printed number and a handle: without it, a handle is a control
158
+ * you cannot press by accident and then change your mind about,
159
+ * and every stray tap on a row reorders the list.
160
+ */
161
+ test("a press with no movement reorders nothing", async () => {
162
+ const { canvas } = await mountStory(Default)
163
+
164
+ const grip = canvas.getAllByTitle("Drag to reorder")[0]
165
+
166
+ if (!grip) {
167
+ throw new Error("The story drew no rows to drag.")
168
+ }
169
+
170
+ await userEvent.pointer([
171
+ { keys: "[MouseLeft>]", target: grip },
172
+ { keys: "[/MouseLeft]", target: grip },
173
+ ])
174
+
175
+ expect(getRowLabels(canvas)[0]).toContain(
176
+ "Measure the alcove",
177
+ )
178
+
179
+ expect(
180
+ canvas.getByRole("status", {
181
+ name: "Shelf build activity",
182
+ }),
183
+ ).toHaveTextContent("")
184
+ })
185
+
186
+ test("has no axe violations", async () => {
187
+ const { canvasElement } = await mountStory(Default)
188
+
189
+ await expectNoAxeViolations(canvasElement)
190
+ })
@@ -0,0 +1,331 @@
1
+ import type { ReactNode, Ref } from "react"
2
+ import { useCallback, useState } from "react"
3
+
4
+ import {
5
+ getIsMoveMeaningful,
6
+ toSettledIndex,
7
+ } from "../Board/boardMove.ts"
8
+ import { useBoardDrag } from "../Board/useBoardDrag.ts"
9
+ import { toClassName } from "../toClassName.ts"
10
+ import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.tsx"
11
+
12
+ /**
13
+ * The attribute the drag's hit test reads on a row. A `data-`
14
+ * attribute rather than a class, the same contract `BoardCard` and
15
+ * `DropRail` already write down: a class is a style and a
16
+ * consumer's tooling may rename it.
17
+ */
18
+ const ITEM_ATTRIBUTE = "data-reorder-item"
19
+
20
+ /**
21
+ * The single lane this component registers with the board's drag.
22
+ * A reorder list is a board with one lane and no lane picker, so
23
+ * the key is a constant and never leaves this file.
24
+ */
25
+ const LANE_KEY = "reorder-list"
26
+
27
+ export type ReorderListItem = {
28
+ key: string
29
+ /**
30
+ * Plain text, and it is not optional. Every accessible name the
31
+ * component writes is built from it — "Move <label> earlier" —
32
+ * and so is the sentence the live region reads after a move. A
33
+ * button announced as "↑" names nothing.
34
+ */
35
+ label: string
36
+ }
37
+
38
+ export type ReorderListRenderArguments<
39
+ TItem extends ReorderListItem,
40
+ > = {
41
+ /**
42
+ * Spread onto whatever the host wants the drag to start FROM.
43
+ * Absent — as an empty object — when the list is too short to
44
+ * reorder, so a one-row list has no dead handle on it.
45
+ *
46
+ * It is a **handle**, never the whole row: a row holds links,
47
+ * and a link is draggable already.
48
+ */
49
+ gripProps: {
50
+ onPointerDown?: (event: {
51
+ clientX: number
52
+ clientY: number
53
+ currentTarget: EventTarget & Element
54
+ pointerId: number
55
+ }) => void
56
+ style?: { touchAction: "none" }
57
+ title?: string
58
+ }
59
+ index: number
60
+ isDragged: boolean
61
+ isFirst: boolean
62
+ isLast: boolean
63
+ item: TItem
64
+ /**
65
+ * The KEYBOARD path, and it is the primary one. The host draws
66
+ * the two buttons because only the host knows where they sit in
67
+ * its own row; the component supplies what they do and whether
68
+ * each end is disabled.
69
+ */
70
+ moveBy: (offset: -1 | 1) => void
71
+ /** 1-based, for printing. */
72
+ position: number
73
+ }
74
+
75
+ export type ReorderListProps<
76
+ TItem extends ReorderListItem,
77
+ > = {
78
+ className?: string
79
+ /**
80
+ * The container element. `ul` by default, because an ordered
81
+ * pile of rows is a list; `div` for a host whose own CSS grid
82
+ * owns the shape and would have to undo list semantics to get
83
+ * it.
84
+ */
85
+ elementType?: "div" | "ol" | "ul"
86
+ /**
87
+ * The container element, for a host that needs it too.
88
+ *
89
+ * The component keeps its own hold on that element — the drag
90
+ * measures its rows off it — so this is MERGED rather than
91
+ * forwarded. Docket plays a FLIP animation off the same
92
+ * element, and without this the two uses would have to fight
93
+ * over one `ref`.
94
+ */
95
+ elementRef?: Ref<HTMLElement>
96
+ itemClassName?: string
97
+ items: readonly TItem[]
98
+ /** The list's accessible name. */
99
+ label: string
100
+ /**
101
+ * `fromIndex` and `toIndex` are both indexes into `items`, and
102
+ * `toIndex` is already corrected for the row's own removal — so
103
+ * a handler is `splice(from, 1)` then `splice(to, 0, row)` with
104
+ * no arithmetic of its own.
105
+ */
106
+ onReorder: (fromIndex: number, toIndex: number) => void
107
+ renderItem: (
108
+ renderArguments: ReorderListRenderArguments<TItem>,
109
+ ) => ReactNode
110
+ }
111
+
112
+ /**
113
+ * A vertical list whose rows can be put in a different order — by
114
+ * two buttons, or by dragging a handle.
115
+ *
116
+ * ### Why it is not a fourth hand-rolled reorder
117
+ *
118
+ * Docket wrote this twice: a subtask checklist on HTML5
119
+ * `draggable`, and a phase queue with the buttons and no drag at
120
+ * all. `Board` wrote a third, properly, on Pointer Events. Three
121
+ * implementations of one shape is the case the workspace's
122
+ * component rule exists for, so the geometry here is **`Board`'s
123
+ * own** — `useBoardDrag` with a single registered lane and the
124
+ * item selector pointed at this component's rows. Nothing about
125
+ * the measuring, the snapshot, the threshold or the pointer
126
+ * capture is written twice.
127
+ *
128
+ * ### The buttons are primary, the drag is the enhancement
129
+ *
130
+ * WCAG 2.5.7 requires a single-pointer alternative to any dragging
131
+ * movement, and the board's decision already argues the stronger
132
+ * version: the buttons are not a fallback bolted onto a drag, they
133
+ * are the only path that works from a keyboard, under a screen
134
+ * reader, or under a thumb in the Narrow View. So `renderItem`
135
+ * receives `moveBy` before it receives `gripProps`, and a host
136
+ * that draws no handle at all is a supported host.
137
+ *
138
+ * ### It takes no drag-and-drop dependency
139
+ *
140
+ * The board priced four libraries at 7–32 KB gzipped and took none
141
+ * of them, because a published library's dependency list is
142
+ * inherited by every consumer
143
+ * ([decision](../../../docs/decisions/2026-08-19-the-board-owns-the-move-and-takes-no-drag-and-drop-dependency.md)).
144
+ * This adds nothing to that arithmetic — it is the same 1.4 KB,
145
+ * called twice.
146
+ *
147
+ * ### `draggable="true"` is the instinct this replaces
148
+ *
149
+ * Native HTML5 drag-and-drop **does not fire on touch at all**, so
150
+ * the checklist Docket shipped on it cannot be reordered on the
151
+ * tablet its owner reads it on. Pointer Events are one code path
152
+ * for mouse, touch and pen, and `setPointerCapture` keeps the drag
153
+ * alive once the row has moved out from under the finger.
154
+ *
155
+ * ### What it deliberately does not do
156
+ *
157
+ * It renders no row, no handle, no buttons and no glyph. The host
158
+ * renders all four through `renderItem`, which is what lets one
159
+ * component serve a checklist, a queue and a card list whose CSS
160
+ * grids have nothing in common.
161
+ */
162
+ export const ReorderList = <TItem extends ReorderListItem>({
163
+ className,
164
+ elementRef,
165
+ elementType = "ul",
166
+ itemClassName,
167
+ items,
168
+ label,
169
+ onReorder,
170
+ renderItem,
171
+ }: ReorderListProps<TItem>): ReactNode => {
172
+ const [announcement, setAnnouncement] = useState("")
173
+
174
+ /**
175
+ * A single row cannot be put anywhere else, so it gets no grip
176
+ * and no buttons. This is checked here rather than in every
177
+ * host, because "hide the control when it can do nothing" is the
178
+ * part each hand-rolled copy forgot.
179
+ */
180
+ const isReorderable = items.length > 1
181
+
182
+ const commit = useCallback(
183
+ (fromIndex: number, toIndex: number) => {
184
+ setAnnouncement(
185
+ `Moved ${items[fromIndex]?.label ?? ""} to position ${String(toIndex + 1)} of ${String(items.length)}.`,
186
+ )
187
+
188
+ onReorder(fromIndex, toIndex)
189
+ },
190
+ [items, onReorder],
191
+ )
192
+
193
+ const { drag, registerLane, startDrag } = useBoardDrag({
194
+ itemSelector: `[${ITEM_ATTRIBUTE}]`,
195
+ onDrop: (origin, target) => {
196
+ if (
197
+ !getIsMoveMeaningful({
198
+ fromIndex: origin.fromIndex,
199
+ fromLaneKey: origin.fromLaneKey,
200
+ toIndex: target.index,
201
+ toLaneKey: target.laneKey,
202
+ })
203
+ ) {
204
+ return
205
+ }
206
+
207
+ commit(
208
+ origin.fromIndex,
209
+ toSettledIndex({
210
+ fromIndex: origin.fromIndex,
211
+ fromLaneKey: origin.fromLaneKey,
212
+ toIndex: target.index,
213
+ toLaneKey: target.laneKey,
214
+ }),
215
+ )
216
+ },
217
+ })
218
+
219
+ const ListElement = elementType
220
+
221
+ const ItemElement =
222
+ elementType === "div" ? "div" : ("li" as const)
223
+
224
+ return (
225
+ <>
226
+ <ListElement
227
+ aria-label={label}
228
+ className={toClassName("relative", className)}
229
+ ref={(element: HTMLElement | null) => {
230
+ registerLane(LANE_KEY)(element)
231
+
232
+ if (typeof elementRef === "function") {
233
+ elementRef(element)
234
+
235
+ return
236
+ }
237
+
238
+ if (elementRef) {
239
+ elementRef.current = element
240
+ }
241
+ }}
242
+ >
243
+ {items.map((item, index) => (
244
+ <ItemElement
245
+ className={itemClassName}
246
+ key={item.key}
247
+ {...{ [ITEM_ATTRIBUTE]: item.key }}
248
+ >
249
+ {renderItem({
250
+ gripProps: isReorderable
251
+ ? {
252
+ onPointerDown: (pointerEvent) => {
253
+ startDrag(pointerEvent, {
254
+ fromIndex: index,
255
+ fromLaneKey: LANE_KEY,
256
+ itemKey: item.key,
257
+ })
258
+ },
259
+ /*
260
+ * Without it the browser claims the gesture
261
+ * for a scroll and the `pointermove` stream
262
+ * stops one frame in — which reads as "drag
263
+ * works on the desktop and not on the
264
+ * tablet", the exact bug this component
265
+ * exists to end.
266
+ */
267
+ style: { touchAction: "none" },
268
+ title: "Drag to reorder",
269
+ }
270
+ : {},
271
+ index,
272
+ isDragged: drag?.itemKey === item.key,
273
+ isFirst: index === 0,
274
+ isLast: index === items.length - 1,
275
+ item,
276
+ moveBy: (offset) => {
277
+ const toIndex = index + offset
278
+
279
+ if (
280
+ !isReorderable ||
281
+ toIndex < 0 ||
282
+ toIndex > items.length - 1
283
+ ) {
284
+ return
285
+ }
286
+
287
+ commit(index, toIndex)
288
+ },
289
+ position: index + 1,
290
+ })}
291
+ </ItemElement>
292
+ ))}
293
+
294
+ {/*
295
+ * A real element rather than a border on a row, and
296
+ * absolutely positioned against a snapshot taken at grab.
297
+ * Inserting it must not move the rows below it: that
298
+ * feedback loop is what makes an indicator jump, and the
299
+ * reasoning is written out in `useBoardDrag.ts`.
300
+ */}
301
+ {drag?.indicatorOffset == null ? null : (
302
+ <ItemElement
303
+ aria-hidden="true"
304
+ className="pointer-events-none absolute end-0 start-0 h-0.5 rounded-full bg-intent-accent-solid"
305
+ style={{
306
+ insetBlockStart: `${String(drag.indicatorOffset)}px`,
307
+ }}
308
+ />
309
+ )}
310
+ </ListElement>
311
+
312
+ {/*
313
+ * `aria-label` on a `role="status"`, per the library's own
314
+ * rule — a live region with no name is announced with no
315
+ * context, and this one fires while the reader's attention
316
+ * is on a row rather than on the list.
317
+ *
318
+ * It names the POSITION, because "moved" leaves a
319
+ * screen-reader user unable to tell the top of a list from
320
+ * the bottom of thirty. That is the one piece of feedback a
321
+ * sighted user gets free from watching the row land.
322
+ */}
323
+ <VisuallyHidden
324
+ aria-label={`${label} activity`}
325
+ role="status"
326
+ >
327
+ {announcement}
328
+ </VisuallyHidden>
329
+ </>
330
+ )
331
+ }
package/src/index.ts CHANGED
@@ -276,12 +276,31 @@ export type {
276
276
  export { Menu } from "./Menu/Menu.tsx"
277
277
  export type { ModalProps } from "./Modal/Modal.tsx"
278
278
  export { Modal } from "./Modal/Modal.tsx"
279
+ export type {
280
+ NavLayout,
281
+ NavProps,
282
+ } from "./Nav/Nav.tsx"
283
+ export { Nav } from "./Nav/Nav.tsx"
284
+ export type {
285
+ NavItem,
286
+ NavRailItem,
287
+ } from "./Nav/navItems.ts"
288
+ export {
289
+ getIsCurrentHref,
290
+ resolveActiveKey,
291
+ } from "./Nav/navItems.ts"
292
+ export type {
293
+ NavLayoutMode,
294
+ NavLayoutState,
295
+ ResolvedNavLayout,
296
+ UseNavLayoutOptions,
297
+ } from "./Nav/useNavLayout.ts"
298
+ export { useNavLayout } from "./Nav/useNavLayout.ts"
279
299
  export type {
280
300
  NavBarItem,
281
301
  NavBarProps,
282
302
  } from "./NavBar/NavBar.tsx"
283
303
  export { NavBar } from "./NavBar/NavBar.tsx"
284
- export { getIsCurrentHref } from "./NavBar/navBarCurrent.ts"
285
304
  export type {
286
305
  PickerOption,
287
306
  PickerProps,
@@ -316,6 +335,12 @@ export type {
316
335
  RailSide,
317
336
  } from "./Rail/Rail.tsx"
318
337
  export { Rail } from "./Rail/Rail.tsx"
338
+ export type {
339
+ ReorderListItem,
340
+ ReorderListProps,
341
+ ReorderListRenderArguments,
342
+ } from "./ReorderList/ReorderList.tsx"
343
+ export { ReorderList } from "./ReorderList/ReorderList.tsx"
319
344
  export { AnchorLink } from "./RouterLink/AnchorLink.tsx"
320
345
  export {
321
346
  RouterLinkProvider,
@@ -512,7 +512,32 @@ test("the barrel is the only place components are re-exported", async () => {
512
512
  // is too, while still being exported from the barrel — an app that
513
513
  // marks a destination current anywhere else must ask the same
514
514
  // question the same way.
515
- expect(componentNames.length).toBe(58)
515
+ //
516
+ // `ReorderList` — a vertical list put in a different order, by
517
+ // two buttons or by dragging a handle — is +1 -> 59. It is the
518
+ // clearest case yet for the fleet's build-it-here rule: Docket
519
+ // had written the shape twice (a subtask checklist on HTML5
520
+ // `draggable`, which does not fire on touch at all, and a phase
521
+ // queue with the buttons and no drag), and `Board` had written
522
+ // it a third time correctly. So this one does not fork the
523
+ // geometry — it CALLS `useBoardDrag` with a single registered
524
+ // lane and the item selector pointed at its own rows, the way
525
+ // `VirtualizedGrid` calls `useAdaptiveColumns`. It renders no
526
+ // row, no handle and no buttons; `renderItem` is what lets one
527
+ // component serve three grids that have nothing in common.
528
+ //
529
+ // `Nav` — the destinations as a rail, a collapsed icon rail or a
530
+ // bottom strip — is +1 -> 60. It is the other half of `NavBar`,
531
+ // which folds a horizontal row and cannot become a vertical one:
532
+ // no media query moves an element across `Shell`'s grid, so the
533
+ // rail is a different corner of the page rather than a restyling
534
+ // of the same one. Two members stay out of this count by the
535
+ // `<Name>/<Name>.tsx` rule and are exported from the barrel
536
+ // anyway — `navItems.ts`, because `resolveActiveKey` is the
537
+ // active-path rule an app's tests want to assert directly, and
538
+ // `useNavLayout.ts`, because the app has to read the width rule to
539
+ // know which corner to render into.
540
+ expect(componentNames.length).toBe(60)
516
541
 
517
542
  for (const name of componentNames) {
518
543
  expect(barrel).toContain(`export { ${name} }`)
@@ -1,43 +0,0 @@
1
- import type { ControlSize } from "@charcuterie/tokens";
2
- import type { ReactNode } from "react";
3
- import type { NavBarItem } from "./NavBar.tsx";
4
- export type NavBarLinkProps = {
5
- isCurrent: boolean;
6
- item: NavBarItem;
7
- /**
8
- * Fires when the reader activates this link **in a way that keeps
9
- * them on the page** — a plain left click, Enter, Space. It is
10
- * what closes the collapsed panel behind them.
11
- *
12
- * A modified click (ctrl, meta, shift, alt) opens a new tab and
13
- * leaves this document exactly where it was, so it deliberately
14
- * does not fire: shutting the menu the reader is still working
15
- * through is the behaviour that makes people stop ctrl-clicking.
16
- */
17
- onNavigate?: () => void;
18
- /** Where it is drawn. A bar row and a panel row paint differently. */
19
- placement: "bar" | "panel";
20
- size: ControlSize;
21
- /** Hands the bar row's box to the measuring hook. Bar only. */
22
- trackElement?: (key: string, element: HTMLElement | null) => void;
23
- };
24
- /**
25
- * One destination, drawn either in the bar or in the collapsed
26
- * panel — and it is a real `<a href>` in both.
27
- *
28
- * That is the whole reason this component exists rather than the
29
- * `Menu` a collapsed `Toolbar` opens: `MenuItem` is `{ label,
30
- * onSelect }`, so a nav folded into one becomes a row of
31
- * `<button>`s. It looks identical in a screenshot and it loses
32
- * middle-click, ctrl-click, "open in a new tab", "copy link
33
- * address", the status bar, and the browser's own visited styling.
34
- * `BuildingAnApp.mdx` names `<button onClick={navigate}>` as the
35
- * fleet's most common navigation defect; a collapse mechanism that
36
- * introduced it at narrow widths would be that defect on a timer.
37
- *
38
- * The router seam is `ButtonLink`'s, unchanged: an external
39
- * destination or a fragment falls back to a plain anchor rather than
40
- * being pushed onto a history stack that has no route for it.
41
- */
42
- export declare const NavBarLink: ({ isCurrent, item, onNavigate, placement, size, trackElement, }: NavBarLinkProps) => ReactNode;
43
- //# sourceMappingURL=NavBarLink.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"NavBarLink.d.ts","sourceRoot":"","sources":["../../src/NavBar/NavBarLink.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,OAAO,CAAA;AAUlD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,OAAO,CAAA;IAClB,IAAI,EAAE,UAAU,CAAA;IAChB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,sEAAsE;IACtE,SAAS,EAAE,KAAK,GAAG,OAAO,CAAA;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,CACb,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,WAAW,GAAG,IAAI,KACxB,IAAI,CAAA;CACV,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,UAAU,GAAI,iEAOxB,eAAe,KAAG,SA4EpB,CAAA"}