@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,147 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SlotProps } from "../slotProps.ts";
3
+ export type MarkdownImageUpload = {
4
+ /** Alt text. Defaults to the file's name when omitted. */
5
+ alt?: string;
6
+ /** Where the image now lives. Inserted verbatim. */
7
+ url: string;
8
+ };
9
+ /**
10
+ * The toolbar's glyphs, by action.
11
+ *
12
+ * **No default, and none shipped** — this package ships no icons,
13
+ * and a `⋯`/`▨` default renders as nothing where the font lacks it
14
+ * ([decision](../../../../docs/decisions/2026-07-29-ship-no-icons-and-no-symbol-glyphs.md)).
15
+ * With no icons the toolbar is nine word buttons, which works
16
+ * everywhere and collapses into the overflow menu early; an app
17
+ * that brings lucide gets a compact bar and keeps the words as the
18
+ * accessible names.
19
+ */
20
+ export type MarkdownEditorIcons = Partial<Record<"bold" | "bulletedList" | "code" | "heading" | "italic" | "link" | "numberedList" | "quote" | "taskList", ReactNode>>;
21
+ export type MarkdownEditorProps = SlotProps & {
22
+ className?: string;
23
+ /**
24
+ * **Initial** only. This control is uncontrolled, and the long
25
+ * reason is in the doc comment below — it is the whole design.
26
+ */
27
+ defaultValue?: string;
28
+ /** See `MarkdownEditorIcons`. */
29
+ icons?: MarkdownEditorIcons;
30
+ isDisabled?: boolean;
31
+ isReadOnly?: boolean;
32
+ /**
33
+ * The accessible name, as `aria-label`. Omit it inside a `Field`,
34
+ * which supplies a real `<label htmlFor>` — same contract as
35
+ * `Select`.
36
+ */
37
+ label?: string;
38
+ onChange?: (value: string) => void;
39
+ /**
40
+ * A failed upload, after the placeholder has been taken back out
41
+ * of the text. Pair it with a `Toast`; this component does not
42
+ * own one.
43
+ */
44
+ onUploadError?: (file: File, error: unknown) => void;
45
+ /**
46
+ * Paste or drop an image and this runs. **The component does not
47
+ * own storage** — it hands you a `File`, you put it somewhere,
48
+ * and the URL you return is inserted at the caret as
49
+ * `![alt](url)`. Docket keeps its own blob storage precisely so
50
+ * the URL stays stable and theirs.
51
+ *
52
+ * Omit it and image paste is simply not offered: a `<textarea>`
53
+ * ignores a pasted image on its own, which is the correct
54
+ * fallback rather than a broken one.
55
+ */
56
+ onUploadImage?: (file: File) => Promise<MarkdownImageUpload>;
57
+ placeholder?: string;
58
+ /** The editing surface's accessible name in the toolbar's label. */
59
+ toolbarLabel?: string;
60
+ };
61
+ /**
62
+ * A live hybrid markdown editor whose stored value is markdown and
63
+ * only ever markdown.
64
+ *
65
+ * Docket asked for this one and the requirement is unusually sharp,
66
+ * because the tracker it replaces destroyed data by storing HTML in
67
+ * a field it later tried to read back as text. So the constraint
68
+ * here is not "prefer markdown" — it is that **an HTML tag must not
69
+ * be able to reach the stored string**, and the design is chosen to
70
+ * make that mechanical rather than policed:
71
+ *
72
+ * - the editing surface is a `<textarea>`, which takes
73
+ * `text/plain` off the clipboard and nothing else. Copy a
74
+ * styled paragraph out of a browser and the `text/html` flavour
75
+ * is simply not read;
76
+ * - every command in `markdownCommands.ts` returns markdown, and
77
+ * there is no serialiser anywhere in the component;
78
+ * - the painted layer emits `{ kind, text }` spans that React
79
+ * renders as **text nodes**. No `innerHTML`, no sanitiser, and
80
+ * nothing to get wrong — a `<script>` in the source is four
81
+ * coloured characters on screen.
82
+ *
83
+ * `MarkdownEditor.test.tsx` round-trips a value full of the
84
+ * characters that tempt escaping and asserts no tag ever appears.
85
+ *
86
+ * ## Why a `<textarea>` and not an editor core
87
+ *
88
+ * Charcuterie is published, so a dependency here is a permanent
89
+ * cost to every consumer of the package — measured, for a markdown
90
+ * setup: CodeMirror 6 at 176 KB gzipped, Lexical at 148 KB, TipTap
91
+ * at 238 KB. This component adds **nothing**. The full argument,
92
+ * with the numbers and with what the choice gives up, is
93
+ * `docs/decisions/2026-08-19-the-markdown-editor-is-a-textarea-with-a-painted-layer.md`.
94
+ *
95
+ * The accessibility half of that argument is worth repeating here,
96
+ * because it is the half that is hard to buy back later. A
97
+ * `<textarea>` arrives with caret movement, word-wise selection,
98
+ * `Home`/`End`, IME composition, spellcheck, autocorrect, the
99
+ * platform's own undo stack, `role="textbox"` and
100
+ * `aria-multiline="true"` — all of it, in every browser, tested by
101
+ * the vendors. A `contenteditable` starts with none of those and
102
+ * re-earns each one, and the ones it never fully re-earns are IME
103
+ * and undo. This library's central claim is that an agent and a
104
+ * keyboard can drive every component; starting from the platform's
105
+ * own text control is the cheapest way to keep it.
106
+ *
107
+ * ## It is uncontrolled, and that is not a shortcut
108
+ *
109
+ * `defaultValue` in, `onChange` out — the same contract as `Select`
110
+ * ([decision](../../../../docs/decisions/2026-07-31-select-is-uncontrolled-because-the-platform-owns-the-value.md)),
111
+ * and here it protects something specific. A controlled React
112
+ * `<textarea>` round-trips every keystroke through render, and any
113
+ * programmatic `value` assignment **wipes the browser's undo
114
+ * stack**: Ctrl+Z after a toolbar click then skips the paragraph
115
+ * you typed before it, or does nothing at all.
116
+ *
117
+ * So the textarea is never handed a `value`. Its text is mirrored
118
+ * into state for *painting* only, and every programmatic edit — a
119
+ * toolbar action, a shortcut, an image insertion — is applied as a
120
+ * range replacement through `document.execCommand("insertText")`,
121
+ * which is deprecated, is the only API that appends to the native
122
+ * undo stack, and is exactly why `getMinimalEdit` exists.
123
+ *
124
+ * A consumer that must swap the whole document — Docket opening a
125
+ * different task — remounts with a `key`. That is one line, and it
126
+ * is honest about the fact that the old undo history belonged to
127
+ * the old document.
128
+ *
129
+ * ## Composition, and the one component that did not fit
130
+ *
131
+ * The toolbar is this package's `Toolbar`, so the row collapses
132
+ * into a menu **by measurement** rather than at a breakpoint —
133
+ * which is what the Narrow View actually needs, since a lane in a
134
+ * three-up board is narrow on a 4K display. `Field` wraps it
135
+ * through `SlotProps`, so a label, a description and an error wire
136
+ * themselves to the textarea.
137
+ *
138
+ * `FileDropZone` does **not** wrap it, and that is the interesting
139
+ * one. That component is a `<label>` around a file input covering
140
+ * its whole area — correct for "drop a disc image here", and wrong
141
+ * for a text surface, where the drop has to land *at a character
142
+ * offset* and the area has to keep taking clicks as caret
143
+ * placement. So the editor handles its own drop, and `FileDropZone`
144
+ * stays the right answer for the attachment list beside it.
145
+ */
146
+ export declare const MarkdownEditor: ({ className, defaultValue, icons, isDisabled, isReadOnly, label, onChange, onUploadError, onUploadImage, placeholder, toolbarLabel, ...receivedSlotProps }: MarkdownEditorProps) => ReactNode;
147
+ //# sourceMappingURL=MarkdownEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownEditor.d.ts","sourceRoot":"","sources":["../../src/MarkdownEditor/MarkdownEditor.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAwBhD,MAAM,MAAM,mBAAmB,GAAG;IAChC,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,mBAAmB,GAAG,OAAO,CACvC,MAAM,CACF,MAAM,GACN,cAAc,GACd,MAAM,GACN,SAAS,GACT,QAAQ,GACR,MAAM,GACN,cAAc,GACd,OAAO,GACP,UAAU,EACZ,SAAS,CACV,CACF,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iCAAiC;IACjC,KAAK,CAAC,EAAE,mBAAmB,CAAA;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IACpD;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,CACd,IAAI,EAAE,IAAI,KACP,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AA+DD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoFG;AACH,eAAO,MAAM,cAAc,GAAI,4JAa5B,mBAAmB,KAAG,SAilBxB,CAAA"}
@@ -0,0 +1,492 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useUniqueId } from "@charcuterie/logic";
3
+ import { Fragment, useRef, useState } from "react";
4
+ import { mergeSlotProps } from "../slotProps.js";
5
+ import { Toolbar } from "../Toolbar/Toolbar.js";
6
+ import { toClassName } from "../toClassName.js";
7
+ import { continueList, getMinimalEdit, indentLines, insertLink, insertText, MARKDOWN_LINE_PREFIXES, outdentLines, toggleHeading, toggleInlineMarker, toggleLinePrefix, toMarkdownImage, } from "./markdownCommands.js";
8
+ import { toLineIndex, toMarkdownLines, } from "./markdownSpans.js";
9
+ /**
10
+ * The paint for each span kind.
11
+ *
12
+ * **Written out, never interpolated** — `` `text-${kind}` ``
13
+ * generates nothing at all and fails silently, which is the rule
14
+ * `intentStyles.ts` is 48 literals for.
15
+ *
16
+ * ### Every one of these is metric-neutral, and that is the design
17
+ *
18
+ * The editing surface is a real `<textarea>` with this painted
19
+ * layer behind it in the same grid cell. The textarea owns the
20
+ * caret; the layer owns the colour. The two only stay registered
21
+ * while they **lay out identically**, so anything that changes a
22
+ * glyph's advance width — `font-bold`, `text-lg`, a different
23
+ * family, `tracking-wide` — shears the caret away from the letters
24
+ * it is supposed to be sitting between. Not subtly: by a character
25
+ * or two per emphasis run, cumulative across the line.
26
+ *
27
+ * So bold is faux bold. `text-shadow` at ±0.02em paints the glyph
28
+ * twice, a hair apart, which thickens it and moves nothing.
29
+ * Italic is real, because the surface is monospaced and a
30
+ * monospaced italic keeps the advance width by definition. Headings
31
+ * get weight and colour rather than size, for the same reason.
32
+ *
33
+ * This is the honest limit of the textarea-plus-layer approach and
34
+ * it is written down here rather than discovered later:
35
+ * **markers are dimmed, not hidden.** Concealing `**` would change
36
+ * the line's width and there is no way to do it in this medium.
37
+ * The staging decision record has the rest.
38
+ */
39
+ const SPAN_KIND_CLASS = {
40
+ code: "text-intent-info-content",
41
+ emphasis: "italic text-content-primary",
42
+ heading: "text-content-primary [text-shadow:0.02em_0_0_currentColor,-0.02em_0_0_currentColor]",
43
+ link: "text-intent-accent-content underline",
44
+ marker: "",
45
+ plain: "text-content-primary",
46
+ strikethrough: "text-content-secondary line-through",
47
+ strong: "text-content-primary [text-shadow:0.02em_0_0_currentColor,-0.02em_0_0_currentColor]",
48
+ url: "text-content-secondary underline",
49
+ };
50
+ /**
51
+ * The one thing that changes with the caret.
52
+ *
53
+ * Obsidian's live preview reveals a line's raw syntax when the
54
+ * cursor enters it. Here the markers are always present — they have
55
+ * to be, see above — so "reveal" becomes a contrast change: muted
56
+ * everywhere, accent on the line you are editing. Both are real
57
+ * token roles that clear AA on their own, which an `opacity`
58
+ * treatment would not.
59
+ */
60
+ const ACTIVE_MARKER_CLASS = "text-intent-accent-content";
61
+ const RESTING_MARKER_CLASS = "text-content-muted";
62
+ const isImageFile = (file) => file.type.startsWith("image/");
63
+ /**
64
+ * A live hybrid markdown editor whose stored value is markdown and
65
+ * only ever markdown.
66
+ *
67
+ * Docket asked for this one and the requirement is unusually sharp,
68
+ * because the tracker it replaces destroyed data by storing HTML in
69
+ * a field it later tried to read back as text. So the constraint
70
+ * here is not "prefer markdown" — it is that **an HTML tag must not
71
+ * be able to reach the stored string**, and the design is chosen to
72
+ * make that mechanical rather than policed:
73
+ *
74
+ * - the editing surface is a `<textarea>`, which takes
75
+ * `text/plain` off the clipboard and nothing else. Copy a
76
+ * styled paragraph out of a browser and the `text/html` flavour
77
+ * is simply not read;
78
+ * - every command in `markdownCommands.ts` returns markdown, and
79
+ * there is no serialiser anywhere in the component;
80
+ * - the painted layer emits `{ kind, text }` spans that React
81
+ * renders as **text nodes**. No `innerHTML`, no sanitiser, and
82
+ * nothing to get wrong — a `<script>` in the source is four
83
+ * coloured characters on screen.
84
+ *
85
+ * `MarkdownEditor.test.tsx` round-trips a value full of the
86
+ * characters that tempt escaping and asserts no tag ever appears.
87
+ *
88
+ * ## Why a `<textarea>` and not an editor core
89
+ *
90
+ * Charcuterie is published, so a dependency here is a permanent
91
+ * cost to every consumer of the package — measured, for a markdown
92
+ * setup: CodeMirror 6 at 176 KB gzipped, Lexical at 148 KB, TipTap
93
+ * at 238 KB. This component adds **nothing**. The full argument,
94
+ * with the numbers and with what the choice gives up, is
95
+ * `docs/decisions/2026-08-19-the-markdown-editor-is-a-textarea-with-a-painted-layer.md`.
96
+ *
97
+ * The accessibility half of that argument is worth repeating here,
98
+ * because it is the half that is hard to buy back later. A
99
+ * `<textarea>` arrives with caret movement, word-wise selection,
100
+ * `Home`/`End`, IME composition, spellcheck, autocorrect, the
101
+ * platform's own undo stack, `role="textbox"` and
102
+ * `aria-multiline="true"` — all of it, in every browser, tested by
103
+ * the vendors. A `contenteditable` starts with none of those and
104
+ * re-earns each one, and the ones it never fully re-earns are IME
105
+ * and undo. This library's central claim is that an agent and a
106
+ * keyboard can drive every component; starting from the platform's
107
+ * own text control is the cheapest way to keep it.
108
+ *
109
+ * ## It is uncontrolled, and that is not a shortcut
110
+ *
111
+ * `defaultValue` in, `onChange` out — the same contract as `Select`
112
+ * ([decision](../../../../docs/decisions/2026-07-31-select-is-uncontrolled-because-the-platform-owns-the-value.md)),
113
+ * and here it protects something specific. A controlled React
114
+ * `<textarea>` round-trips every keystroke through render, and any
115
+ * programmatic `value` assignment **wipes the browser's undo
116
+ * stack**: Ctrl+Z after a toolbar click then skips the paragraph
117
+ * you typed before it, or does nothing at all.
118
+ *
119
+ * So the textarea is never handed a `value`. Its text is mirrored
120
+ * into state for *painting* only, and every programmatic edit — a
121
+ * toolbar action, a shortcut, an image insertion — is applied as a
122
+ * range replacement through `document.execCommand("insertText")`,
123
+ * which is deprecated, is the only API that appends to the native
124
+ * undo stack, and is exactly why `getMinimalEdit` exists.
125
+ *
126
+ * A consumer that must swap the whole document — Docket opening a
127
+ * different task — remounts with a `key`. That is one line, and it
128
+ * is honest about the fact that the old undo history belonged to
129
+ * the old document.
130
+ *
131
+ * ## Composition, and the one component that did not fit
132
+ *
133
+ * The toolbar is this package's `Toolbar`, so the row collapses
134
+ * into a menu **by measurement** rather than at a breakpoint —
135
+ * which is what the Narrow View actually needs, since a lane in a
136
+ * three-up board is narrow on a 4K display. `Field` wraps it
137
+ * through `SlotProps`, so a label, a description and an error wire
138
+ * themselves to the textarea.
139
+ *
140
+ * `FileDropZone` does **not** wrap it, and that is the interesting
141
+ * one. That component is a `<label>` around a file input covering
142
+ * its whole area — correct for "drop a disc image here", and wrong
143
+ * for a text surface, where the drop has to land *at a character
144
+ * offset* and the area has to keep taking clicks as caret
145
+ * placement. So the editor handles its own drop, and `FileDropZone`
146
+ * stays the right answer for the attachment list beside it.
147
+ */
148
+ export const MarkdownEditor = ({ className, defaultValue = "", icons, isDisabled = false, isReadOnly = false, label, onChange, onUploadError, onUploadImage, placeholder, toolbarLabel = "Markdown formatting", ...receivedSlotProps }) => {
149
+ const baseId = useUniqueId();
150
+ const hintId = `${baseId}-hint`;
151
+ const statusId = `${baseId}-status`;
152
+ const textareaRef = useRef(null);
153
+ const layerRef = useRef(null);
154
+ const uploadCount = useRef(0);
155
+ const [value, setValue] = useState(defaultValue);
156
+ const [caretOffset, setCaretOffset] = useState(0);
157
+ const [isFocused, setIsFocused] = useState(false);
158
+ const [uploadMessage, setUploadMessage] = useState("");
159
+ const readSelection = () => {
160
+ const element = textareaRef.current;
161
+ return {
162
+ selectionEnd: element?.selectionEnd ?? 0,
163
+ selectionStart: element?.selectionStart ?? 0,
164
+ text: element?.value ?? "",
165
+ };
166
+ };
167
+ const syncFromElement = () => {
168
+ const element = textareaRef.current;
169
+ if (!element) {
170
+ return;
171
+ }
172
+ setCaretOffset(element.selectionStart);
173
+ if (element.value === value) {
174
+ return;
175
+ }
176
+ setValue(element.value);
177
+ onChange?.(element.value);
178
+ };
179
+ /**
180
+ * Apply a command's result to the live control **without**
181
+ * assigning `value`.
182
+ *
183
+ * `execCommand` is deprecated and is the only route that adds to
184
+ * the browser's own undo stack, so it is tried first and the
185
+ * modern `setRangeText` is the fallback for whatever eventually
186
+ * removes it. Losing undo is a degradation; losing the edit would
187
+ * not be.
188
+ */
189
+ const applyEdit = (next) => {
190
+ const element = textareaRef.current;
191
+ if (!element) {
192
+ return;
193
+ }
194
+ const edit = getMinimalEdit(element.value, next.text);
195
+ element.focus();
196
+ element.setSelectionRange(edit.start, edit.end);
197
+ const isApplied = (() => {
198
+ try {
199
+ return edit.text === ""
200
+ ? document.execCommand("delete")
201
+ : document.execCommand("insertText", false, edit.text);
202
+ }
203
+ catch {
204
+ return false;
205
+ }
206
+ })();
207
+ if (!isApplied) {
208
+ element.setRangeText(edit.text, edit.start, edit.end, "end");
209
+ }
210
+ element.setSelectionRange(next.selectionStart, next.selectionEnd);
211
+ syncFromElement();
212
+ };
213
+ const runCommand = (command) => {
214
+ if (isDisabled || isReadOnly) {
215
+ return;
216
+ }
217
+ applyEdit(command(readSelection()));
218
+ };
219
+ /**
220
+ * Upload one file, with a **markdown** placeholder holding the
221
+ * caret's place while the request is in flight.
222
+ *
223
+ * The placeholder is real text in the document rather than an
224
+ * overlay, because the user keeps typing during an upload and the
225
+ * insertion point has to move with what they type. It carries a
226
+ * counter so two files of the same name do not resolve into each
227
+ * other's slot, and it is markdown like everything else — a
228
+ * failed upload leaves the document in a state the user can read
229
+ * and fix, not a dangling widget.
230
+ */
231
+ const uploadImage = async (file) => {
232
+ if (!onUploadImage) {
233
+ return;
234
+ }
235
+ uploadCount.current += 1;
236
+ const placeholder = `![Uploading ${file.name}](#uploading-${uploadCount.current})`;
237
+ applyEdit(insertText(readSelection(), placeholder));
238
+ setUploadMessage(`Uploading ${file.name}`);
239
+ const replacePlaceholder = (insertion) => {
240
+ const element = textareaRef.current;
241
+ if (!element) {
242
+ return;
243
+ }
244
+ const start = element.value.indexOf(placeholder);
245
+ if (start === -1) {
246
+ return;
247
+ }
248
+ const end = start + placeholder.length;
249
+ applyEdit({
250
+ selectionEnd: start + insertion.length,
251
+ selectionStart: start + insertion.length,
252
+ text: element.value.slice(0, start) +
253
+ insertion +
254
+ element.value.slice(end),
255
+ });
256
+ };
257
+ try {
258
+ const uploaded = await onUploadImage(file);
259
+ replacePlaceholder(toMarkdownImage({
260
+ alt: uploaded.alt ?? file.name,
261
+ url: uploaded.url,
262
+ }));
263
+ setUploadMessage(`Inserted ${file.name}`);
264
+ }
265
+ catch (error) {
266
+ replacePlaceholder("");
267
+ setUploadMessage(`Could not upload ${file.name}`);
268
+ onUploadError?.(file, error);
269
+ }
270
+ };
271
+ const uploadImages = (files) => {
272
+ for (const file of files.filter(isImageFile)) {
273
+ void uploadImage(file);
274
+ }
275
+ };
276
+ const handlePaste = (pasteEvent) => {
277
+ const files = Array.from(pasteEvent.clipboardData.files).filter(isImageFile);
278
+ if (files.length === 0 || !onUploadImage) {
279
+ // Left to the browser on purpose. A `<textarea>` pastes
280
+ // `text/plain` and nothing else, so the no-HTML guarantee
281
+ // needs no code here at all — intercepting the paste to
282
+ // "sanitise" it would be the version that can have a bug.
283
+ return;
284
+ }
285
+ pasteEvent.preventDefault();
286
+ uploadImages(files);
287
+ };
288
+ const handleDrop = (dropEvent) => {
289
+ const files = Array.from(dropEvent.dataTransfer.files).filter(isImageFile);
290
+ if (files.length === 0 || !onUploadImage) {
291
+ return;
292
+ }
293
+ dropEvent.preventDefault();
294
+ uploadImages(files);
295
+ };
296
+ /**
297
+ * `Tab` is never captured, at any time.
298
+ *
299
+ * Indenting a list with Tab is the convention in editors that own
300
+ * the whole page, and inside a form it is a keyboard trap: WCAG
301
+ * 2.1.2 wants focus to be able to leave every component with the
302
+ * standard gesture, and a multi-line field on a task form is
303
+ * exactly where somebody tabs onward. Indent and outdent are
304
+ * `Ctrl+]` and `Ctrl+[` instead, which is what browsers'
305
+ * own editors bind them to.
306
+ */
307
+ const handleKeyDown = (keyEvent) => {
308
+ if (isReadOnly || isDisabled) {
309
+ return;
310
+ }
311
+ if (keyEvent.key === "Enter" &&
312
+ !keyEvent.shiftKey &&
313
+ !keyEvent.altKey &&
314
+ !keyEvent.ctrlKey &&
315
+ !keyEvent.metaKey) {
316
+ const continued = continueList(readSelection());
317
+ if (continued) {
318
+ keyEvent.preventDefault();
319
+ applyEdit(continued);
320
+ }
321
+ return;
322
+ }
323
+ const isModified = keyEvent.ctrlKey || keyEvent.metaKey;
324
+ if (!isModified) {
325
+ return;
326
+ }
327
+ const command = (() => {
328
+ switch (keyEvent.key.toLowerCase()) {
329
+ case "[": {
330
+ return outdentLines;
331
+ }
332
+ case "]": {
333
+ return indentLines;
334
+ }
335
+ case "b": {
336
+ return (state) => toggleInlineMarker(state, "**");
337
+ }
338
+ case "e": {
339
+ return (state) => toggleInlineMarker(state, "`");
340
+ }
341
+ case "i": {
342
+ return (state) => toggleInlineMarker(state, "_");
343
+ }
344
+ case "k": {
345
+ return (state) => insertLink(state);
346
+ }
347
+ default: {
348
+ return null;
349
+ }
350
+ }
351
+ })();
352
+ if (!command) {
353
+ return;
354
+ }
355
+ keyEvent.preventDefault();
356
+ runCommand(command);
357
+ };
358
+ const lines = toMarkdownLines(value);
359
+ const activeLineIndex = toLineIndex(value, caretOffset);
360
+ const surfaceClassName = "col-start-1 row-start-1 m-0 min-h-32 whitespace-pre-wrap break-words border-0 p-3 font-mono text-md leading-relaxed cq-md:min-h-48";
361
+ return (_jsxs("div", { className: toClassName(
362
+ // `@container` here and not on a wrapper: a container query
363
+ // matches the container's *descendants*, never the
364
+ // container itself, so nothing in this string may be a
365
+ // `cq-*:` variant.
366
+ "@container flex flex-col gap-2 rounded-md border border-border-default bg-surface-base text-content-primary focus-within:outline-solid focus-within:outline-(length:--focus-ring-width) focus-within:outline-offset-(--focus-ring-offset) focus-within:outline-focus-ring",
367
+ // A token pair, never `opacity-*`. Fading the whole box
368
+ // fades the hint under it too, and axe measures the
369
+ // *composited* colour: `content-secondary` at 60% came out
370
+ // at 3.77:1 and failed AA on a `<p>` that is not itself
371
+ // disabled and owes no exemption.
372
+ isDisabled &&
373
+ "border-border-subtle bg-surface-sunken", className), children: [_jsx("div", { className: "border-border-subtle border-b px-2 pt-2 pb-1", children: _jsx(Toolbar, { items: [
374
+ {
375
+ icon: icons?.bold,
376
+ isDisabled: isDisabled || isReadOnly,
377
+ key: "bold",
378
+ label: "Bold",
379
+ onSelect: () => {
380
+ runCommand((state) => toggleInlineMarker(state, "**"));
381
+ },
382
+ },
383
+ {
384
+ icon: icons?.italic,
385
+ isDisabled: isDisabled || isReadOnly,
386
+ key: "italic",
387
+ label: "Italic",
388
+ onSelect: () => {
389
+ runCommand((state) => toggleInlineMarker(state, "_"));
390
+ },
391
+ },
392
+ {
393
+ icon: icons?.heading,
394
+ isDisabled: isDisabled || isReadOnly,
395
+ key: "heading",
396
+ label: "Heading",
397
+ onSelect: () => {
398
+ runCommand((state) => toggleHeading(state, 2));
399
+ },
400
+ },
401
+ {
402
+ icon: icons?.bulletedList,
403
+ isDisabled: isDisabled || isReadOnly,
404
+ key: "bulleted-list",
405
+ label: "Bulleted list",
406
+ onSelect: () => {
407
+ runCommand((state) => toggleLinePrefix(state, MARKDOWN_LINE_PREFIXES.bulletList));
408
+ },
409
+ },
410
+ {
411
+ icon: icons?.taskList,
412
+ isDisabled: isDisabled || isReadOnly,
413
+ key: "task-list",
414
+ label: "Task list",
415
+ onSelect: () => {
416
+ runCommand((state) => toggleLinePrefix(state, MARKDOWN_LINE_PREFIXES.taskList));
417
+ },
418
+ },
419
+ {
420
+ icon: icons?.numberedList,
421
+ isDisabled: isDisabled || isReadOnly,
422
+ key: "numbered-list",
423
+ label: "Numbered list",
424
+ onSelect: () => {
425
+ runCommand((state) => toggleLinePrefix(state, MARKDOWN_LINE_PREFIXES.orderedList));
426
+ },
427
+ },
428
+ {
429
+ icon: icons?.quote,
430
+ isDisabled: isDisabled || isReadOnly,
431
+ key: "quote",
432
+ label: "Quote",
433
+ onSelect: () => {
434
+ runCommand((state) => toggleLinePrefix(state, MARKDOWN_LINE_PREFIXES.blockquote));
435
+ },
436
+ },
437
+ {
438
+ icon: icons?.code,
439
+ isDisabled: isDisabled || isReadOnly,
440
+ key: "code",
441
+ label: "Code",
442
+ onSelect: () => {
443
+ runCommand((state) => toggleInlineMarker(state, "`"));
444
+ },
445
+ },
446
+ {
447
+ icon: icons?.link,
448
+ isDisabled: isDisabled || isReadOnly,
449
+ key: "link",
450
+ label: "Link",
451
+ onSelect: () => {
452
+ runCommand((state) => insertLink(state));
453
+ },
454
+ },
455
+ ], label: toolbarLabel, overflow: "menu" }) }), _jsxs("div", { className: "grid", children: [_jsx("div", { "aria-hidden": "true", className: toClassName(surfaceClassName, "pointer-events-none overflow-hidden"), ref: layerRef, children: lines.map((line, lineIndex) => (_jsxs(Fragment
456
+ // The index *is* the identity here: line 3 is line 3,
457
+ // and its content is what changes.
458
+ // biome-ignore lint/suspicious/noArrayIndexKey: a line's position is its identity
459
+ , { children: [lineIndex === 0 ? null : "\n", line.spans.map((span, spanIndex) => (_jsx("span", { className: span.kind === "marker"
460
+ ? isFocused &&
461
+ lineIndex === activeLineIndex
462
+ ? ACTIVE_MARKER_CLASS
463
+ : RESTING_MARKER_CLASS
464
+ : SPAN_KIND_CLASS[span.kind], children: span.text }, spanIndex)))] }, lineIndex))) }), _jsx("textarea", { ...mergeSlotProps(receivedSlotProps, {
465
+ "aria-describedby": hintId,
466
+ }), "aria-label": label, className: toClassName(surfaceClassName,
467
+ // The text is transparent and the layer behind supplies
468
+ // every glyph. The caret is not: `caret-color` is its
469
+ // own property precisely so a transparent-text overlay
470
+ // like this one can keep a visible cursor.
471
+ "resize-y bg-transparent text-transparent caret-content-primary outline-none placeholder:text-content-muted"), defaultValue: defaultValue, disabled: isDisabled, onBlur: () => {
472
+ setIsFocused(false);
473
+ }, onClick: syncFromElement, onDragOver: (dragEvent) => {
474
+ if (onUploadImage) {
475
+ // The default action for `dragover` is to reject the
476
+ // drop, so without this `onDrop` never fires at all —
477
+ // the same footgun `FileDropZone` documents.
478
+ dragEvent.preventDefault();
479
+ }
480
+ }, onDrop: handleDrop, onFocus: () => {
481
+ setIsFocused(true);
482
+ syncFromElement();
483
+ }, onInput: syncFromElement, onKeyDown: handleKeyDown, onKeyUp: syncFromElement, onPaste: handlePaste, onScroll: () => {
484
+ const element = textareaRef.current;
485
+ if (element && layerRef.current) {
486
+ layerRef.current.scrollTop = element.scrollTop;
487
+ layerRef.current.scrollLeft =
488
+ element.scrollLeft;
489
+ }
490
+ }, onSelect: syncFromElement, placeholder: placeholder, readOnly: isReadOnly, ref: textareaRef, spellCheck: true })] }), _jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1 px-3 pb-2", children: [_jsx("p", { className: "sr-only text-content-secondary text-sm cq-sm:not-sr-only", id: hintId, children: "Markdown. Ctrl+B bold, Ctrl+I italic, Ctrl+K link, Ctrl+E code." }), _jsx("span", { "aria-label": "Image upload", className: "text-content-secondary text-sm", id: statusId, role: "status", children: uploadMessage })] })] }));
491
+ };
492
+ //# sourceMappingURL=MarkdownEditor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownEditor.js","sourceRoot":"","sources":["../../src/MarkdownEditor/MarkdownEditor.tsx"],"names":[],"mappings":"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"}