@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.
- package/README.md +10 -0
- package/dist/Badge/Badge.d.ts +33 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +20 -2
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Board/Board.d.ts +119 -0
- package/dist/Board/Board.d.ts.map +1 -0
- package/dist/Board/Board.js +165 -0
- package/dist/Board/Board.js.map +1 -0
- package/dist/Board/BoardCard.d.ts +120 -0
- package/dist/Board/BoardCard.d.ts.map +1 -0
- package/dist/Board/BoardCard.js +77 -0
- package/dist/Board/BoardCard.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +103 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -0
- package/dist/Board/BoardLaneList.js +69 -0
- package/dist/Board/BoardLaneList.js.map +1 -0
- package/dist/Board/boardMove.d.ts +116 -0
- package/dist/Board/boardMove.d.ts.map +1 -0
- package/dist/Board/boardMove.js +116 -0
- package/dist/Board/boardMove.js.map +1 -0
- package/dist/Board/useBoardDrag.d.ts +44 -0
- package/dist/Board/useBoardDrag.d.ts.map +1 -0
- package/dist/Board/useBoardDrag.js +237 -0
- package/dist/Board/useBoardDrag.js.map +1 -0
- package/dist/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox/Checkbox.js +15 -4
- package/dist/Checkbox/Checkbox.js.map +1 -1
- package/dist/DataTable/DataTable.d.ts +159 -0
- package/dist/DataTable/DataTable.d.ts.map +1 -0
- package/dist/DataTable/DataTable.js +197 -0
- package/dist/DataTable/DataTable.js.map +1 -0
- package/dist/DatePicker/DateGrid.d.ts +79 -0
- package/dist/DatePicker/DateGrid.d.ts.map +1 -0
- package/dist/DatePicker/DateGrid.js +220 -0
- package/dist/DatePicker/DateGrid.js.map +1 -0
- package/dist/DatePicker/DatePicker.d.ts +138 -0
- package/dist/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/DatePicker/DatePicker.js +384 -0
- package/dist/DatePicker/DatePicker.js.map +1 -0
- package/dist/DatePicker/datePresets.d.ts +29 -0
- package/dist/DatePicker/datePresets.d.ts.map +1 -0
- package/dist/DatePicker/datePresets.js +28 -0
- package/dist/DatePicker/datePresets.js.map +1 -0
- package/dist/DatePicker/parseDateInput.d.ts +106 -0
- package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
- package/dist/DatePicker/parseDateInput.js +419 -0
- package/dist/DatePicker/parseDateInput.js.map +1 -0
- package/dist/DatePicker/plainDate.d.ts +167 -0
- package/dist/DatePicker/plainDate.d.ts.map +1 -0
- package/dist/DatePicker/plainDate.js +272 -0
- package/dist/DatePicker/plainDate.js.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.js +379 -0
- package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.js +303 -0
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
- package/dist/categoricalStyles.d.ts +53 -0
- package/dist/categoricalStyles.d.ts.map +1 -0
- package/dist/categoricalStyles.js +195 -0
- package/dist/categoricalStyles.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Badge/Badge.mdx +46 -0
- package/src/Badge/Badge.stories.tsx +98 -4
- package/src/Badge/Badge.test.tsx +157 -2
- package/src/Badge/Badge.tsx +49 -3
- package/src/Board/Board.mdx +199 -0
- package/src/Board/Board.stories.tsx +707 -0
- package/src/Board/Board.test.tsx +310 -0
- package/src/Board/Board.tsx +333 -0
- package/src/Board/BoardCard.tsx +290 -0
- package/src/Board/BoardLaneList.tsx +275 -0
- package/src/Board/boardMove.test.ts +213 -0
- package/src/Board/boardMove.ts +225 -0
- package/src/Board/useBoardDrag.ts +413 -0
- package/src/Checkbox/Checkbox.tsx +34 -3
- package/src/DataTable/DataTable.mdx +123 -0
- package/src/DataTable/DataTable.stories.tsx +411 -0
- package/src/DataTable/DataTable.test.tsx +386 -0
- package/src/DataTable/DataTable.tsx +497 -0
- package/src/DatePicker/DateGrid.tsx +467 -0
- package/src/DatePicker/DatePicker.mdx +170 -0
- package/src/DatePicker/DatePicker.stories.tsx +268 -0
- package/src/DatePicker/DatePicker.test.tsx +455 -0
- package/src/DatePicker/DatePicker.tsx +852 -0
- package/src/DatePicker/datePresets.ts +34 -0
- package/src/DatePicker/parseDateInput.test.ts +187 -0
- package/src/DatePicker/parseDateInput.ts +629 -0
- package/src/DatePicker/plainDate.test.ts +375 -0
- package/src/DatePicker/plainDate.ts +465 -0
- package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
- package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
- package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
- package/src/MarkdownEditor/markdownCommands.ts +566 -0
- package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
- package/src/MarkdownEditor/markdownSpans.ts +458 -0
- package/src/argTypes.storyHelpers.ts +39 -0
- package/src/categoricalStyles.ts +244 -0
- package/src/index.ts +89 -0
- 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.
|