@charcuterie/ui 3.3.0 → 3.4.1

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 (95) hide show
  1. package/README.md +8 -0
  2. package/dist/Field/Field.d.ts.map +1 -1
  3. package/dist/Field/Field.js +19 -1
  4. package/dist/Field/Field.js.map +1 -1
  5. package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -1
  6. package/dist/MarkdownEditor/MarkdownEditor.js +21 -8
  7. package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -1
  8. package/dist/MarkdownEditor/markdownCommands.d.ts +32 -0
  9. package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -1
  10. package/dist/MarkdownEditor/markdownCommands.js +47 -0
  11. package/dist/MarkdownEditor/markdownCommands.js.map +1 -1
  12. package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -1
  13. package/dist/MarkdownEditor/markdownSpans.js +30 -0
  14. package/dist/MarkdownEditor/markdownSpans.js.map +1 -1
  15. package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.d.ts +93 -0
  16. package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.d.ts.map +1 -0
  17. package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js +545 -0
  18. package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js.map +1 -0
  19. package/dist/markdownEditorCodeMirror/index.d.ts +16 -0
  20. package/dist/markdownEditorCodeMirror/index.d.ts.map +1 -0
  21. package/dist/markdownEditorCodeMirror/index.js +14 -0
  22. package/dist/markdownEditorCodeMirror/index.js.map +1 -0
  23. package/dist/markdownEditorCodeMirror/livePreview.d.ts +40 -0
  24. package/dist/markdownEditorCodeMirror/livePreview.d.ts.map +1 -0
  25. package/dist/markdownEditorCodeMirror/livePreview.js +428 -0
  26. package/dist/markdownEditorCodeMirror/livePreview.js.map +1 -0
  27. package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts +128 -0
  28. package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts.map +1 -0
  29. package/dist/markdownEditorCodeMirror/livePreviewRanges.js +345 -0
  30. package/dist/markdownEditorCodeMirror/livePreviewRanges.js.map +1 -0
  31. package/package.json +36 -1
  32. package/src/Accordion/Accordion.stories.tsx +1 -1
  33. package/src/AdaptiveGrid/AdaptiveGrid.stories.tsx +1 -1
  34. package/src/Alert/Alert.stories.tsx +1 -1
  35. package/src/Badge/Badge.stories.tsx +1 -1
  36. package/src/Board/Board.stories.tsx +1 -1
  37. package/src/Button/Button.stories.tsx +1 -1
  38. package/src/ButtonLink/ButtonLink.stories.tsx +1 -1
  39. package/src/Card/Card.stories.tsx +1 -1
  40. package/src/Checkbox/Checkbox.stories.tsx +1 -1
  41. package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.stories.tsx +1 -1
  42. package/src/ColorSchemeToggle/ColorSchemeToggle.stories.tsx +1 -1
  43. package/src/Combobox/Combobox.stories.tsx +1 -1
  44. package/src/DataTable/DataTable.stories.tsx +1 -1
  45. package/src/DatePicker/DatePicker.stories.tsx +1 -1
  46. package/src/Dialog/Dialog.stories.tsx +1 -1
  47. package/src/EmptyState/EmptyState.stories.tsx +1 -1
  48. package/src/Field/Field.stories.tsx +1 -1
  49. package/src/Field/Field.test.tsx +35 -0
  50. package/src/Field/Field.tsx +18 -0
  51. package/src/FileDropZone/FileDropZone.stories.tsx +1 -1
  52. package/src/Header/Header.stories.tsx +1 -1
  53. package/src/IconButton/IconButton.stories.tsx +1 -1
  54. package/src/Lightbox/Lightbox.stories.tsx +1 -1
  55. package/src/Listbox/Listbox.stories.tsx +1 -1
  56. package/src/LiveStatusIndicator/LiveStatusIndicator.stories.tsx +1 -1
  57. package/src/LogViewer/LogViewer.stories.tsx +1 -1
  58. package/src/Main/Main.stories.tsx +1 -1
  59. package/src/MarkdownEditor/MarkdownEditor.stories.tsx +1 -1
  60. package/src/MarkdownEditor/MarkdownEditor.tsx +33 -7
  61. package/src/MarkdownEditor/markdownCommands.test.ts +55 -0
  62. package/src/MarkdownEditor/markdownCommands.ts +60 -0
  63. package/src/MarkdownEditor/markdownSpans.test.ts +65 -0
  64. package/src/MarkdownEditor/markdownSpans.ts +35 -0
  65. package/src/MediaTile/MediaTile.stories.tsx +1 -1
  66. package/src/Menu/Menu.stories.tsx +1 -1
  67. package/src/Modal/Modal.stories.tsx +1 -1
  68. package/src/Picker/Picker.stories.tsx +1 -1
  69. package/src/Popover/Popover.stories.tsx +1 -1
  70. package/src/ProgressBar/ProgressBar.stories.tsx +1 -1
  71. package/src/QueryBuilder/QueryBuilder.stories.tsx +1 -1
  72. package/src/RadioGroup/RadioGroup.stories.tsx +1 -1
  73. package/src/Rail/Rail.stories.tsx +1 -1
  74. package/src/SegmentedControl/SegmentedControl.stories.tsx +1 -1
  75. package/src/Select/Select.stories.tsx +17 -6
  76. package/src/Shell/Shell.stories.tsx +1 -1
  77. package/src/Skeleton/Skeleton.stories.tsx +1 -1
  78. package/src/SortableTableHeader/SortableTableHeader.stories.tsx +1 -1
  79. package/src/Spinner/Spinner.stories.tsx +1 -1
  80. package/src/Swatch/Swatch.stories.tsx +1 -1
  81. package/src/Switch/Switch.stories.tsx +1 -1
  82. package/src/Tabs/Tabs.stories.tsx +1 -1
  83. package/src/TextLink/TextLink.stories.tsx +1 -1
  84. package/src/Toast/Toast.stories.tsx +1 -1
  85. package/src/Toolbar/Toolbar.stories.tsx +1 -1
  86. package/src/Tooltip/Tooltip.stories.tsx +1 -1
  87. package/src/VisuallyHidden/VisuallyHidden.stories.tsx +1 -1
  88. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +144 -0
  89. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.stories.tsx +147 -0
  90. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.tsx +866 -0
  91. package/src/markdownEditorCodeMirror/index.ts +26 -0
  92. package/src/markdownEditorCodeMirror/livePreview.ts +535 -0
  93. package/src/markdownEditorCodeMirror/livePreviewRanges.test.ts +431 -0
  94. package/src/markdownEditorCodeMirror/livePreviewRanges.ts +581 -0
  95. package/src/sourceRules.test.ts +20 -0
