@charcuterie/ui 3.0.0 → 3.2.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 (130) hide show
  1. package/README.md +10 -0
  2. package/dist/Badge/Badge.d.ts +33 -3
  3. package/dist/Badge/Badge.d.ts.map +1 -1
  4. package/dist/Badge/Badge.js +20 -2
  5. package/dist/Badge/Badge.js.map +1 -1
  6. package/dist/Board/Board.d.ts +119 -0
  7. package/dist/Board/Board.d.ts.map +1 -0
  8. package/dist/Board/Board.js +165 -0
  9. package/dist/Board/Board.js.map +1 -0
  10. package/dist/Board/BoardCard.d.ts +120 -0
  11. package/dist/Board/BoardCard.d.ts.map +1 -0
  12. package/dist/Board/BoardCard.js +77 -0
  13. package/dist/Board/BoardCard.js.map +1 -0
  14. package/dist/Board/BoardLaneList.d.ts +103 -0
  15. package/dist/Board/BoardLaneList.d.ts.map +1 -0
  16. package/dist/Board/BoardLaneList.js +69 -0
  17. package/dist/Board/BoardLaneList.js.map +1 -0
  18. package/dist/Board/boardMove.d.ts +116 -0
  19. package/dist/Board/boardMove.d.ts.map +1 -0
  20. package/dist/Board/boardMove.js +116 -0
  21. package/dist/Board/boardMove.js.map +1 -0
  22. package/dist/Board/useBoardDrag.d.ts +44 -0
  23. package/dist/Board/useBoardDrag.d.ts.map +1 -0
  24. package/dist/Board/useBoardDrag.js +237 -0
  25. package/dist/Board/useBoardDrag.js.map +1 -0
  26. package/dist/Checkbox/Checkbox.d.ts.map +1 -1
  27. package/dist/Checkbox/Checkbox.js +15 -4
  28. package/dist/Checkbox/Checkbox.js.map +1 -1
  29. package/dist/DataTable/DataTable.d.ts +159 -0
  30. package/dist/DataTable/DataTable.d.ts.map +1 -0
  31. package/dist/DataTable/DataTable.js +197 -0
  32. package/dist/DataTable/DataTable.js.map +1 -0
  33. package/dist/DatePicker/DateGrid.d.ts +79 -0
  34. package/dist/DatePicker/DateGrid.d.ts.map +1 -0
  35. package/dist/DatePicker/DateGrid.js +220 -0
  36. package/dist/DatePicker/DateGrid.js.map +1 -0
  37. package/dist/DatePicker/DatePicker.d.ts +138 -0
  38. package/dist/DatePicker/DatePicker.d.ts.map +1 -0
  39. package/dist/DatePicker/DatePicker.js +384 -0
  40. package/dist/DatePicker/DatePicker.js.map +1 -0
  41. package/dist/DatePicker/datePresets.d.ts +29 -0
  42. package/dist/DatePicker/datePresets.d.ts.map +1 -0
  43. package/dist/DatePicker/datePresets.js +28 -0
  44. package/dist/DatePicker/datePresets.js.map +1 -0
  45. package/dist/DatePicker/parseDateInput.d.ts +106 -0
  46. package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
  47. package/dist/DatePicker/parseDateInput.js +419 -0
  48. package/dist/DatePicker/parseDateInput.js.map +1 -0
  49. package/dist/DatePicker/plainDate.d.ts +167 -0
  50. package/dist/DatePicker/plainDate.d.ts.map +1 -0
  51. package/dist/DatePicker/plainDate.js +272 -0
  52. package/dist/DatePicker/plainDate.js.map +1 -0
  53. package/dist/Dialog/Dialog.d.ts +12 -2
  54. package/dist/Dialog/Dialog.d.ts.map +1 -1
  55. package/dist/Dialog/Dialog.js +2 -2
  56. package/dist/Dialog/Dialog.js.map +1 -1
  57. package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
  58. package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
  59. package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
  60. package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
  61. package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
  62. package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
  63. package/dist/MarkdownEditor/markdownCommands.js +379 -0
  64. package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
  65. package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
  66. package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
  67. package/dist/MarkdownEditor/markdownSpans.js +303 -0
  68. package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
  69. package/dist/Modal/Modal.d.ts +6 -2
  70. package/dist/Modal/Modal.d.ts.map +1 -1
  71. package/dist/Modal/Modal.js +1 -1
  72. package/dist/Modal/Modal.js.map +1 -1
  73. package/dist/Overlay/OverlayPanel.d.ts +13 -2
  74. package/dist/Overlay/OverlayPanel.d.ts.map +1 -1
  75. package/dist/Overlay/OverlayPanel.js +5 -1
  76. package/dist/Overlay/OverlayPanel.js.map +1 -1
  77. package/dist/categoricalStyles.d.ts +53 -0
  78. package/dist/categoricalStyles.d.ts.map +1 -0
  79. package/dist/categoricalStyles.js +195 -0
  80. package/dist/categoricalStyles.js.map +1 -0
  81. package/dist/index.d.ts +22 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +11 -0
  84. package/dist/index.js.map +1 -1
  85. package/package.json +2 -2
  86. package/src/Badge/Badge.mdx +46 -0
  87. package/src/Badge/Badge.stories.tsx +98 -4
  88. package/src/Badge/Badge.test.tsx +157 -2
  89. package/src/Badge/Badge.tsx +49 -3
  90. package/src/Board/Board.mdx +199 -0
  91. package/src/Board/Board.stories.tsx +707 -0
  92. package/src/Board/Board.test.tsx +310 -0
  93. package/src/Board/Board.tsx +333 -0
  94. package/src/Board/BoardCard.tsx +290 -0
  95. package/src/Board/BoardLaneList.tsx +275 -0
  96. package/src/Board/boardMove.test.ts +213 -0
  97. package/src/Board/boardMove.ts +225 -0
  98. package/src/Board/useBoardDrag.ts +413 -0
  99. package/src/Checkbox/Checkbox.tsx +34 -3
  100. package/src/DataTable/DataTable.mdx +123 -0
  101. package/src/DataTable/DataTable.stories.tsx +411 -0
  102. package/src/DataTable/DataTable.test.tsx +386 -0
  103. package/src/DataTable/DataTable.tsx +497 -0
  104. package/src/DatePicker/DateGrid.tsx +467 -0
  105. package/src/DatePicker/DatePicker.mdx +170 -0
  106. package/src/DatePicker/DatePicker.stories.tsx +268 -0
  107. package/src/DatePicker/DatePicker.test.tsx +455 -0
  108. package/src/DatePicker/DatePicker.tsx +852 -0
  109. package/src/DatePicker/datePresets.ts +34 -0
  110. package/src/DatePicker/parseDateInput.test.ts +187 -0
  111. package/src/DatePicker/parseDateInput.ts +629 -0
  112. package/src/DatePicker/plainDate.test.ts +375 -0
  113. package/src/DatePicker/plainDate.ts +465 -0
  114. package/src/Dialog/Dialog.stories.tsx +48 -0
  115. package/src/Dialog/Dialog.test.tsx +30 -0
  116. package/src/Dialog/Dialog.tsx +13 -1
  117. package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
  118. package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
  119. package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
  120. package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
  121. package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
  122. package/src/MarkdownEditor/markdownCommands.ts +566 -0
  123. package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
  124. package/src/MarkdownEditor/markdownSpans.ts +458 -0
  125. package/src/Modal/Modal.tsx +7 -1
  126. package/src/Overlay/OverlayPanel.tsx +17 -1
  127. package/src/argTypes.storyHelpers.ts +39 -0
  128. package/src/categoricalStyles.ts +244 -0
  129. package/src/index.ts +89 -0
  130. package/src/sourceRules.test.ts +52 -1
