@charcuterie/ui 3.13.0 → 3.14.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 (32) hide show
  1. package/dist/markdownEditorCodeMirror/MarkdownView.d.ts +140 -0
  2. package/dist/markdownEditorCodeMirror/MarkdownView.d.ts.map +1 -0
  3. package/dist/markdownEditorCodeMirror/MarkdownView.js +268 -0
  4. package/dist/markdownEditorCodeMirror/MarkdownView.js.map +1 -0
  5. package/dist/markdownEditorCodeMirror/index.d.ts +14 -3
  6. package/dist/markdownEditorCodeMirror/index.d.ts.map +1 -1
  7. package/dist/markdownEditorCodeMirror/index.js +12 -3
  8. package/dist/markdownEditorCodeMirror/index.js.map +1 -1
  9. package/dist/markdownEditorCodeMirror/livePreview.d.ts +42 -2
  10. package/dist/markdownEditorCodeMirror/livePreview.d.ts.map +1 -1
  11. package/dist/markdownEditorCodeMirror/livePreview.js +298 -28
  12. package/dist/markdownEditorCodeMirror/livePreview.js.map +1 -1
  13. package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts.map +1 -1
  14. package/dist/markdownEditorCodeMirror/livePreviewRanges.js +141 -10
  15. package/dist/markdownEditorCodeMirror/livePreviewRanges.js.map +1 -1
  16. package/dist/markdownEditorCodeMirror/safeUrls.d.ts +46 -0
  17. package/dist/markdownEditorCodeMirror/safeUrls.d.ts.map +1 -0
  18. package/dist/markdownEditorCodeMirror/safeUrls.js +123 -0
  19. package/dist/markdownEditorCodeMirror/safeUrls.js.map +1 -0
  20. package/package.json +1 -1
  21. package/src/MarkdownEditor/MarkdownEditor.mdx +5 -0
  22. package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +11 -0
  23. package/src/markdownEditorCodeMirror/MarkdownView.mdx +173 -0
  24. package/src/markdownEditorCodeMirror/MarkdownView.stories.tsx +362 -0
  25. package/src/markdownEditorCodeMirror/MarkdownView.test.tsx +349 -0
  26. package/src/markdownEditorCodeMirror/MarkdownView.tsx +373 -0
  27. package/src/markdownEditorCodeMirror/index.ts +17 -2
  28. package/src/markdownEditorCodeMirror/livePreview.ts +391 -35
  29. package/src/markdownEditorCodeMirror/livePreviewRanges.test.ts +233 -0
  30. package/src/markdownEditorCodeMirror/livePreviewRanges.ts +192 -10
  31. package/src/markdownEditorCodeMirror/safeUrls.test.ts +0 -0
  32. package/src/markdownEditorCodeMirror/safeUrls.ts +142 -0
