@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,213 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import type {
4
+ BoardDropLane,
5
+ BoardRect,
6
+ } from "./boardMove.ts"
7
+ import {
8
+ chooseDropIndex,
9
+ chooseDropTarget,
10
+ describeMove,
11
+ getIsMoveMeaningful,
12
+ toSettledIndex,
13
+ } from "./boardMove.ts"
14
+
15
+ /**
16
+ * The geometry, in Node, against fixtures — because the browser
17
+ * cannot test it honestly.
18
+ *
19
+ * `userEvent` will dispatch every `pointermove` you ask it for, but
20
+ * nothing in a `jsdom`-shaped world has laid out, and even in the
21
+ * real chromium the test canvas is mounted, measured and torn down
22
+ * inside one frame. Every rectangle the drop calculation would
23
+ * reason about is plausible-looking garbage, so a browser test of
24
+ * "did it pick the right lane" is a test of whether two zeroes
25
+ * compare equal.
26
+ *
27
+ * Split out, the rule is exact: these are the numbers, this is the
28
+ * answer, including the ties.
29
+ */
30
+
31
+ const toRect = (
32
+ top: number,
33
+ height: number,
34
+ ): BoardRect => ({
35
+ height,
36
+ left: 0,
37
+ top,
38
+ width: 100,
39
+ })
40
+
41
+ /** Three cards, 40px each, stacked from y=0. */
42
+ const CARD_RECTS: BoardRect[] = [
43
+ toRect(0, 40),
44
+ toRect(40, 40),
45
+ toRect(80, 40),
46
+ ]
47
+
48
+ const LANES: BoardDropLane[] = [
49
+ {
50
+ cardRects: CARD_RECTS,
51
+ key: "todo",
52
+ rect: { height: 200, left: 0, top: 0, width: 100 },
53
+ },
54
+ {
55
+ cardRects: [toRect(0, 40)],
56
+ key: "doing",
57
+ rect: { height: 200, left: 120, top: 0, width: 100 },
58
+ },
59
+ {
60
+ cardRects: [],
61
+ key: "done",
62
+ rect: { height: 200, left: 240, top: 0, width: 100 },
63
+ },
64
+ ]
65
+
66
+ test("the insertion point flips at a card's midpoint, not its edge", () => {
67
+ // Above the first midpoint: before everything.
68
+ expect(chooseDropIndex(CARD_RECTS, { x: 10, y: 5 })).toBe(
69
+ 0,
70
+ )
71
+
72
+ // Still in the first card, but past its centre: after it.
73
+ expect(
74
+ chooseDropIndex(CARD_RECTS, { x: 10, y: 25 }),
75
+ ).toBe(1)
76
+
77
+ expect(
78
+ chooseDropIndex(CARD_RECTS, { x: 10, y: 65 }),
79
+ ).toBe(2)
80
+
81
+ // Below everything.
82
+ expect(
83
+ chooseDropIndex(CARD_RECTS, { x: 10, y: 300 }),
84
+ ).toBe(3)
85
+ })
86
+
87
+ test("exactly on a midpoint counts as before, not after", () => {
88
+ // The tie, stated rather than discovered. `>` and not `>=`, so a
89
+ // pointer resting precisely on the centre line does not oscillate
90
+ // between two indices as the last sub-pixel rounds.
91
+ expect(
92
+ chooseDropIndex(CARD_RECTS, { x: 10, y: 20 }),
93
+ ).toBe(0)
94
+ })
95
+
96
+ test("an empty lane accepts a drop at index 0", () => {
97
+ expect(
98
+ chooseDropTarget(LANES, { x: 250, y: 100 }),
99
+ ).toEqual({ index: 0, laneKey: "done" })
100
+ })
101
+
102
+ test("the pointer picks the lane it is inside", () => {
103
+ expect(chooseDropTarget(LANES, { x: 10, y: 90 })).toEqual(
104
+ { index: 2, laneKey: "todo" },
105
+ )
106
+
107
+ expect(
108
+ chooseDropTarget(LANES, { x: 130, y: 10 }),
109
+ ).toEqual({ index: 0, laneKey: "doing" })
110
+ })
111
+
112
+ test("a release just outside a lane still lands in the nearest one", () => {
113
+ // Two pixels past the edge is a drop, not a cancel. Returning
114
+ // `null` here is how a board earns "it sometimes just puts the
115
+ // card back".
116
+ expect(
117
+ chooseDropTarget(LANES, { x: 105, y: 100 }),
118
+ ).toEqual({ index: 2, laneKey: "todo" })
119
+
120
+ expect(
121
+ chooseDropTarget(LANES, { x: 400, y: 100 }),
122
+ ).toEqual({ index: 0, laneKey: "done" })
123
+ })
124
+
125
+ test("there is no target when there are no lanes", () => {
126
+ expect(chooseDropTarget([], { x: 0, y: 0 })).toBeNull()
127
+ })
128
+
129
+ test("dropping a card back where it was is not a move", () => {
130
+ // Both sides of its own midpoint resolve to "no change", because
131
+ // removing the card shifts everything below it up by one.
132
+ expect(
133
+ getIsMoveMeaningful({
134
+ fromIndex: 2,
135
+ fromLaneKey: "todo",
136
+ toIndex: 2,
137
+ toLaneKey: "todo",
138
+ }),
139
+ ).toBe(false)
140
+
141
+ expect(
142
+ getIsMoveMeaningful({
143
+ fromIndex: 2,
144
+ fromLaneKey: "todo",
145
+ toIndex: 3,
146
+ toLaneKey: "todo",
147
+ }),
148
+ ).toBe(false)
149
+
150
+ expect(
151
+ getIsMoveMeaningful({
152
+ fromIndex: 2,
153
+ fromLaneKey: "todo",
154
+ toIndex: 4,
155
+ toLaneKey: "todo",
156
+ }),
157
+ ).toBe(true)
158
+ })
159
+
160
+ test("a cross-lane move to the same index is always meaningful", () => {
161
+ expect(
162
+ getIsMoveMeaningful({
163
+ fromIndex: 1,
164
+ fromLaneKey: "todo",
165
+ toIndex: 1,
166
+ toLaneKey: "doing",
167
+ }),
168
+ ).toBe(true)
169
+ })
170
+
171
+ test("a downward within-lane index is corrected for the removal", () => {
172
+ expect(
173
+ toSettledIndex({
174
+ fromIndex: 0,
175
+ fromLaneKey: "todo",
176
+ toIndex: 3,
177
+ toLaneKey: "todo",
178
+ }),
179
+ ).toBe(2)
180
+
181
+ // Upward inside the lane, and any cross-lane move, are already
182
+ // quoted against the list the card is not in.
183
+ expect(
184
+ toSettledIndex({
185
+ fromIndex: 3,
186
+ fromLaneKey: "todo",
187
+ toIndex: 1,
188
+ toLaneKey: "todo",
189
+ }),
190
+ ).toBe(1)
191
+
192
+ expect(
193
+ toSettledIndex({
194
+ fromIndex: 0,
195
+ fromLaneKey: "todo",
196
+ toIndex: 3,
197
+ toLaneKey: "doing",
198
+ }),
199
+ ).toBe(3)
200
+ })
201
+
202
+ test("the announcement names the destination and the position, one-based", () => {
203
+ expect(
204
+ describeMove({
205
+ index: 2,
206
+ laneLabel: "In Progress",
207
+ laneSize: 6,
208
+ title: "Unify the page chrome",
209
+ }),
210
+ ).toBe(
211
+ "Moved Unify the page chrome to In Progress, position 3 of 6.",
212
+ )
213
+ })
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Everything about a board move that is arithmetic rather than
3
+ * React — so it can be tested in Node, exhaustively, without a
4
+ * browser and without a pointer.
5
+ *
6
+ * That split is not tidiness. The pointer path is the one part of
7
+ * this component a test cannot drive convincingly: `userEvent` can
8
+ * dispatch `pointermove`, but it cannot lay anything out, so every
9
+ * rectangle it would reason about is `0x0` in a test environment
10
+ * that has not painted. Pulling the geometry out means the *rule* —
11
+ * which lane, which index, what happens on a tie — is red/green in
12
+ * Node, and the browser test is left proving only the wiring.
13
+ */
14
+
15
+ /**
16
+ * A lane as the drop calculation sees it: a box, a key, and the
17
+ * boxes of the cards inside it.
18
+ *
19
+ * Deliberately plain data rather than elements. `chooseDropTarget`
20
+ * never touches the DOM, which is why it can be called with
21
+ * fixtures.
22
+ */
23
+ export type BoardDropLane = {
24
+ cardRects: readonly BoardRect[]
25
+ key: string
26
+ rect: BoardRect
27
+ }
28
+
29
+ export type BoardPoint = {
30
+ x: number
31
+ y: number
32
+ }
33
+
34
+ /**
35
+ * The subset of `DOMRect` this needs. Named rather than reusing
36
+ * `DOMRect`, because `DOMRect` is a browser global and this module
37
+ * is imported by the Node test project.
38
+ */
39
+ export type BoardRect = {
40
+ height: number
41
+ left: number
42
+ top: number
43
+ width: number
44
+ }
45
+
46
+ export type BoardDropTarget = {
47
+ index: number
48
+ laneKey: string
49
+ }
50
+
51
+ /**
52
+ * `left`/`top` rather than `insetInlineStart`, and that is not a
53
+ * violation of the logical-properties rule.
54
+ *
55
+ * The rule is scoped to `className` literals, because a Tailwind
56
+ * utility is a *style* and a style has a writing direction.
57
+ * `getBoundingClientRect()` returns physical pixels in the
58
+ * viewport's own coordinate space; there is no logical version of it
59
+ * to prefer, and inventing one here would mean re-deriving a number
60
+ * the browser already gave us. The lint rule carves this out
61
+ * explicitly.
62
+ */
63
+ const getIsPointInside = (
64
+ rect: BoardRect,
65
+ point: BoardPoint,
66
+ ): boolean =>
67
+ point.x >= rect.left &&
68
+ point.x <= rect.left + rect.width &&
69
+ point.y >= rect.top &&
70
+ point.y <= rect.top + rect.height
71
+
72
+ /**
73
+ * How far a point is from a rectangle, zero when it is inside.
74
+ *
75
+ * Used only as a fallback, and the fallback is the reason a drag
76
+ * feels finished rather than cancelled: a pointer released two
77
+ * pixels past a lane's edge is a drop *on that lane* as far as the
78
+ * person doing it is concerned, and returning `null` there is how a
79
+ * board earns "it just puts the card back sometimes".
80
+ */
81
+ const getDistanceToRect = (
82
+ rect: BoardRect,
83
+ point: BoardPoint,
84
+ ): number => {
85
+ const dx = Math.max(
86
+ rect.left - point.x,
87
+ 0,
88
+ point.x - (rect.left + rect.width),
89
+ )
90
+
91
+ const dy = Math.max(
92
+ rect.top - point.y,
93
+ 0,
94
+ point.y - (rect.top + rect.height),
95
+ )
96
+
97
+ return Math.sqrt(dx * dx + dy * dy)
98
+ }
99
+
100
+ /**
101
+ * Where in a lane a point lands: the number of cards whose midpoint
102
+ * is above it.
103
+ *
104
+ * Midpoints rather than edges, so the insertion flips when the
105
+ * pointer passes the *centre* of a card. Against edges, the top
106
+ * half of every card is a dead zone that means "same place you
107
+ * already were", and the indicator stutters.
108
+ */
109
+ export const chooseDropIndex = (
110
+ cardRects: readonly BoardRect[],
111
+ point: BoardPoint,
112
+ ): number =>
113
+ cardRects.filter(
114
+ (rect) => point.y > rect.top + rect.height / 2,
115
+ ).length
116
+
117
+ /**
118
+ * The lane and index a pointer is currently over.
119
+ *
120
+ * Hit test first, nearest-lane second, and `null` only when there
121
+ * are no lanes at all — a drag that started has to end somewhere.
122
+ */
123
+ export const chooseDropTarget = (
124
+ lanes: readonly BoardDropLane[],
125
+ point: BoardPoint,
126
+ ): BoardDropTarget | null => {
127
+ if (lanes.length === 0) {
128
+ return null
129
+ }
130
+
131
+ const hitLane = lanes.find((lane) =>
132
+ getIsPointInside(lane.rect, point),
133
+ )
134
+
135
+ const lane =
136
+ hitLane ??
137
+ lanes.reduce((nearest, candidate) =>
138
+ getDistanceToRect(candidate.rect, point) <
139
+ getDistanceToRect(nearest.rect, point)
140
+ ? candidate
141
+ : nearest,
142
+ )
143
+
144
+ return {
145
+ index: chooseDropIndex(lane.cardRects, point),
146
+ laneKey: lane.key,
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Whether a move would actually change anything.
152
+ *
153
+ * The subtle half is `toIndex`. Taking a card out of its own lane
154
+ * shifts every card below it up by one, so "drop it back where it
155
+ * was" arrives as both `fromIndex` and `fromIndex + 1` depending on
156
+ * which side of its own midpoint the pointer ended on. Both are
157
+ * no-ops, and a board that fires `onMove` for them makes a consumer
158
+ * re-render — and, if the consumer is optimistic, flash — for a
159
+ * drag that went nowhere.
160
+ */
161
+ export const getIsMoveMeaningful = ({
162
+ fromIndex,
163
+ fromLaneKey,
164
+ toIndex,
165
+ toLaneKey,
166
+ }: {
167
+ fromIndex: number
168
+ fromLaneKey: string
169
+ toIndex: number
170
+ toLaneKey: string
171
+ }): boolean =>
172
+ fromLaneKey !== toLaneKey ||
173
+ (toIndex !== fromIndex && toIndex !== fromIndex + 1)
174
+
175
+ /**
176
+ * Where a card ends up once it has been removed from its old
177
+ * position.
178
+ *
179
+ * A within-lane move is quoted against the list *including* the
180
+ * card being moved — that is what the pointer was over — but a
181
+ * consumer splicing an array removes it first, so every index below
182
+ * the origin is one too high. Folding that here means the consumer's
183
+ * handler is `splice(from, 1)` then `splice(to, 0, item)` with no
184
+ * arithmetic of its own, which is the version nobody gets wrong.
185
+ */
186
+ export const toSettledIndex = ({
187
+ fromIndex,
188
+ fromLaneKey,
189
+ toIndex,
190
+ toLaneKey,
191
+ }: {
192
+ fromIndex: number
193
+ fromLaneKey: string
194
+ toIndex: number
195
+ toLaneKey: string
196
+ }): number =>
197
+ fromLaneKey === toLaneKey && toIndex > fromIndex
198
+ ? toIndex - 1
199
+ : toIndex
200
+
201
+ /**
202
+ * What the live region says after a move.
203
+ *
204
+ * A sentence rather than a status word, and it names the
205
+ * **destination position** as well as the lane, because "moved to In
206
+ * Progress" leaves a screen-reader user with no idea whether the
207
+ * card went to the top of the lane or the bottom of thirty. That is
208
+ * the single piece of feedback a sighted user gets for free from
209
+ * watching the card land, and it is the one every drag-and-drop
210
+ * implementation forgets to say out loud.
211
+ *
212
+ * One-based, because it is read to a person.
213
+ */
214
+ export const describeMove = ({
215
+ index,
216
+ laneLabel,
217
+ laneSize,
218
+ title,
219
+ }: {
220
+ index: number
221
+ laneLabel: string
222
+ laneSize: number
223
+ title: string
224
+ }): string =>
225
+ `Moved ${title} to ${laneLabel}, position ${index + 1} of ${laneSize}.`