@charcuterie/ui 3.0.0 → 3.1.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 (113) 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/MarkdownEditor/MarkdownEditor.d.ts +147 -0
  54. package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
  55. package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
  56. package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
  57. package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
  58. package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
  59. package/dist/MarkdownEditor/markdownCommands.js +379 -0
  60. package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
  61. package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
  62. package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
  63. package/dist/MarkdownEditor/markdownSpans.js +303 -0
  64. package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
  65. package/dist/categoricalStyles.d.ts +53 -0
  66. package/dist/categoricalStyles.d.ts.map +1 -0
  67. package/dist/categoricalStyles.js +195 -0
  68. package/dist/categoricalStyles.js.map +1 -0
  69. package/dist/index.d.ts +22 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +11 -0
  72. package/dist/index.js.map +1 -1
  73. package/package.json +2 -2
  74. package/src/Badge/Badge.mdx +46 -0
  75. package/src/Badge/Badge.stories.tsx +98 -4
  76. package/src/Badge/Badge.test.tsx +157 -2
  77. package/src/Badge/Badge.tsx +49 -3
  78. package/src/Board/Board.mdx +199 -0
  79. package/src/Board/Board.stories.tsx +707 -0
  80. package/src/Board/Board.test.tsx +310 -0
  81. package/src/Board/Board.tsx +333 -0
  82. package/src/Board/BoardCard.tsx +290 -0
  83. package/src/Board/BoardLaneList.tsx +275 -0
  84. package/src/Board/boardMove.test.ts +213 -0
  85. package/src/Board/boardMove.ts +225 -0
  86. package/src/Board/useBoardDrag.ts +413 -0
  87. package/src/Checkbox/Checkbox.tsx +34 -3
  88. package/src/DataTable/DataTable.mdx +123 -0
  89. package/src/DataTable/DataTable.stories.tsx +411 -0
  90. package/src/DataTable/DataTable.test.tsx +386 -0
  91. package/src/DataTable/DataTable.tsx +497 -0
  92. package/src/DatePicker/DateGrid.tsx +467 -0
  93. package/src/DatePicker/DatePicker.mdx +170 -0
  94. package/src/DatePicker/DatePicker.stories.tsx +268 -0
  95. package/src/DatePicker/DatePicker.test.tsx +455 -0
  96. package/src/DatePicker/DatePicker.tsx +852 -0
  97. package/src/DatePicker/datePresets.ts +34 -0
  98. package/src/DatePicker/parseDateInput.test.ts +187 -0
  99. package/src/DatePicker/parseDateInput.ts +629 -0
  100. package/src/DatePicker/plainDate.test.ts +375 -0
  101. package/src/DatePicker/plainDate.ts +465 -0
  102. package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
  103. package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
  104. package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
  105. package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
  106. package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
  107. package/src/MarkdownEditor/markdownCommands.ts +566 -0
  108. package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
  109. package/src/MarkdownEditor/markdownSpans.ts +458 -0
  110. package/src/argTypes.storyHelpers.ts +39 -0
  111. package/src/categoricalStyles.ts +244 -0
  112. package/src/index.ts +89 -0
  113. package/src/sourceRules.test.ts +52 -1
