@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,384 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import {
4
+ continueList,
5
+ getMinimalEdit,
6
+ indentLines,
7
+ insertImage,
8
+ insertLink,
9
+ insertText,
10
+ MARKDOWN_LINE_PREFIXES,
11
+ outdentLines,
12
+ toggleHeading,
13
+ toggleInlineMarker,
14
+ toggleLinePrefix,
15
+ toMarkdownImage,
16
+ } from "./markdownCommands.ts"
17
+
18
+ const at = (text: string, marker = "|") => {
19
+ const selectionStart = text.indexOf(marker)
20
+
21
+ const withoutFirst = text.replace(marker, "")
22
+
23
+ const second = withoutFirst.indexOf(marker)
24
+
25
+ return {
26
+ selectionEnd: second === -1 ? selectionStart : second,
27
+ selectionStart,
28
+ text: withoutFirst.replace(marker, ""),
29
+ }
30
+ }
31
+
32
+ test("bold wraps a selection", () => {
33
+ expect(
34
+ toggleInlineMarker(at("a |word| b"), "**"),
35
+ ).toEqual({
36
+ selectionEnd: 8,
37
+ selectionStart: 4,
38
+ text: "a **word** b",
39
+ })
40
+ })
41
+
42
+ /**
43
+ * The case a hand-rolled toggle misses. Double-click a word inside
44
+ * `**bold**` and the browser selects `bold`, not `**bold**` — so
45
+ * looking only *inside* the selection produces `****bold****`.
46
+ */
47
+ test("bold unwraps when the markers are outside the selection", () => {
48
+ expect(
49
+ toggleInlineMarker(at("a **|word|** b"), "**"),
50
+ ).toEqual({
51
+ selectionEnd: 6,
52
+ selectionStart: 2,
53
+ text: "a word b",
54
+ })
55
+ })
56
+
57
+ test("bold unwraps when the markers are inside the selection", () => {
58
+ expect(
59
+ toggleInlineMarker(at("a |**word**| b"), "**"),
60
+ ).toEqual({
61
+ selectionEnd: 6,
62
+ selectionStart: 2,
63
+ text: "a word b",
64
+ })
65
+ })
66
+
67
+ test("bold with no selection parks the caret between the markers", () => {
68
+ const result = toggleInlineMarker(at("a |b"), "**")
69
+
70
+ expect(result.text).toBe("a ****b")
71
+
72
+ expect(result.selectionStart).toBe(4)
73
+
74
+ expect(result.selectionEnd).toBe(4)
75
+ })
76
+
77
+ test("a bulleted list toggles across every selected line", () => {
78
+ const bulleted = toggleLinePrefix(
79
+ at("|one\ntwo\nthree|"),
80
+ MARKDOWN_LINE_PREFIXES.bulletList,
81
+ )
82
+
83
+ expect(bulleted.text).toBe("- one\n- two\n- three")
84
+
85
+ expect(
86
+ toggleLinePrefix(
87
+ { ...bulleted, selectionStart: 0 },
88
+ MARKDOWN_LINE_PREFIXES.bulletList,
89
+ ).text,
90
+ ).toBe("one\ntwo\nthree")
91
+ })
92
+
93
+ test("a numbered list renumbers rather than repeating `1.`", () => {
94
+ expect(
95
+ toggleLinePrefix(
96
+ at("|one\ntwo\nthree|"),
97
+ MARKDOWN_LINE_PREFIXES.orderedList,
98
+ ).text,
99
+ ).toBe("1. one\n2. two\n3. three")
100
+ })
101
+
102
+ /**
103
+ * A mixed block gets the prefix added to all of it. Removal wins
104
+ * only when *every* line already has it — otherwise the button's
105
+ * effect depends on invisible state.
106
+ */
107
+ test("a mixed block gains the prefix rather than losing it", () => {
108
+ expect(
109
+ toggleLinePrefix(
110
+ at("|- one\ntwo|"),
111
+ MARKDOWN_LINE_PREFIXES.bulletList,
112
+ ).text,
113
+ ).toBe("- one\n- two")
114
+ })
115
+
116
+ test("a task replaces a bullet instead of nesting inside it", () => {
117
+ expect(
118
+ toggleLinePrefix(
119
+ at("|- one|"),
120
+ MARKDOWN_LINE_PREFIXES.taskList,
121
+ ).text,
122
+ ).toBe("- [ ] one")
123
+ })
124
+
125
+ test("indentation survives a prefix toggle", () => {
126
+ expect(
127
+ toggleLinePrefix(
128
+ at(" |one|"),
129
+ MARKDOWN_LINE_PREFIXES.bulletList,
130
+ ).text,
131
+ ).toBe(" - one")
132
+ })
133
+
134
+ test("a heading replaces a heading rather than stacking", () => {
135
+ expect(toggleHeading(at("### |a|"), 2).text).toBe("## a")
136
+
137
+ expect(toggleHeading(at("## |a|"), 2).text).toBe("a")
138
+ })
139
+
140
+ test("Enter continues a bullet, a task, an ordered item and a quote", () => {
141
+ expect(continueList(at("- one|"))?.text).toBe("- one\n- ")
142
+
143
+ expect(continueList(at("- [x] one|"))?.text).toBe(
144
+ "- [x] one\n- [ ] ",
145
+ )
146
+
147
+ expect(continueList(at("3. one|"))?.text).toBe(
148
+ "3. one\n4. ",
149
+ )
150
+
151
+ expect(continueList(at("> one|"))?.text).toBe("> one\n> ")
152
+
153
+ expect(continueList(at(" - one|"))?.text).toBe(
154
+ " - one\n - ",
155
+ )
156
+ })
157
+
158
+ /**
159
+ * Without this an empty bullet is a trap: every Enter makes another
160
+ * empty bullet and the only way out is Backspace.
161
+ */
162
+ test("Enter on an empty item clears the marker", () => {
163
+ const cleared = continueList(at("- one\n- |"))
164
+
165
+ expect(cleared?.text).toBe("- one\n")
166
+
167
+ expect(cleared?.selectionStart).toBe(6)
168
+ })
169
+
170
+ /**
171
+ * `null` rather than an unchanged state, so the component does not
172
+ * call `preventDefault` and the Enter lands on the native undo
173
+ * stack as an ordinary typing step.
174
+ */
175
+ test("Enter outside a list reports null", () => {
176
+ expect(continueList(at("plain|"))).toBeNull()
177
+
178
+ // A selection is a replacement, not a continuation.
179
+ expect(continueList(at("- |one|"))).toBeNull()
180
+ })
181
+
182
+ test("indent and outdent move whole lines", () => {
183
+ const indented = indentLines(at("|one\ntwo|"))
184
+
185
+ expect(indented.text).toBe(" one\n two")
186
+
187
+ expect(
188
+ outdentLines({ ...indented, selectionStart: 0 }).text,
189
+ ).toBe("one\ntwo")
190
+
191
+ // Outdenting a line with nothing to give leaves it alone rather
192
+ // than eating its first character.
193
+ expect(outdentLines(at("|one|")).text).toBe("one")
194
+ })
195
+
196
+ test("a link parks the caret in the destination", () => {
197
+ const linked = insertLink(at("see |this| page"))
198
+
199
+ expect(linked.text).toBe("see [this]() page")
200
+
201
+ expect(linked.selectionStart).toBe(11)
202
+
203
+ expect(linked.selectionEnd).toBe(11)
204
+ })
205
+
206
+ test("an image escapes its alt text and brackets a dirty URL", () => {
207
+ expect(
208
+ toMarkdownImage({
209
+ alt: "a [bracket] and a \\slash",
210
+ url: "https://example.test/a.png",
211
+ }),
212
+ ).toBe(
213
+ "![a \\[bracket\\] and a \\\\slash](https://example.test/a.png)",
214
+ )
215
+
216
+ expect(
217
+ toMarkdownImage({
218
+ alt: "spaced",
219
+ url: "https://example.test/one two.png",
220
+ }),
221
+ ).toBe("![spaced](<https://example.test/one two.png>)")
222
+
223
+ // A newline in either half would end the construct early and
224
+ // leave stray text in the document.
225
+ expect(
226
+ toMarkdownImage({
227
+ alt: "two\nlines",
228
+ url: "https://example.test/a\n.png",
229
+ }),
230
+ ).toBe("![two lines](https://example.test/a.png)")
231
+ })
232
+
233
+ test("an image lands at the caret", () => {
234
+ expect(
235
+ insertImage(at("before | after"), {
236
+ alt: "shot",
237
+ url: "https://example.test/s.png",
238
+ }).text,
239
+ ).toBe("before ![shot](https://example.test/s.png) after")
240
+ })
241
+
242
+ /**
243
+ * **The no-HTML round trip.**
244
+ *
245
+ * Docket exists partly because HTML in a description field
246
+ * destroyed a previous tracker's data, so this is the assertion
247
+ * that has to hold no matter what else changes: put in a value full
248
+ * of the characters that tempt escaping, run every command over it,
249
+ * and there must still be no tag and no entity in the string that
250
+ * gets stored.
251
+ *
252
+ * It is a strong test because it is checking a *structural*
253
+ * property rather than a behaviour: none of these functions
254
+ * constructs HTML, so none of them can leak it. If somebody later
255
+ * reaches for a "render to HTML and parse it back" shortcut, this
256
+ * is what fails.
257
+ */
258
+ test("no command can put an HTML tag or an entity in the value", () => {
259
+ const dangerous = [
260
+ "<script>alert('xss')</script>",
261
+ "<b>already bold</b>",
262
+ "a < b && c > d",
263
+ "&amp; &lt; &gt; &quot; &#x27;",
264
+ "\"double\" and 'single' quotes",
265
+ "5 < 6 & 7 > 6",
266
+ "- <li>a list item that is not one</li>",
267
+ "| <td> | </td> |",
268
+ ].join("\n")
269
+
270
+ const wholeDocument = {
271
+ selectionEnd: dangerous.length,
272
+ selectionStart: 0,
273
+ text: dangerous,
274
+ }
275
+
276
+ // Everything that transforms the document while leaving its
277
+ // characters in place. An escaping pass anywhere in the pipeline
278
+ // would shred all three of the fragments below.
279
+ const transformed = [
280
+ toggleInlineMarker(wholeDocument, "**"),
281
+ toggleInlineMarker(wholeDocument, "`"),
282
+ toggleHeading(wholeDocument, 3),
283
+ toggleLinePrefix(
284
+ wholeDocument,
285
+ MARKDOWN_LINE_PREFIXES.bulletList,
286
+ ),
287
+ toggleLinePrefix(
288
+ wholeDocument,
289
+ MARKDOWN_LINE_PREFIXES.orderedList,
290
+ ),
291
+ toggleLinePrefix(
292
+ wholeDocument,
293
+ MARKDOWN_LINE_PREFIXES.taskList,
294
+ ),
295
+ toggleLinePrefix(
296
+ wholeDocument,
297
+ MARKDOWN_LINE_PREFIXES.blockquote,
298
+ ),
299
+ indentLines(wholeDocument),
300
+ outdentLines(wholeDocument),
301
+ insertLink(wholeDocument, "https://example.test/x"),
302
+ ]
303
+
304
+ for (const result of transformed) {
305
+ // Verbatim, not merely "contains no `&lt;`" — the source
306
+ // deliberately *has* `&lt;` in it as literal text, so a naive
307
+ // absence check would pass on an escaped value and fail on a
308
+ // correct one.
309
+ expect(result.text).toContain(
310
+ "<script>alert('xss')</script>",
311
+ )
312
+
313
+ expect(result.text).toContain("<b>already bold</b>")
314
+
315
+ expect(result.text).toContain(
316
+ "&amp; &lt; &gt; &quot; &#x27;",
317
+ )
318
+
319
+ expect(result.text).not.toContain("&lt;script&gt;")
320
+ }
321
+
322
+ // The two commands that *replace* the selection rather than
323
+ // transforming it, checked on what they emit.
324
+ expect(
325
+ insertImage(wholeDocument, {
326
+ alt: "<img onerror=alert(1)>",
327
+ url: "https://example.test/x.png",
328
+ }).text,
329
+ ).toBe(
330
+ "![<img onerror=alert(1)>](https://example.test/x.png)",
331
+ )
332
+
333
+ expect(insertText(wholeDocument, dangerous).text).toBe(
334
+ dangerous,
335
+ )
336
+
337
+ // And the round trip: typing the value straight back in returns
338
+ // it character for character.
339
+ expect(
340
+ insertText(
341
+ { selectionEnd: 0, selectionStart: 0, text: "" },
342
+ dangerous,
343
+ ).text,
344
+ ).toBe(dangerous)
345
+ })
346
+
347
+ /**
348
+ * Whole-string assignment wipes the browser's undo stack, so every
349
+ * command's result is turned back into the smallest range
350
+ * replacement and applied through the platform's own editing
351
+ * pipeline. One bold toggle should be one undo step over four
352
+ * characters, not a document-sized one.
353
+ */
354
+ test("the minimal edit is the smallest range that explains the change", () => {
355
+ expect(
356
+ getMinimalEdit("a word b", "a **word** b"),
357
+ ).toEqual({ end: 6, start: 2, text: "**word**" })
358
+
359
+ expect(getMinimalEdit("same", "same")).toEqual({
360
+ end: 4,
361
+ start: 4,
362
+ text: "",
363
+ })
364
+
365
+ expect(getMinimalEdit("abc", "ac")).toEqual({
366
+ end: 2,
367
+ start: 1,
368
+ text: "",
369
+ })
370
+
371
+ // Repetitive strings are where a naive prefix/suffix scan
372
+ // produces an overlapping — i.e. negative-length — range.
373
+ expect(getMinimalEdit("aaaa", "aaaaa")).toEqual({
374
+ end: 4,
375
+ start: 4,
376
+ text: "a",
377
+ })
378
+
379
+ expect(getMinimalEdit("aaaaa", "aaaa")).toEqual({
380
+ end: 5,
381
+ start: 4,
382
+ text: "",
383
+ })
384
+ })