@@ -19,7 +19,7 @@ import { VisuallyHidden } from "./VisuallyHidden.tsx"
19
19
  * loses translation.
20
20
  */
21
21
  const meta = {
22
- title: "Foundation/VisuallyHidden",
22
+ title: "Utilities/VisuallyHidden",
23
23
  component: VisuallyHidden,
24
24
  parameters: { layout: "padded" },
25
25
  } satisfies Meta<typeof VisuallyHidden>
@@ -0,0 +1,144 @@
1
+ import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ import * as MarkdownEditorCodeMirrorStories from "./MarkdownEditorCodeMirror.stories";
4
+
5
+ <Meta of={MarkdownEditorCodeMirrorStories} />
6
+
7
+ # MarkdownEditorCodeMirror
8
+
9
+ Obsidian-style live preview: markers conceal themselves once the caret leaves, headings are
10
+ genuinely larger, images render in place, task boxes are real checkboxes — and the stored
11
+ value is still markdown, byte for byte.
12
+
13
+ It is **opt-in**, from a subpath:
14
+
15
+ ```tsx
16
+ import { MarkdownEditorCodeMirror } from "@charcuterie/ui/markdown-editor-codemirror"
17
+ ```
18
+
19
+ <Canvas of={MarkdownEditorCodeMirrorStories.Default} />
20
+
21
+ ## Props
22
+
23
+ <Controls />
24
+
25
+ ## Which of the two editors you want
26
+
27
+ `MarkdownEditor` is still the default, and most consumers should keep it.
28
+
29
+ | | `MarkdownEditor` | `MarkdownEditorCodeMirror` |
30
+ | --- | --- | --- |
31
+ | Surface | `<textarea>` + painted layer | CodeMirror 6 |
32
+ | New dependency | **none** | ~176 KB gz, optional peer |
33
+ | Markers | dimmed, never hidden | **concealed** until the caret arrives |
34
+ | Headings | weight and colour only | **actually larger** |
35
+ | Images | shown as `![alt](url)` | **rendered in place** |
36
+ | Tasks | shown as `- [ ]` | **checkbox you can click** |
37
+ | Bare URLs | painted as a URL | **clickable link** |
38
+ | Undo, IME, spellcheck | the platform's own | CodeMirror's |
39
+
40
+ Take the subpath when the description field is a **primary** surface of the app and the
41
+ bundle has room. Keep the default when it is one field on a form — a component library that
42
+ makes every consumer install an editor core to render a description has made the wrong
43
+ trade, and `castkit/packages/slatecast` has 60 KB gz of total budget to prove it.
44
+
45
+ ## Markdown is still the stored value
46
+
47
+ This is the part worth being exact about, because "WYSIWYG" usually means the opposite.
48
+
49
+ There is **no document model here and no serialiser**. CodeMirror's state holds the markdown
50
+ *text*. Live preview is a set of decorations *over* that text: concealing `**` hides two
51
+ characters that are still in the document, still in `onChange`, and still in whatever you
52
+ save. Replacing `![alt](url)` with an image does not rewrite the line — move the caret into
53
+ it and the markup is simply there again.
54
+
55
+ That is why this is not TipTap or Lexical. An editor core inverts the relationship: its
56
+ document model becomes authoritative and markdown becomes a lossy *export*, so any construct
57
+ the serialiser does not know about is silently dropped or silently becomes HTML. Docket
58
+ exists partly because HTML in a description field destroyed a previous tracker's data, and
59
+ an editor core would have been the easier build and the one thing that could not be allowed.
60
+
61
+ The [decision record][decision] has the measured bundle table and the full argument.
62
+
63
+ [decision]: https://github.com/Sawtaytoes/charcuterie/blob/master/docs/decisions/2026-08-19-the-markdown-editor-is-a-textarea-with-a-painted-layer.md
64
+
65
+ ## Concealment is the caret's answer, not the parser's
66
+
67
+ <Canvas of={MarkdownEditorCodeMirrorStories.CaretReveal} />
68
+
69
+ A construct reveals its markers when the selection **touches** it, and hides them again when
70
+ the selection leaves. Two consequences that are easy to mistake for bugs:
71
+
72
+ - **An unfocused editor reveals nothing.** There is no caret in a document nobody is editing,
73
+ so the resting offset of `0` deliberately does not count as "inside" the first construct.
74
+ Without this, a description that opens with a heading rendered its `#` on load and no other
75
+ heading's.
76
+ - **Reveal is per-construct, not per-line.** Two bold runs on one line reveal independently.
77
+
78
+ ## The "Markdown source" toggle
79
+
80
+ <Canvas of={MarkdownEditorCodeMirrorStories.RawMode} />
81
+
82
+ Nothing is concealed, nothing is replaced by a widget, no line is rescaled, and the surface
83
+ switches to monospace — because a source view is code. The syntax stays **coloured**: the
84
+ point of the toggle is to see the markup, not to lose the highlighting.
85
+
86
+ It is a `Switch` rather than a toolbar button because it is state, not a verb, and a
87
+ `ToolbarAction` renders a `Button` that announces nothing about being on.
88
+
89
+ ## What a mark does when it is hidden
90
+
91
+ Not every marker conceals, and the rule is narrower than "block marks stay":
92
+
93
+ > **A mark persists unless something else already says what it said.**
94
+
95
+ A blockquote gets an accent bar down its inline start, so its `>` is redundant and hides. A
96
+ fenced code block keeps its ` ``` ` and its language tag, because concealing them leaves a
97
+ blank line at the top and bottom of the block that reads as a rendering bug. A list bullet
98
+ stays, because hiding `-` turns an unordered list into indented prose — Obsidian substitutes
99
+ a `•`, which means choosing a width, and this component has no business choosing one.
100
+
101
+ ## Clicking a link opens it
102
+
103
+ Obsidian's behaviour, not VS Code's: where the markup is hidden, a link that looks like a
104
+ link should act like one. Bare URLs count — GFM autolinks them, so a URL pasted into a
105
+ description is clickable with nothing typed around it.
106
+
107
+ The trade is real and worth stating: **you cannot click into link text to edit it.** Arrow in
108
+ from either edge, or flip the Markdown source toggle, which is exactly what it is for.
109
+
110
+ ## Pasting
111
+
112
+ - **An image** (or a drop) runs `onUploadImage` and inserts `![alt](url)` at the caret, then
113
+ renders it in place. The component does not own storage — it hands you a `File`.
114
+ - **A URL over selected text** becomes `[selection](url)`, the way it does in every other
115
+ editor. Deliberately strict: one absolute `http(s)` URL and nothing else, because mangling
116
+ a literal paste is worse than not linkifying one.
117
+ - **Anything else** is left to the browser.
118
+
119
+ ## Accessibility, and the honest cost
120
+
121
+ The `<textarea>` sibling gets caret movement, IME composition, spellcheck, the platform undo
122
+ stack and `role="textbox"` from the browser, tested by the vendors. A `contenteditable`
123
+ re-earns every one of those, and CodeMirror is the reason that is acceptable rather than
124
+ reckless — it is the most heavily exercised implementation of that re-earning there is. It is
125
+ still a re-implementation, and that is what this subpath costs beyond its bytes.
126
+
127
+ `Tab` is **never** captured, at any time. Indenting a list with Tab is the convention in
128
+ editors that own the whole page, and inside a form it is a keyboard trap — WCAG 2.1.2 wants
129
+ focus to leave every component with the standard gesture. Indent and outdent are `Ctrl+]` and
130
+ `Ctrl+[`. `indentWithTab` is the extension that would change this, and it is deliberately not
131
+ installed.
132
+
133
+ <Canvas of={MarkdownEditorCodeMirrorStories.AllStates} />
134
+
135
+ ## It is uncontrolled
136
+
137
+ `defaultValue` in, `onChange` out — the same contract as `Select` and as the sibling editor.
138
+ An `EditorView` owns the caret, the selection, the scroll position and the undo history, and
139
+ rebuilding it throws all four away, so the view is built exactly **once**. Props that can
140
+ legitimately change afterwards reach it through a compartment (`isReadOnly`, `isDisabled`) or
141
+ through a ref (the callbacks); the rest are frozen at mount.
142
+
143
+ To swap documents — Docket opening a different task — remount with a `key`. That is one line,
144
+ and it is honest about the old undo history belonging to the old document.
@@ -0,0 +1,147 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+
3
+ import {
4
+ StoryCell,
5
+ StoryGrid,
6
+ } from "../board.storyHelpers.tsx"
7
+ import { Field } from "../Field/Field.tsx"
8
+ import { MarkdownEditorCodeMirror } from "./MarkdownEditorCodeMirror.tsx"
9
+
10
+ /**
11
+ * Invented, like every fixture in this repo — a task in a made-up
12
+ * homelab project, exercising each construct the live-preview layer
13
+ * knows about.
14
+ *
15
+ * The bare URL on the last line is the point of the whole exercise:
16
+ * nobody typed `[](…)` around it, and it is a link anyway.
17
+ */
18
+ const SAMPLE = `# Rack move, phase two
19
+
20
+ The **short** version: pull the _old_ switch, land the new one, and
21
+ keep the ~~three~~ two uplinks live throughout.
22
+
23
+ ## Before the window
24
+
25
+ - [x] Label every patch lead
26
+ - [ ] Photograph the current cabling
27
+ - [ ] Cut over at 02:00
28
+
29
+ 1. Power down the shelf
30
+ 2. Swap the rails
31
+ 3. Bring it back up
32
+
33
+ > Nothing here touches the storage pool.
34
+
35
+ Run \`ping -c 3 gateway.invalid\` before and after, and read
36
+ [the runbook](https://example.invalid/runbook) first.
37
+
38
+ | Port | Goes to |
39
+ | --- | --- |
40
+ | 1 | uplink |
41
+ | 2 | shelf |
42
+
43
+ \`\`\`sh
44
+ ip -br addr show
45
+ \`\`\`
46
+
47
+ Replacement rails ordered from https://example.invalid/product?id=1234
48
+ `
49
+
50
+ const meta = {
51
+ title: "Components/Controls/MarkdownEditorCodeMirror",
52
+ component: MarkdownEditorCodeMirror,
53
+ parameters: { layout: "padded" },
54
+ args: {
55
+ isDisabled: false,
56
+ isReadOnly: false,
57
+ label: "Description",
58
+ placeholder: "Write it in markdown…",
59
+ },
60
+ } satisfies Meta<typeof MarkdownEditorCodeMirror>
61
+
62
+ export default meta
63
+
64
+ type Story = StoryObj<typeof meta>
65
+
66
+ /**
67
+ * The whole argument for this subpath, in one frame: the heading is
68
+ * genuinely larger, `**` is nowhere on screen, the checkboxes are
69
+ * checkboxes, and the bare URL is a link.
70
+ */
71
+ export const Default: Story = {
72
+ args: {
73
+ defaultValue: SAMPLE,
74
+ label: "Description",
75
+ },
76
+ }
77
+
78
+ /**
79
+ * The "edit Markdown" toggle, flipped.
80
+ *
81
+ * Nothing is concealed and no line is rescaled — but the syntax is
82
+ * still coloured, because the point of the toggle is to see the
83
+ * markup, not to lose the highlighting.
84
+ */
85
+ export const RawMode: Story = {
86
+ args: {
87
+ defaultValue: SAMPLE,
88
+ isRawModeDefault: true,
89
+ label: "Description",
90
+ },
91
+ }
92
+
93
+ export const AllStates: Story = {
94
+ args: { label: "Description" },
95
+ render: () => (
96
+ <StoryGrid columns={2}>
97
+ <StoryCell align="stretch" label="empty">
98
+ <MarkdownEditorCodeMirror
99
+ label="Empty description"
100
+ placeholder="Write it in markdown…"
101
+ />
102
+ </StoryCell>
103
+
104
+ <StoryCell align="stretch" label="read only">
105
+ <MarkdownEditorCodeMirror
106
+ defaultValue={
107
+ "A **done** description.\n\n- [x] shipped"
108
+ }
109
+ isReadOnly
110
+ label="Read-only description"
111
+ />
112
+ </StoryCell>
113
+
114
+ <StoryCell align="stretch" label="disabled">
115
+ <MarkdownEditorCodeMirror
116
+ defaultValue={"You cannot edit _this_ one."}
117
+ isDisabled
118
+ label="Disabled description"
119
+ />
120
+ </StoryCell>
121
+
122
+ <StoryCell align="stretch" label="in a Field">
123
+ <Field
124
+ description="Markdown. Paste a screenshot straight in."
125
+ label="Description"
126
+ >
127
+ <MarkdownEditorCodeMirror defaultValue="Wired through `SlotProps`." />
128
+ </Field>
129
+ </StoryCell>
130
+ </StoryGrid>
131
+ ),
132
+ }
133
+
134
+ /**
135
+ * Concealment is per-construct, and the caret is what reveals it.
136
+ *
137
+ * This story exists because the behaviour is invisible in a static
138
+ * frame: click into the bold run and the `**` comes back, in the
139
+ * flow, without the line reflowing around it.
140
+ */
141
+ export const CaretReveal: Story = {
142
+ args: {
143
+ defaultValue:
144
+ "Click into the **bold run** and its markers come back.\n\nLeave, and they go again.",
145
+ label: "Description",
146
+ },
147
+ }