@@ -0,0 +1,199 @@
1
+ import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ import * as BoardStories from "./Board.stories";
4
+
5
+ <Meta of={BoardStories} />
6
+
7
+ # Board
8
+
9
+ Lanes of cards, sized by their **container**, moved by keyboard or by pointer — and with
10
+ **no drag-and-drop dependency**.
11
+
12
+ <Canvas of={BoardStories.Default} />
13
+
14
+ ## Props
15
+
16
+ <Controls />
17
+
18
+ ## The container rule, and why it is the whole component
19
+
20
+ There is no media query anywhere in `Board`, `BoardCard` or `BoardLaneList`, and adding one
21
+ would be a defect rather than a shortcut. Two facts make window width useless here, and they
22
+ compound:
23
+
24
+ - **A lane in a three-up board is narrow on any monitor.** Three lanes across a 1600px window
25
+ is ~500px each — a phone's worth of room, on a 4K display, maximised.
26
+ - **A zoomed browser lies about pixels.** A 1500px window at 175% zoom is ~860 effective CSS
27
+ pixels, so the number a `@media` query reads is not the number of pixels anything has.
28
+
29
+ So there are **two nested containers**, answering two different questions:
30
+
31
+ | Container | Decides |
32
+ | --- | --- |
33
+ | the board's own box | how many lanes are on screen — three-up at `cq-lg`, otherwise one lane and a segmented control |
34
+ | each lane's wrapper | what shape a card is — two lines, one line, or its own card |
35
+
36
+ `Responsive` is the only honest way to show it: three fixed-width panels inside one browser
37
+ window that never moves. Resizing the viewport would prove nothing, because the thing that
38
+ changed is the component's container, not the window.
39
+
40
+ <Canvas of={BoardStories.Responsive} />
41
+
42
+ The narrow panel there is **not a phone**. It is the same board in a sidebar, in a split
43
+ window, or on a display somebody has zoomed to 175% — and a media query cannot tell any of
44
+ those apart from a maximised 4K window.
45
+
46
+ ### The three card shapes
47
+
48
+ Straight from the consumer requirement, which is specific because the owner answered *"which
49
+ row style?"* with **"all three, depending"**:
50
+
51
+ | The lane is | The card is |
52
+ | --- | --- |
53
+ | narrower than `cq-sm` (24rem) | **two lines** — title clamped to two, metadata underneath, trailing slot beside it |
54
+ | `cq-sm` to `cq-lg` | **one line** — title ellipsised, metadata right-aligned |
55
+ | `cq-lg` (48rem) and wider | **a card** — its own border, surface and elevation instead of a divider in a list |
56
+
57
+ It dissolved a bug rather than only satisfying a taste: a *fixed* two-line row collapsed its
58
+ title to `a…` inside a narrow lane, because the metadata beside it could not shrink. A card
59
+ sized by its container has no such state to be in.
60
+
61
+ ### The wrappers are load-bearing
62
+
63
+ A container query matches **descendants only**, never the element that declares the
64
+ container. So the lane's panel cannot be the container *and* take its own frame off at
65
+ `cq-lg`; the `@container` sits on a wrapper `<div>` and the panel inside it does the
66
+ querying. Same reason `EmptyState` has one. Written on one element it compiles, generates
67
+ real CSS, and silently never fires — which is why `sourceRules.test.ts` fails on it.
68
+
69
+ ## Moving a card, and the drag-and-drop decision
70
+
71
+ **No DnD library is taken as a dependency.** The full reasoning — licence, provenance, bundle
72
+ cost, and what a general DnD library would actually be buying for one card going into one
73
+ lane — is in
74
+ [the decision record](https://github.com/Sawtaytoes/charcuterie/blob/master/docs/decisions/2026-08-19-the-board-owns-the-move-and-takes-no-drag-and-drop-dependency.md).
75
+
76
+ The move is one control with two drivers, and they commit through the same `onMove`:
77
+
78
+ 1. **A `Menu` on every card**, listing the other lanes. This is the **primary** path, not an
79
+ accessible alternative to a real one. It is the only one that works from the keyboard, the
80
+ only one a screen reader can drive, the only one that works on touch without a long-press
81
+ convention nobody discovers — and the only one available in the Narrow View, where the
82
+ other lanes are not on screen to drop onto.
83
+ 2. **Pointer dragging from the same handle.** Pointer Events, so mouse, touch and pen are one
84
+ code path, with `setPointerCapture` so the drag survives the pointer leaving the card.
85
+ Escape abandons it.
86
+
87
+ <Canvas of={BoardStories.Interactive} />
88
+
89
+ Every move is announced in a `role="status"` region that names the **destination and the
90
+ position** — "Moved *X* to In Progress, position 3 of 6." That is the feedback a sighted user
91
+ gets free from watching the card land, and the thing almost every drag implementation forgets
92
+ to say out loud.
93
+
94
+ ### Why not `draggable="true"`
95
+
96
+ The zero-dependency instinct, and the wrong one. HTML5 drag-and-drop does not fire on touch at
97
+ all, its drag image is a browser-drawn ghost with no styling hook worth using, and
98
+ `dataTransfer` is readable only inside a real `drop` — which makes a live drop indicator
99
+ awkward. Pointer Events cost the same (nothing) and cover every input.
100
+
101
+ ### The move handle is a word by default
102
+
103
+ `Board` ships **no icon**, because a `⋮` renders as nothing where the font lacks it — this
104
+ sandbox's chromium, the kiosk image, the ePaper build
105
+ ([decision](https://github.com/Sawtaytoes/charcuterie/blob/master/docs/decisions/2026-07-29-ship-no-icons-and-no-symbol-glyphs.md)).
106
+ An app that already owns a glyph set passes `moveIcon` and buys back ~55px a row, which in a
107
+ 500px lane is the difference between twenty characters of title and thirty.
108
+
109
+ <Canvas of={BoardStories.AllVariants} />
110
+
111
+ **No `onMove` means read-only**, all or nothing: no handles, no menus, no drag. A per-card
112
+ `isMovable` would put the decision in the data, where a card that cannot move looks identical
113
+ to one nobody has thought about.
114
+
115
+ ## Where the line is drawn
116
+
117
+ `Board` owns lanes, counts, truncation, empty states and the move. It does **not** own:
118
+
119
+ | Not here | Because |
120
+ | --- | --- |
121
+ | **Data** | It is a view of a list the consumer owns. `onMove` hands back a `fromIndex`/`toIndex` pair; the consumer splices. |
122
+ | **Virtualisation** | A lane truncates by construction — `itemCount` is the true total, `items` is what you chose to paint, and the difference becomes "+ n more". *"Long lanes truncate with an honest `+ n more` rather than scrolling forever."* There is nothing left to window. |
123
+ | **The cross-lane "needs attention" panel** | It reaches across every lane, so it is not a lane; it is absent when empty, so the board cannot reserve room for it; and it is already `Alert`. Composing it keeps `Board` about lanes and keeps the banner reusable above a list that is not a board. |
124
+ | **Sorting and filtering** | Both are operations on the array you pass in. |
125
+
126
+ <Canvas of={BoardStories.InBoardScreen} />
127
+
128
+ That story is the copy-wholesale template: a page heading, an `Alert` for what needs
129
+ attention, and the board under it with `headingLevel={2}` so the outline stays whole.
130
+
131
+ ## `toIndex` is already corrected for the removal
132
+
133
+ The most common bug in a hand-rolled board, folded into the component instead:
134
+
135
+ ```tsx
136
+ <Board
137
+ label="Today"
138
+ lanes={lanes}
139
+ onMove={(move) => {
140
+ setLanes((current) =>
141
+ current.map((lane) => {
142
+ const kept =
143
+ lane.key === move.fromLaneKey
144
+ ? lane.items.filter((item) => item.key !== move.itemKey)
145
+ : lane.items
146
+
147
+ if (lane.key !== move.toLaneKey) {
148
+ return { ...lane, items: kept }
149
+ }
150
+
151
+ return {
152
+ ...lane,
153
+ items: [
154
+ ...kept.slice(0, move.toIndex),
155
+ moved,
156
+ ...kept.slice(move.toIndex),
157
+ ],
158
+ }
159
+ }),
160
+ )
161
+ }}
162
+ />
163
+ ```
164
+
165
+ No arithmetic. A within-lane move quoted against the un-spliced list is one too high for every
166
+ position below the origin, so the component subtracts it once, in
167
+ [`toSettledIndex`](https://github.com/Sawtaytoes/charcuterie/blob/master/packages/ui/src/Board/boardMove.ts),
168
+ which is unit-tested in Node against fixtures — including the tie where a card dropped back on
169
+ its own midpoint must be **no move at all**.
170
+
171
+ ## Accessibility
172
+
173
+ - The board is a **named landmark**; each lane is a `role="group"`, not a second landmark. A
174
+ board with four `region`s buries the page's real ones, and two boards on one page put two
175
+ landmarks called "Todo" in the document. Same call the
176
+ [accordion panel](https://github.com/Sawtaytoes/charcuterie/blob/master/docs/decisions/2026-07-31-an-accordion-panel-is-a-group-not-a-landmark.md)
177
+ made. It is a `<div role="group">` rather than a `<section role="group">` because axe's
178
+ `landmark-unique` matches `section[aria-labelledby]` on the element and audits it as a
179
+ landmark even after the role is overridden.
180
+ - **The priority bar's meaning is in words.** `accentIntent` paints it; `accentLabel` puts the
181
+ same fact in a `VisuallyHidden`, because colour alone fails WCAG 1.4.1 and is invisible to
182
+ every screen reader. Do the same for anything you put in `marks` — a bare coloured dot is
183
+ the identical failure.
184
+ - **`headingLevel` is yours.** Only the caller knows what the board sits under; an `<h3>`
185
+ directly below the page `<h1>` is an axe `heading-order` failure.
186
+ - Every move handle's accessible name says *which* card and *where it is now*. Thirty controls
187
+ named "Move" are thirty indistinguishable buttons to a screen reader and to
188
+ `getByRole("button", { name })`.
189
+
190
+ <Canvas of={BoardStories.AllStates} />
191
+
192
+ ## Never invent a token name
193
+
194
+ The colour families here are `--color-intent-<intent>-<role>`, plus `surface`, `content` and
195
+ `border`. **There is no numeric scale.** Docket's first board painted every priority bar with
196
+ `--color-danger-9`, a Radix-style name this token set has never had, so every bar was
197
+ transparent — while every "is it rendered" assertion passed and only looking at the image
198
+ caught it. `accentIntent` is typed as `IntentName` for exactly that reason: a wrong value is a
199
+ compile error rather than an invisible rectangle.