@@ -0,0 +1,173 @@
1
+ import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ import * as MarkdownViewStories from "./MarkdownView.stories";
4
+
5
+ <Meta of={MarkdownViewStories} />
6
+
7
+ # MarkdownView
8
+
9
+ Rendered markdown, with **no toolbar in the DOM** and nothing to type into. The same
10
+ document, drawn the same way, as the editor beside it.
11
+
12
+ It is **opt-in**, from the same subpath as the editor:
13
+
14
+ ```tsx
15
+ import { MarkdownView } from "@charcuterie/ui/markdown-editor-codemirror"
16
+ ```
17
+
18
+ <Canvas of={MarkdownViewStories.Default} />
19
+
20
+ ## Props
21
+
22
+ <Controls />
23
+
24
+ ## Which of the three you want
25
+
26
+ | | `MarkdownEditor` | `MarkdownEditorCodeMirror` | `MarkdownView` |
27
+ | --- | --- | --- | --- |
28
+ | For | typing markdown | typing markdown, rendered | **reading** markdown |
29
+ | Surface | `<textarea>` + painted layer | CodeMirror 6 | CodeMirror 6 |
30
+ | New dependency | none | ~176 KB gz, optional peer | the same one |
31
+ | Toolbar | nine buttons | nine buttons + a switch | **none, ever** |
32
+ | Value | `defaultValue` | `defaultValue` | **`value`, controlled** |
33
+ | Content role | `textbox` | `textbox` | **`article`** |
34
+
35
+ `isReadOnly` on either editor is **not** this. It means *an editor you cannot type in*: the
36
+ nine formatting buttons are still there, still in the accessibility tree, drawn in their
37
+ disabled colours above a document nobody is editing. That is right for a form field that is
38
+ temporarily locked, and wrong for reading a file — which is what this exists for.
39
+
40
+ ## No toolbar means no toolbar
41
+
42
+ `queryAllByRole("toolbar")` returns nothing here, and so does `queryAllByRole("button")`.
43
+ That is asserted rather than described, because the workaround it replaces was a CSS rule
44
+ in a consuming app keyed on `role="toolbar"`, and CSS is a poor place to keep a promise:
45
+ `visibility: hidden` leaves the buttons in the accessibility tree, `opacity: 0` leaves them
46
+ in the tab order, and even `display: none` leaves the *overflow menu button* the toolbar
47
+ collapses into at narrow widths.
48
+
49
+ ## It renders what the editor renders
50
+
51
+ <Canvas of={MarkdownViewStories.SameAsTheEditor} />
52
+
53
+ One string, two surfaces, side by side. They agree because they **are the same code** —
54
+ `livePreviewRanges.ts` decides what every construct looks like and both call it, over the
55
+ same GFM parse, under the same theme.
56
+
57
+ That is the whole reason this lives in the CodeMirror subpath instead of being a
58
+ `react-markdown` reader in the main barrel. A second renderer agrees on the day it is
59
+ written and drifts afterwards, one construct at a time, and the bug arrives six months later
60
+ as *"the table changed when I saved it"*. There is no second renderer here to drift.
61
+
62
+ The cost is honest and worth stating: a consumer who wants **only** the reader still installs
63
+ CodeMirror. For an app that renders markdown in three places and edits it in one, that is the
64
+ right way round — the dependency is paid once.
65
+
66
+ ## It is controlled, and the editors are not
67
+
68
+ `value` in, and that is it. Fetch a file at a different commit hash, pass the new string, and
69
+ the page changes — no `key`, no remount, no stale document sitting above a new title.
70
+
71
+ The editors take `defaultValue` because reassigning an `EditorView`'s document from outside
72
+ throws away the undo history that belonged to it, which is a real cost and is why they ask
73
+ for a `key` instead. A view has no undo history and no caret to protect, so it has nothing to
74
+ lose by simply rendering what it is given.
75
+
76
+ ## A `- [ ]` box is inert unless you say where the tick goes
77
+
78
+ <Canvas of={MarkdownViewStories.AllStates} />
79
+
80
+ Pass `onToggleTask` and the checkboxes are operable; leave it out and they render
81
+ **disabled**. The state is still on screen either way — that is a fact about the document
82
+ and belongs there — but nothing pretends the reader can change it.
83
+
84
+ This is the same shape as `onUploadImage` on the editors: omit the handler and the capability
85
+ is simply not offered, which beats offering one that half works. Two of the three surfaces
86
+ this component was built for — a rendered comment, and a repo file fetched at a commit hash —
87
+ have nowhere to put a tick, and a box that visibly toggles and then loses the change on
88
+ reload is worse than one that says up front it cannot.
89
+
90
+ `onToggleTask` hands back the **whole next markdown document**, exactly like the editors'
91
+ `onChange`, so one autosave wires to both. The view applies the tick to its own document
92
+ immediately so the box responds under the pointer; if the consumer rejects the change, the
93
+ next `value` puts it back.
94
+
95
+ ## Link text that carries its own markup
96
+
97
+ <Canvas of={MarkdownViewStories.NestedLinkText} />
98
+
99
+ A link whose text is a code span, a bolded phrase or an emphasised one is ordinary in a
100
+ docs file, and until 2026-08-24 one of them took the whole surface down. The closing `]`
101
+ was read as *the sibling after the opening bracket*, which is true only while the text is
102
+ plain words — put an `InlineCode` in it and that sibling is the code span, so
103
+ ``[`a`](url)`` produced a link-text mark with `from === to`. CodeMirror refuses an empty
104
+ mark decoration and throws out of the whole `ViewPlugin`, so **every** decoration in the
105
+ document is lost and the reader gets raw markdown.
106
+
107
+ The failure looks nothing like a link bug, which is why the story is here: one frame where
108
+ every link is a link, and a heading above them that has nothing to do with links and
109
+ stopped rendering anyway.
110
+
111
+ ## Hostile markdown
112
+
113
+ This will render files fetched from a git host, so the document is untrusted by default.
114
+
115
+ <Canvas of={MarkdownViewStories.HostileMarkdown} />
116
+
117
+ **Raw HTML in the source is never parsed as HTML.** There is no `innerHTML` in this subpath,
118
+ no `dangerouslySetInnerHTML`, and no sanitiser to have a bug in. CodeMirror's state holds the
119
+ source as *text* and every rendered node is `createElement` plus `textContent`, so a
120
+ `<script>` in a fetched README is drawn as the characters `<script>`. It is not stripped,
121
+ not escaped and not "sanitised" — there is simply no code path that would turn it into an
122
+ element.
123
+
124
+ **A URL is checked before it is used as one.** That was the one place the document reached the
125
+ platform, and it was live in the editor too: `[click](javascript:…)` concealed its markup,
126
+ painted as a link, and `window.open` ran it in this origin. Both components now go through an
127
+ allowlist that normalises the way a browser does, so `JaVaScript:` and a tab in the middle of
128
+ the word are the same URL to it. A rejected link is **left as source** — brackets, parentheses
129
+ and all — rather than repainted as a link-coloured word that silently refuses to work.
130
+ Images take `http:`, `https:` and `data:image/*` only; `data:text/html` is not an image.
131
+
132
+ **The reader is not a beacon.** Remote images carry `referrerpolicy="no-referrer"`, so an
133
+ image in somebody else's file cannot learn which page loaded it.
134
+
135
+ What is *not* claimed: nothing here stops a document linking somewhere unpleasant, and the
136
+ images the markdown names are still fetched — that is what rendering them means.
137
+
138
+ ## Accessibility
139
+
140
+ The content is `role="article"`, not the `role="textbox"` CodeMirror writes by default, and
141
+ that swap is the difference between reading a document and being trapped in a text field: a
142
+ screen reader entering a textbox switches to forms mode and reads the contents as one flat
143
+ string, with no heading navigation and no table navigation.
144
+
145
+ So the structure has to be real, and two pieces of it are:
146
+
147
+ - **Headings carry `role="heading"` and an `aria-level`**, so the H key steps between
148
+ sections. The editor deliberately has none of this — ARIA flattens a textbox's contents, so
149
+ a heading role inside one would be a decoration that lies about what it does.
150
+ - **Links are real `<a href>` elements**, in the tab order, activated by Enter, with an
151
+ address the browser will show and copy. The editor cannot have these: an anchor inside a
152
+ `contenteditable` competes with the caret for its line, which is why the editor paints a
153
+ span and translates a `mousedown` instead — and why clicking link text *there* cannot place
154
+ a caret.
155
+
156
+ <Canvas of={MarkdownViewStories.Interactive} />
157
+
158
+ **The document itself is not a tab stop.** There is nothing in it to operate, and a reading
159
+ surface that swallowed a Tab would be one more thing between the reader and the next control.
160
+ Every link inside it is reachable, in document order.
161
+
162
+ List structure is **visual only**, and worth knowing rather than discovering: the `-` bullet
163
+ is drawn as a dimmed marker rather than concealed, the same way it is in the editor, so a
164
+ list reads as a list on screen but is not a `<ul>` in the tree. Tables are real `<table>`
165
+ elements with real `<th>`s, because the table widget builds one.
166
+
167
+ ## The Narrow View
168
+
169
+ <Canvas of={MarkdownViewStories.Responsive} />
170
+
171
+ A table too wide for its column scrolls **inside its own block** rather than pushing the
172
+ column sideways, and an image is capped at the width it is given. Nothing here is keyed to a
173
+ viewport width; a column in a three-up board is narrow on a 4K display.
@@ -0,0 +1,362 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+ import { useState } from "react"
3
+
4
+ import {
5
+ ContainerBoard,
6
+ StoryCell,
7
+ StoryGrid,
8
+ } from "../board.storyHelpers.tsx"
9
+ import { Card } from "../Card/Card.tsx"
10
+ import { MarkdownEditorCodeMirror } from "./MarkdownEditorCodeMirror.tsx"
11
+ import { MarkdownView } from "./MarkdownView.tsx"
12
+
13
+ /**
14
+ * Invented, like every fixture in this repo — a task in a made-up
15
+ * homelab project, and deliberately the **same string** the editor's
16
+ * stories use.
17
+ *
18
+ * That is what makes the `SameAsTheEditor` story an argument rather
19
+ * than an illustration: the two surfaces are handed one document and
20
+ * a reader compares the frames.
21
+ */
22
+ const SAMPLE = `# Rack move, phase two
23
+
24
+ The **short** version: pull the _old_ switch, land the new one, and
25
+ keep the ~~three~~ two uplinks live throughout.
26
+
27
+ ## Before the window
28
+
29
+ - [x] Label every patch lead
30
+ - [ ] Photograph the current cabling
31
+ - [ ] Cut over at 02:00
32
+
33
+ 1. Power down the shelf
34
+ 2. Swap the rails
35
+ 3. Bring it back up
36
+
37
+ > Nothing here touches the storage pool.
38
+
39
+ Run \`ping -c 3 gateway.invalid\` before and after, and read
40
+ [the runbook](https://example.invalid/runbook) first.
41
+
42
+ | Port | Goes to |
43
+ | --- | --- |
44
+ | 1 | uplink |
45
+ | 2 | shelf |
46
+
47
+ \`\`\`sh
48
+ ip -br addr show
49
+ \`\`\`
50
+
51
+ Replacement rails ordered from https://example.invalid/product?id=1234
52
+ `
53
+
54
+ /**
55
+ * Link text with inline markup in it — the shape that used to take
56
+ * the **whole document** down to raw source.
57
+ *
58
+ * The closing bracket was read as the node right after `[`, which
59
+ * is the bracket only while the link text is plain prose. A code
60
+ * span or a `**bold**` run becomes that node instead, the link text
61
+ * measures zero characters, `Decoration.mark` refuses an empty
62
+ * range, and the throw destroys the view plugin — so every
63
+ * decoration in the file disappears at once, not just this link's.
64
+ *
65
+ * Invented, like every fixture here, but the shape is not invented:
66
+ * agent-written task descriptions are full of ``[`file.md`](path)``.
67
+ */
68
+ const NESTED_LINK_TEXT = `## Where the fix landed
69
+
70
+ The range walk lives in [\`livePreviewRanges.ts\`](https://example.invalid/ranges)
71
+ and the decorations in [\`livePreview.ts\`](https://example.invalid/preview).
72
+
73
+ - A code span: [\`AGENTS.md\`](https://example.invalid/agents)
74
+ - Strong: [**the runbook**](https://example.invalid/runbook)
75
+ - Emphasis: [*the older note*](https://example.invalid/note)
76
+ - Struck through: [~~the retired page~~](https://example.invalid/retired)
77
+ - Mixed: [read \`this\` **first**](https://example.invalid/first)
78
+
79
+ Plain links still work: [the index](https://example.invalid/index).
80
+ `
81
+
82
+ /**
83
+ * A drawing rather than a fetched file, so the story needs no
84
+ * network and the frame is the same on every machine. The same
85
+ * technique `Avatar`'s portrait uses, and one of the few things
86
+ * `data:` is genuinely good for.
87
+ */
88
+ const DIAGRAM_URL = `data:image/svg+xml;utf8,${encodeURIComponent(
89
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 60"><rect width="240" height="60" rx="6" fill="#2F6F4E"/><rect x="12" y="14" width="96" height="32" rx="4" fill="#F2E8CF"/><rect x="132" y="14" width="96" height="32" rx="4" fill="#F2E8CF"/><path d="M108 30h24" stroke="#F2E8CF" stroke-width="4"/></svg>',
90
+ )}`
91
+
92
+ const WITH_IMAGE = `## Rack inventory
93
+
94
+ ![Two shelves and the uplink between them](${DIAGRAM_URL})
95
+
96
+ | Port | Goes to | **Speed** |
97
+ | :--- | :---: | ---: |
98
+ | 1 | uplink | 10G |
99
+ | 2 | shelf | 1G |
100
+ `
101
+
102
+ /**
103
+ * The document nobody in this house wrote: what a fetched file looks
104
+ * like when its author would rather it did something else.
105
+ *
106
+ * Every line here is a thing that used to work. The `javascript:`
107
+ * link opened and ran; the `<script>` was never a script but is
108
+ * worth showing anyway; the `data:text/html` image was an `src` this
109
+ * component wrote verbatim.
110
+ */
111
+ const HOSTILE = `# A file fetched from somewhere else
112
+
113
+ [Looks like a link](javascript:alert(document.domain)) — and is not one.
114
+
115
+ <script>alert("this is four coloured characters, not a script")</script>
116
+
117
+ <img src=x onerror="alert(1)">
118
+
119
+ ![Not an image](data:text/html,<script>alert(1)</script>)
120
+
121
+ [This one is fine](https://example.invalid/runbook), and so is ./RELEASING.md.
122
+ `
123
+
124
+ const CHECKLIST = `## Before the window
125
+
126
+ - [x] Label every patch lead
127
+ - [ ] Photograph the current cabling
128
+ - [ ] Cut over at 02:00
129
+ `
130
+
131
+ /**
132
+ * A second list rather than the same one twice, and the difference
133
+ * is load bearing: two checkboxes with the same accessible name on
134
+ * one page are two things `getByRole("checkbox", { name })` cannot
135
+ * tell apart — which is the drivability property this library gates
136
+ * on, failing in a *story* rather than in a component.
137
+ */
138
+ const TICKABLE_CHECKLIST = `## After the window
139
+
140
+ - [x] Bring the shelf back up
141
+ - [ ] Re-check both uplinks
142
+ - [ ] Close the change record
143
+ `
144
+
145
+ /**
146
+ * The state the checkbox story cannot show without one: a tick that
147
+ * goes somewhere.
148
+ *
149
+ * `onToggleTask` hands back the whole next markdown document, which
150
+ * is the same shape the editors' `onChange` uses — so a consumer
151
+ * wires one autosave and points both at it.
152
+ */
153
+ const TickableChecklist = () => {
154
+ const [value, setValue] = useState(TICKABLE_CHECKLIST)
155
+
156
+ return (
157
+ <MarkdownView
158
+ label="Tickable checklist"
159
+ onToggleTask={setValue}
160
+ value={value}
161
+ />
162
+ )
163
+ }
164
+
165
+ const meta = {
166
+ title: "Components/Data/MarkdownView",
167
+ component: MarkdownView,
168
+ parameters: { layout: "padded" },
169
+ args: {
170
+ label: "Description",
171
+ onToggleTask: undefined,
172
+ value: SAMPLE,
173
+ },
174
+ } satisfies Meta<typeof MarkdownView>
175
+
176
+ export default meta
177
+
178
+ type Story = StoryObj<typeof meta>
179
+
180
+ /**
181
+ * The whole point in one frame: the document is drawn, and there is
182
+ * **no toolbar above it**. Not a disabled one, not a hidden one —
183
+ * `getAllByRole("toolbar")` returns nothing, because nothing in this
184
+ * component can render one.
185
+ */
186
+ export const Default: Story = {}
187
+
188
+ /**
189
+ * The same string, in both surfaces, side by side.
190
+ *
191
+ * This is the requirement the component exists to meet, and it is
192
+ * the one a screenshot can actually check: the heading is the same
193
+ * size, the table has the same borders and the same alignment, the
194
+ * checkboxes are in the same places, and the bare URL is a link in
195
+ * both. They agree because they are the same code — one parser, one
196
+ * set of range rules, one theme — rather than because somebody kept
197
+ * two renderers in step.
198
+ */
199
+ export const SameAsTheEditor: Story = {
200
+ render: (viewProps) => (
201
+ <StoryGrid columns={2}>
202
+ <StoryCell align="stretch" label="MarkdownView">
203
+ <MarkdownView {...viewProps} />
204
+ </StoryCell>
205
+
206
+ <StoryCell
207
+ align="stretch"
208
+ label="MarkdownEditorCodeMirror"
209
+ >
210
+ <MarkdownEditorCodeMirror
211
+ defaultValue={SAMPLE}
212
+ label="Description"
213
+ />
214
+ </StoryCell>
215
+ </StoryGrid>
216
+ ),
217
+ }
218
+
219
+ /**
220
+ * Everything the reader can be shown, including the two states that
221
+ * are easy to get wrong: an empty document, and a checkbox nobody
222
+ * may tick.
223
+ */
224
+ export const AllStates: Story = {
225
+ render: (viewProps) => (
226
+ <StoryGrid columns={2}>
227
+ <StoryCell
228
+ align="stretch"
229
+ label="checkboxes are inert without onToggleTask"
230
+ >
231
+ <MarkdownView
232
+ {...viewProps}
233
+ label="Read-only checklist"
234
+ value={CHECKLIST}
235
+ />
236
+ </StoryCell>
237
+
238
+ <StoryCell
239
+ align="stretch"
240
+ label="…and operable with one"
241
+ >
242
+ <TickableChecklist />
243
+ </StoryCell>
244
+
245
+ <StoryCell
246
+ align="stretch"
247
+ label="an image and a table"
248
+ >
249
+ <MarkdownView
250
+ {...viewProps}
251
+ label="Rack inventory"
252
+ value={WITH_IMAGE}
253
+ />
254
+ </StoryCell>
255
+
256
+ <StoryCell align="stretch" label="an empty document">
257
+ <MarkdownView
258
+ {...viewProps}
259
+ label="Nothing written yet"
260
+ value=""
261
+ />
262
+ </StoryCell>
263
+ </StoryGrid>
264
+ ),
265
+ }
266
+
267
+ /**
268
+ * A file from a git host, rendering as the characters it contains.
269
+ *
270
+ * The `javascript:` link keeps its brackets and its parentheses on
271
+ * purpose. A link-coloured word that silently refuses to work tells
272
+ * the reader nothing; the source tells them exactly what the file
273
+ * tried to do.
274
+ */
275
+ export const HostileMarkdown: Story = {
276
+ args: {
277
+ label: "README.md at 9f3c1ab",
278
+ value: HOSTILE,
279
+ },
280
+ }
281
+
282
+ /**
283
+ * The three container widths. A table that will not fit scrolls
284
+ * inside its own block rather than pushing the column sideways, and
285
+ * an image is capped at the width it is given — which is what keeps
286
+ * the Narrow View readable instead of merely small.
287
+ */
288
+ export const Responsive: Story = {
289
+ render: (viewProps) => (
290
+ <ContainerBoard>
291
+ {(width) => (
292
+ <Card heading={`Rack inventory (${width})`}>
293
+ <MarkdownView
294
+ {...viewProps}
295
+ label={`Rack inventory at ${width}`}
296
+ value={WITH_IMAGE}
297
+ />
298
+ </Card>
299
+ )}
300
+ </ContainerBoard>
301
+ ),
302
+ }
303
+
304
+ /**
305
+ * The complete keyboard path, which for a reader is short and is the
306
+ * point.
307
+ *
308
+ * The document itself is **not** a tab stop — there is nothing in it
309
+ * to operate, and a reading surface that swallowed a Tab would be
310
+ * one more thing between the reader and the next control. What *is*
311
+ * reachable is every link in it, in document order, because they are
312
+ * real anchors rather than painted spans: Tab to one, Enter to open
313
+ * it, and the context menu will copy its address.
314
+ *
315
+ * The buttons on either side are there so the path is visible — Tab
316
+ * from the first lands on each link in turn and ends on the second.
317
+ */
318
+ export const Interactive: Story = {
319
+ args: {
320
+ label: "Rack move, phase two",
321
+ value: SAMPLE,
322
+ },
323
+ render: (viewProps) => (
324
+ <div className="flex flex-col items-start gap-3">
325
+ <button
326
+ className="rounded-md border border-border-default px-3 py-1 text-content-primary text-sm"
327
+ type="button"
328
+ >
329
+ {"Before"}
330
+ </button>
331
+
332
+ <MarkdownView {...viewProps} />
333
+
334
+ <button
335
+ className="rounded-md border border-border-default px-3 py-1 text-content-primary text-sm"
336
+ type="button"
337
+ >
338
+ {"After"}
339
+ </button>
340
+ </div>
341
+ ),
342
+ }
343
+
344
+ /**
345
+ * The regression frame.
346
+ *
347
+ * Every line here is a link whose text carries inline markup, and
348
+ * the assertion a reader can make from the picture is the one that
349
+ * matters: the document is **drawn**. Before the fix this frame was
350
+ * the markdown source, brackets and parentheses and all, because
351
+ * one empty mark decoration killed the plugin for the whole file.
352
+ *
353
+ * The last line is the control. A plain `[text](url)` never had the
354
+ * bug, so if it renders and the ones above it do not, the failure
355
+ * is this and not something else.
356
+ */
357
+ export const NestedLinkText: Story = {
358
+ args: {
359
+ label: "Where the fix landed",
360
+ value: NESTED_LINK_TEXT,
361
+ },
362
+ }