@@ -0,0 +1,411 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+ import type { ReactNode } from "react"
3
+ import { useState } from "react"
4
+
5
+ import { Badge } from "../Badge/Badge.tsx"
6
+ import {
7
+ ContainerBoard,
8
+ StoryCell,
9
+ StoryGrid,
10
+ StorySection,
11
+ } from "../board.storyHelpers.tsx"
12
+ import { EmptyState } from "../EmptyState/EmptyState.tsx"
13
+ import { TextLink } from "../TextLink/TextLink.tsx"
14
+ import type {
15
+ DataTableColumn,
16
+ DataTableSort,
17
+ } from "./DataTable.tsx"
18
+ import { DataTable } from "./DataTable.tsx"
19
+
20
+ /**
21
+ * Invented rows, and deliberately so: a screenshot is opaque to
22
+ * every grep, so real data in a fixture is real data nobody can
23
+ * find again later.
24
+ */
25
+ type Task = {
26
+ assignee: string
27
+ id: string
28
+ priority: "high" | "low" | "medium"
29
+ status: "blocked" | "done" | "todo"
30
+ title: string
31
+ updatedAt: string
32
+ }
33
+
34
+ const TASKS: readonly Task[] = [
35
+ {
36
+ assignee: "Wren",
37
+ id: "task-1",
38
+ priority: "high",
39
+ status: "todo",
40
+ title: "Re-point the garden wall",
41
+ updatedAt: "2026-08-18",
42
+ },
43
+ {
44
+ assignee: "Kestrel",
45
+ id: "task-2",
46
+ priority: "medium",
47
+ status: "blocked",
48
+ title: "Order the replacement extractor fan filter",
49
+ updatedAt: "2026-08-14",
50
+ },
51
+ {
52
+ assignee: "Wren",
53
+ id: "task-3",
54
+ priority: "low",
55
+ status: "done",
56
+ title: "Label the spare keys",
57
+ updatedAt: "2026-08-11",
58
+ },
59
+ {
60
+ assignee: "Marlowe",
61
+ id: "task-4",
62
+ priority: "high",
63
+ status: "todo",
64
+ title:
65
+ "Rebuild the seed-tray shelving now the propagator has moved",
66
+ updatedAt: "2026-08-19",
67
+ },
68
+ {
69
+ assignee: "Kestrel",
70
+ id: "task-5",
71
+ priority: "medium",
72
+ status: "todo",
73
+ title: "Book the chimney sweep",
74
+ updatedAt: "2026-08-09",
75
+ },
76
+ ]
77
+
78
+ const STATUS_INTENT = {
79
+ blocked: "danger",
80
+ done: "success",
81
+ todo: "neutral",
82
+ } as const
83
+
84
+ const PRIORITY_ORDER = {
85
+ high: 0,
86
+ low: 2,
87
+ medium: 1,
88
+ } as const
89
+
90
+ /**
91
+ * The columns, as a consumer writes them. `renderCell` returns a
92
+ * node, so the title cell is a real `TextLink` and the status cell
93
+ * is a `Badge` — the table composes what the package already has
94
+ * rather than growing a `type: "badge"` column kind that could only
95
+ * ever render one of them.
96
+ */
97
+ /**
98
+ * The widths are themselves container queries — `cq-lg:w-32`, not
99
+ * `w-32`. A fixed width is a promise about space the container may
100
+ * not have: four columns pinned at `w-32` inside a 34rem table leave
101
+ * the title 48px, and the title is the column anyone reads.
102
+ */
103
+ const COLUMNS: readonly DataTableColumn<Task>[] = [
104
+ {
105
+ header: "Task",
106
+ isSortable: true,
107
+ key: "title",
108
+ renderCell: (task) => (
109
+ <TextLink href={`#${task.id}`}>{task.title}</TextLink>
110
+ ),
111
+ },
112
+ {
113
+ className: "cq-lg:w-32",
114
+ header: "Status",
115
+ isSortable: true,
116
+ key: "status",
117
+ renderCell: (task) => (
118
+ <Badge
119
+ appearance="soft"
120
+ intent={STATUS_INTENT[task.status]}
121
+ >
122
+ {task.status}
123
+ </Badge>
124
+ ),
125
+ },
126
+ {
127
+ className: "cq-lg:w-28",
128
+ header: "Priority",
129
+ isSortable: true,
130
+ key: "priority",
131
+ renderCell: (task) => task.priority,
132
+ },
133
+ {
134
+ className: "cq-lg:w-32",
135
+ header: "Assignee",
136
+ key: "assignee",
137
+ renderCell: (task) => task.assignee,
138
+ },
139
+ {
140
+ className: "cq-lg:w-32",
141
+ header: "Updated",
142
+ isSortable: true,
143
+ key: "updatedAt",
144
+ renderCell: (task) => task.updatedAt,
145
+ },
146
+ ]
147
+
148
+ /**
149
+ * Comparing two values is the consumer's, and this is what that
150
+ * looks like: three of these five columns do not sort
151
+ * alphabetically. `priority` is an enum whose order is
152
+ * high → medium → low and whose alphabetical order is
153
+ * high → low → medium, which is the case a table that sorted itself
154
+ * would get quietly wrong.
155
+ */
156
+ const getSortedTasks = (
157
+ tasks: readonly Task[],
158
+ sort: DataTableSort,
159
+ ): Task[] => {
160
+ const direction = sort.direction === "ascending" ? 1 : -1
161
+
162
+ return [...tasks].sort((one, two) => {
163
+ if (sort.columnKey === "priority") {
164
+ return (
165
+ (PRIORITY_ORDER[one.priority] -
166
+ PRIORITY_ORDER[two.priority]) *
167
+ direction
168
+ )
169
+ }
170
+
171
+ const key =
172
+ sort.columnKey === "title"
173
+ ? "title"
174
+ : sort.columnKey === "status"
175
+ ? "status"
176
+ : sort.columnKey === "assignee"
177
+ ? "assignee"
178
+ : "updatedAt"
179
+
180
+ return one[key].localeCompare(two[key]) * direction
181
+ })
182
+ }
183
+
184
+ /**
185
+ * The state a consumer owns, in the smallest form that is honest:
186
+ * a sort, a selected set, and the rows re-ordered from both. Every
187
+ * story that needs a working table renders this.
188
+ */
189
+ const TaskTable = ({
190
+ isSelectable = false,
191
+ initialSort,
192
+ label = "Tasks",
193
+ tasks = TASKS,
194
+ }: {
195
+ initialSort?: DataTableSort
196
+ isSelectable?: boolean
197
+ label?: string
198
+ tasks?: readonly Task[]
199
+ }): ReactNode => {
200
+ const [sort, setSort] = useState<
201
+ DataTableSort | undefined
202
+ >(initialSort)
203
+
204
+ const [selectedRowKeys, setSelectedRowKeys] = useState<
205
+ readonly string[]
206
+ >([])
207
+
208
+ const sortedTasks = sort
209
+ ? getSortedTasks(tasks, sort)
210
+ : tasks
211
+
212
+ return (
213
+ <DataTable
214
+ columns={COLUMNS}
215
+ emptyState={
216
+ <EmptyState
217
+ description="Nothing matches this filter. The columns and the sort are still here."
218
+ heading="No tasks"
219
+ />
220
+ }
221
+ getRowKey={(task) => task.id}
222
+ label={label}
223
+ onSortChange={setSort}
224
+ rows={sortedTasks}
225
+ selection={
226
+ isSelectable
227
+ ? {
228
+ getRowLabel: (task) => task.title,
229
+ onSelectionChange: setSelectedRowKeys,
230
+ // Not "Select all tasks": an agent's
231
+ // `getByRole("columnheader", { name: "Task" })` is a
232
+ // **substring, case-insensitive** match in Playwright,
233
+ // so a header box called "Select all tasks" makes the
234
+ // "Task" column ambiguous to the query the component
235
+ // exists to be drivable by.
236
+ selectAllLabel: "Select every row",
237
+ selectedRowKeys,
238
+ }
239
+ : undefined
240
+ }
241
+ sort={sort}
242
+ />
243
+ )
244
+ }
245
+
246
+ const meta = {
247
+ title: "Components/DataTable",
248
+ component: DataTable,
249
+ parameters: { layout: "padded" },
250
+ args: {
251
+ columns: COLUMNS,
252
+ getRowKey: (task: Task) => task.id,
253
+ isLabelVisible: false,
254
+ label: "Tasks",
255
+ rows: TASKS,
256
+ },
257
+ } satisfies Meta<typeof DataTable<Task>>
258
+
259
+ export default meta
260
+
261
+ type Story = StoryObj<typeof meta>
262
+
263
+ /**
264
+ * Five rows, five columns, four of them sortable. Click a header and
265
+ * the rows re-order — the story sorts them, which is the whole point
266
+ * of the split: the component says *which* column is sorted, the
267
+ * consumer says what that means for a priority enum.
268
+ */
269
+ export const Default: Story = {
270
+ render: () => (
271
+ <TaskTable
272
+ initialSort={{
273
+ columnKey: "updatedAt",
274
+ direction: "descending",
275
+ }}
276
+ />
277
+ ),
278
+ }
279
+
280
+ /**
281
+ * The three shapes a caller assembles out of the same props: plain,
282
+ * selectable, and empty. There is no `variant` prop — a table with
283
+ * tick boxes differs from one without by having a `selection`, which
284
+ * is a capability rather than a look.
285
+ */
286
+ export const AllVariants: Story = {
287
+ render: () => (
288
+ <StorySection title="Same columns, three assemblies.">
289
+ <StoryGrid columns={1}>
290
+ <StoryCell align="stretch" label="plain">
291
+ <TaskTable />
292
+ </StoryCell>
293
+
294
+ <StoryCell
295
+ align="stretch"
296
+ label="selectable — a leading checkbox column"
297
+ >
298
+ <TaskTable isSelectable />
299
+ </StoryCell>
300
+
301
+ <StoryCell
302
+ align="stretch"
303
+ label="empty — the header and the sort survive"
304
+ >
305
+ <TaskTable tasks={[]} />
306
+ </StoryCell>
307
+ </StoryGrid>
308
+ </StorySection>
309
+ ),
310
+ }
311
+
312
+ /**
313
+ * Sort has three states per column and exactly one column ever holds
314
+ * a direction. The unsorted columns are not blank in the
315
+ * accessibility tree — they say `aria-sort="none"`, which is
316
+ * "sortable, not currently sorted"; an absent `aria-sort` would mean
317
+ * "cannot be sorted at all".
318
+ */
319
+ export const AllStates: Story = {
320
+ render: () => (
321
+ <StorySection title="Sorted ascending, sorted descending, and a selected row.">
322
+ <StoryGrid columns={1}>
323
+ <StoryCell
324
+ align="stretch"
325
+ label='Task, aria-sort="ascending"'
326
+ >
327
+ <TaskTable
328
+ initialSort={{
329
+ columnKey: "title",
330
+ direction: "ascending",
331
+ }}
332
+ />
333
+ </StoryCell>
334
+
335
+ <StoryCell
336
+ align="stretch"
337
+ label='Updated, aria-sort="descending"'
338
+ >
339
+ <TaskTable
340
+ initialSort={{
341
+ columnKey: "updatedAt",
342
+ direction: "descending",
343
+ }}
344
+ />
345
+ </StoryCell>
346
+
347
+ <StoryCell
348
+ align="stretch"
349
+ label="selection — tick a row, or the header box for all of them"
350
+ >
351
+ <TaskTable isSelectable />
352
+ </StoryCell>
353
+ </StoryGrid>
354
+ </StorySection>
355
+ ),
356
+ }
357
+
358
+ /**
359
+ * The **only honest way to story this component.** Three container
360
+ * widths inside one window: the first two are below `--cq-md`
361
+ * (32rem) and each row is a labelled block with the header row
362
+ * wrapped into a strip of sort controls; the third is above it and
363
+ * is a table.
364
+ *
365
+ * Resizing the browser proves nothing here — what changed is the
366
+ * element's width, and a window can be 4K while a board lane is
367
+ * 300px.
368
+ */
369
+ export const Responsive: Story = {
370
+ render: () => (
371
+ <ContainerBoard>
372
+ {(width) => (
373
+ <TaskTable
374
+ initialSort={{
375
+ columnKey: "title",
376
+ direction: "ascending",
377
+ }}
378
+ // Three tables in one story would otherwise be three
379
+ // things called "Tasks", and `getByRole("table", { name:
380
+ // "Tasks" })` — the query an agent writes — would match
381
+ // all three. `ContainerBoard`'s function form exists for
382
+ // exactly this.
383
+ label={`Tasks at ${width}`}
384
+ />
385
+ )}
386
+ </ContainerBoard>
387
+ ),
388
+ }
389
+
390
+ /**
391
+ * The complete keyboard path, and there is no custom key handling in
392
+ * it: `Tab` reaches each sortable header's `<button>` and each row's
393
+ * checkbox in document order, `Enter`/`Space` sorts, `Space` ticks.
394
+ *
395
+ * That is the deliberate half. A table is not a `grid` — it has no
396
+ * roving focus, no arrow-key cell navigation, and no focus trap —
397
+ * so a screen reader's own table-reading mode is what moves through
398
+ * the data, and everything interactive is a real control the browser
399
+ * already knows how to reach.
400
+ */
401
+ export const Interactive: Story = {
402
+ render: () => (
403
+ <TaskTable
404
+ initialSort={{
405
+ columnKey: "title",
406
+ direction: "ascending",
407
+ }}
408
+ isSelectable
409
+ />
410
+ ),
411
+ }