@charcuterie/ui 3.19.2 → 3.21.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.
- package/dist/Board/BoardCard.d.ts +95 -22
- package/dist/Board/BoardCard.d.ts.map +1 -1
- package/dist/Board/BoardCard.js +18 -2
- package/dist/Board/BoardCard.js.map +1 -1
- package/dist/MarkdownEditor/markdownSpans.d.ts +17 -4
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -1
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -1
- package/dist/MarkdownLine/MarkdownLine.d.ts +104 -0
- package/dist/MarkdownLine/MarkdownLine.d.ts.map +1 -0
- package/dist/MarkdownLine/MarkdownLine.js +187 -0
- package/dist/MarkdownLine/MarkdownLine.js.map +1 -0
- package/dist/MarkdownLine/inlineMarkdown.d.ts +82 -0
- package/dist/MarkdownLine/inlineMarkdown.d.ts.map +1 -0
- package/dist/MarkdownLine/inlineMarkdown.js +509 -0
- package/dist/MarkdownLine/inlineMarkdown.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/markdownEditorCodeMirror/index.d.ts +1 -1
- package/dist/markdownEditorCodeMirror/index.d.ts.map +1 -1
- package/dist/markdownEditorCodeMirror/index.js +1 -1
- package/dist/markdownEditorCodeMirror/index.js.map +1 -1
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js +1 -1
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js.map +1 -1
- package/dist/safeUrls.d.ts.map +1 -0
- package/dist/safeUrls.js.map +1 -0
- package/package.json +1 -1
- package/src/Board/Board.stories.tsx +167 -0
- package/src/Board/Board.test.tsx +97 -0
- package/src/Board/BoardCard.tsx +135 -27
- package/src/MarkdownEditor/markdownSpans.ts +18 -5
- package/src/MarkdownLine/MarkdownLine.mdx +135 -0
- package/src/MarkdownLine/MarkdownLine.stories.tsx +290 -0
- package/src/MarkdownLine/MarkdownLine.test.tsx +241 -0
- package/src/MarkdownLine/MarkdownLine.tsx +331 -0
- package/src/MarkdownLine/inlineMarkdown.test.ts +253 -0
- package/src/MarkdownLine/inlineMarkdown.ts +768 -0
- package/src/index.ts +9 -2
- package/src/markdownEditorCodeMirror/index.ts +4 -4
- package/src/markdownEditorCodeMirror/livePreviewRanges.ts +1 -1
- package/src/sourceRules.test.ts +17 -1
- package/dist/markdownEditorCodeMirror/safeUrls.d.ts.map +0 -1
- package/dist/markdownEditorCodeMirror/safeUrls.js.map +0 -1
- /package/dist/{markdownEditorCodeMirror/safeUrls.d.ts → safeUrls.d.ts} +0 -0
- /package/dist/{markdownEditorCodeMirror/safeUrls.js → safeUrls.js} +0 -0
- /package/src/{markdownEditorCodeMirror/safeUrls.test.ts → safeUrls.test.ts} +0 -0
- /package/src/{markdownEditorCodeMirror/safeUrls.ts → safeUrls.ts} +0 -0
package/src/Board/BoardCard.tsx
CHANGED
|
@@ -3,6 +3,11 @@ import type { ReactNode } from "react"
|
|
|
3
3
|
import { useState } from "react"
|
|
4
4
|
|
|
5
5
|
import { Button } from "../Button/Button.tsx"
|
|
6
|
+
import type { CardAccentEdge } from "../Card/cardAccentEdge.ts"
|
|
7
|
+
import {
|
|
8
|
+
getAccentEdgeClassName,
|
|
9
|
+
getAccentEdgeStyle,
|
|
10
|
+
} from "../Card/cardAccentEdge.ts"
|
|
6
11
|
import {
|
|
7
12
|
FOCUS_RING_CLASS,
|
|
8
13
|
INTENT_SOLID_FILL_CLASS,
|
|
@@ -12,32 +17,14 @@ import { TextLink } from "../TextLink/TextLink.tsx"
|
|
|
12
17
|
import { toClassName } from "../toClassName.ts"
|
|
13
18
|
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.tsx"
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The leading colour bar. Priority, in the consumer this was built
|
|
18
|
-
* for — *"Priority is styled as a leading colour bar, and **never**
|
|
19
|
-
* a coloured word before the title."*
|
|
20
|
-
*
|
|
21
|
-
* Pair it with `accentLabel`. A bar is colour and nothing else, so
|
|
22
|
-
* on its own it fails WCAG 1.4.1 outright and is invisible to
|
|
23
|
-
* every screen reader; the label rides along in a
|
|
24
|
-
* `VisuallyHidden` so the same fact reaches both channels. Docket's
|
|
25
|
-
* first board painted these `--color-danger-9` — a Radix-style
|
|
26
|
-
* scale this token set has never had — so every bar was
|
|
27
|
-
* transparent while every "is it rendered" assertion passed. The
|
|
28
|
-
* families here are `--color-intent-<intent>-<role>`; there is no
|
|
29
|
-
* numeric scale to reach for.
|
|
30
|
-
*/
|
|
31
|
-
accentIntent?: IntentName
|
|
32
|
-
/** What the bar means, in words. "Priority 0". */
|
|
20
|
+
type BoardItemFields = {
|
|
21
|
+
/** What the bar means, in words. "Priority 0", "Movies and Shows". */
|
|
33
22
|
accentLabel?: string
|
|
34
23
|
/**
|
|
35
24
|
* Under the title and metadata, spanning the card: a live run
|
|
36
25
|
* line, a gate explanation, a due date. A `ProgressBar` goes here.
|
|
37
26
|
*/
|
|
38
27
|
footer?: ReactNode
|
|
39
|
-
/** Where the title navigates. Routed through `RouterLinkProvider`. */
|
|
40
|
-
href?: string
|
|
41
28
|
/**
|
|
42
29
|
* Visibly present but not actionable — Docket's *gated*: a task
|
|
43
30
|
* whose phase has not opened yet. Paint only; it does not disable
|
|
@@ -57,13 +44,105 @@ export type BoardItem = {
|
|
|
57
44
|
* beside it when there is room. `Badge` is what goes here.
|
|
58
45
|
*/
|
|
59
46
|
meta?: ReactNode
|
|
60
|
-
/**
|
|
61
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The card's name, in **words**.
|
|
49
|
+
*
|
|
50
|
+
* Always a plain string, even when `titleContent` draws something
|
|
51
|
+
* richer, because this is what the move handle and the drag
|
|
52
|
+
* announcements are named after — and a control named after a
|
|
53
|
+
* `ReactNode` is a control with no name at all.
|
|
54
|
+
*/
|
|
62
55
|
title: string
|
|
63
56
|
/** The trailing cluster: an assignee avatar, an elapsed-time chip. */
|
|
64
57
|
trailing?: ReactNode
|
|
65
58
|
}
|
|
66
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The leading colour bar, in one of two shapes — and the second one
|
|
62
|
+
* exists because the first cannot follow a corner.
|
|
63
|
+
*
|
|
64
|
+
* `accentIntent` is an inset pill: a `w-1` span inside the card,
|
|
65
|
+
* beside the content. It was built for priority — *"Priority is
|
|
66
|
+
* styled as a leading colour bar, and **never** a coloured word
|
|
67
|
+
* before the title."* An intent is a claim the design system makes
|
|
68
|
+
* (`danger` says what happens if you press the thing), so this arm
|
|
69
|
+
* is for a bar that means a STATE.
|
|
70
|
+
*
|
|
71
|
+
* `accentEdge` is the card's own edge — `Card`'s treatment, the same
|
|
72
|
+
* pseudo-element, so it takes `border-radius: inherit` and wraps the
|
|
73
|
+
* corner at `cq-lg` where this card grows one. It carries a
|
|
74
|
+
* CATEGORICAL colour or a computed one, which is what an identity
|
|
75
|
+
* needs: a project, a repo, a source. The owner asked for exactly
|
|
76
|
+
* this on Docket's board — *"The Board has no colors. That could
|
|
77
|
+
* use project colors on the sides at least."* — after reporting the
|
|
78
|
+
* same treatment missing from the task rows.
|
|
79
|
+
*
|
|
80
|
+
* The two are mutually exclusive by type, not by convention. Both
|
|
81
|
+
* paint a leading bar in the same place, and a card wearing two of
|
|
82
|
+
* them is a card whose leading edge means two things.
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ Either arm pairs with `accentLabel`. A bar is colour and
|
|
85
|
+
* nothing else, so on its own it fails WCAG 1.4.1 outright and is
|
|
86
|
+
* invisible to every screen reader; the label rides along in a
|
|
87
|
+
* `VisuallyHidden` so the same fact reaches both channels. Docket's
|
|
88
|
+
* first board painted its bars `--color-danger-9` — a Radix-style
|
|
89
|
+
* scale this token set has never had — so every bar was transparent
|
|
90
|
+
* while every "is it rendered" assertion passed.
|
|
91
|
+
*/
|
|
92
|
+
type BoardItemAccent =
|
|
93
|
+
| { accentEdge?: never; accentIntent?: IntentName }
|
|
94
|
+
| { accentEdge: CardAccentEdge; accentIntent?: never }
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Who owns the card title's link — the board, or the caller.
|
|
98
|
+
*
|
|
99
|
+
* A union rather than three loose optionals, because the two halves
|
|
100
|
+
* genuinely cannot coexist. `href` wraps the title in a `TextLink`
|
|
101
|
+
* and `onSelect` wraps it in a `<button>`; a `titleContent` that
|
|
102
|
+
* contains its own anchor would then be an anchor inside an anchor
|
|
103
|
+
* (which the HTML parser silently un-nests, dropping the rest of the
|
|
104
|
+
* card's link) or an anchor inside a button (which is invalid and
|
|
105
|
+
* unreachable by keyboard). Both look completely correct on screen.
|
|
106
|
+
*
|
|
107
|
+
* So the type refuses the combination outright, and a caller who
|
|
108
|
+
* needs a rich title takes the navigation with it:
|
|
109
|
+
*
|
|
110
|
+
* ```tsx
|
|
111
|
+
* {
|
|
112
|
+
* title: toPlainMarkdownText(task.title),
|
|
113
|
+
* titleContent: (
|
|
114
|
+
* <MarkdownLine href={`/tasks/${task.id}`} value={task.title} />
|
|
115
|
+
* ),
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
type BoardItemTitleLink =
|
|
120
|
+
| {
|
|
121
|
+
/** Where the title navigates. Routed through `RouterLinkProvider`. */
|
|
122
|
+
href?: string
|
|
123
|
+
/** Fires when there is no `href` — for a board that opens a modal. */
|
|
124
|
+
onSelect?: () => void
|
|
125
|
+
titleContent?: never
|
|
126
|
+
}
|
|
127
|
+
| {
|
|
128
|
+
href?: never
|
|
129
|
+
onSelect?: never
|
|
130
|
+
/**
|
|
131
|
+
* Drawn instead of `title`, inside the card's own clamping box
|
|
132
|
+
* — so it still truncates the way every other card does.
|
|
133
|
+
*
|
|
134
|
+
* It **owns its navigation**: the board adds no link around it,
|
|
135
|
+
* which is why `href` and `onSelect` are refused beside it.
|
|
136
|
+
* `title` is still required, and is still the name every
|
|
137
|
+
* control on the card is announced by.
|
|
138
|
+
*/
|
|
139
|
+
titleContent: ReactNode
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type BoardItem = BoardItemAccent &
|
|
143
|
+
BoardItemFields &
|
|
144
|
+
BoardItemTitleLink
|
|
145
|
+
|
|
67
146
|
export type BoardCardProps = {
|
|
68
147
|
item: BoardItem
|
|
69
148
|
laneLabel: string
|
|
@@ -137,12 +216,15 @@ export const BoardCard = ({
|
|
|
137
216
|
}: BoardCardProps): ReactNode => {
|
|
138
217
|
const [isMenuVisible, setIsMenuVisible] = useState(false)
|
|
139
218
|
|
|
140
|
-
const
|
|
219
|
+
const titleBlock = (
|
|
141
220
|
// `line-clamp-none` before `truncate`, because `line-clamp-2`
|
|
142
221
|
// sets `display: -webkit-box` and `text-overflow` has nothing to
|
|
143
222
|
// ellipsise inside one.
|
|
223
|
+
//
|
|
224
|
+
// A `titleContent` is clamped by the same box as a plain one —
|
|
225
|
+
// it replaces the card's *words*, not the card's shape.
|
|
144
226
|
<span className="line-clamp-2 cq-sm:line-clamp-none cq-sm:block cq-sm:truncate">
|
|
145
|
-
{item.title}
|
|
227
|
+
{item.titleContent ?? item.title}
|
|
146
228
|
</span>
|
|
147
229
|
)
|
|
148
230
|
|
|
@@ -155,10 +237,36 @@ export const BoardCard = ({
|
|
|
155
237
|
// cannot do this to itself — it is the container, and a
|
|
156
238
|
// container query never matches its own container.
|
|
157
239
|
"cq-lg:mb-2 cq-lg:rounded-lg cq-lg:border cq-lg:bg-surface-raised cq-lg:p-3 cq-lg:shadow-low",
|
|
240
|
+
// The edge is drawn on THIS box, which is why it needs no
|
|
241
|
+
// shape of its own: the box is a row below `cq-lg` and a
|
|
242
|
+
// rounded card above it, and `border-radius: inherit` gives
|
|
243
|
+
// the right answer at both — a straight stripe down a row
|
|
244
|
+
// that has no corner, and a bar that wraps the card's
|
|
245
|
+
// corner once it grows one.
|
|
246
|
+
//
|
|
247
|
+
// `ps-2` is what keeps the content off it. The bar is 3px
|
|
248
|
+
// and overlays rather than displaces, so without this the
|
|
249
|
+
// first mark sits on top of it; at `cq-lg` the card's own
|
|
250
|
+
// `p-3` is already wider than the bar.
|
|
251
|
+
item.accentEdge != null &&
|
|
252
|
+
toClassName(
|
|
253
|
+
"ps-2",
|
|
254
|
+
getAccentEdgeClassName(item.accentEdge),
|
|
255
|
+
),
|
|
158
256
|
item.isDimmed && "opacity-60",
|
|
159
257
|
)}
|
|
160
258
|
data-board-card={item.key}
|
|
259
|
+
{...(item.accentEdge == null
|
|
260
|
+
? {}
|
|
261
|
+
: { style: getAccentEdgeStyle(item.accentEdge) })}
|
|
161
262
|
>
|
|
263
|
+
{/* The label the bar cannot say. An edge has no content of
|
|
264
|
+
its own — it is a pseudo-element — so unlike the pill
|
|
265
|
+
arm below there is nowhere inside it to put the words. */}
|
|
266
|
+
{item.accentEdge != null && item.accentLabel ? (
|
|
267
|
+
<VisuallyHidden>{item.accentLabel}</VisuallyHidden>
|
|
268
|
+
) : null}
|
|
269
|
+
|
|
162
270
|
{item.accentIntent ? (
|
|
163
271
|
<span
|
|
164
272
|
className={toClassName(
|
|
@@ -191,7 +299,7 @@ export const BoardCard = ({
|
|
|
191
299
|
href={item.href}
|
|
192
300
|
intent="neutral"
|
|
193
301
|
>
|
|
194
|
-
{
|
|
302
|
+
{titleBlock}
|
|
195
303
|
</TextLink>
|
|
196
304
|
) : item.onSelect ? (
|
|
197
305
|
<button
|
|
@@ -202,10 +310,10 @@ export const BoardCard = ({
|
|
|
202
310
|
onClick={item.onSelect}
|
|
203
311
|
type="button"
|
|
204
312
|
>
|
|
205
|
-
{
|
|
313
|
+
{titleBlock}
|
|
206
314
|
</button>
|
|
207
315
|
) : (
|
|
208
|
-
|
|
316
|
+
titleBlock
|
|
209
317
|
)}
|
|
210
318
|
</div>
|
|
211
319
|
|
|
@@ -69,7 +69,20 @@ export type MarkdownSpan = {
|
|
|
69
69
|
text: string
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* `MarkdownEditorLine`, not `MarkdownLine` — renamed in 3.18.0 when
|
|
74
|
+
* `MarkdownLine` became a **component**, and a barrel cannot export
|
|
75
|
+
* one name as both a type and a value.
|
|
76
|
+
*
|
|
77
|
+
* The editor's name gave way rather than the component's for two
|
|
78
|
+
* reasons. It is the lower-level of the two — the painted layer's
|
|
79
|
+
* view of one line of a *document*, which is a thing only
|
|
80
|
+
* `MarkdownEditor` has ever needed — and it had no consumer outside
|
|
81
|
+
* this package in any repo in the fleet, checked before the rename.
|
|
82
|
+
* A component that renders a title is the name a call site reaches
|
|
83
|
+
* for; a line-kind union is not.
|
|
84
|
+
*/
|
|
85
|
+
export type MarkdownEditorLineKind =
|
|
73
86
|
| "blockquote"
|
|
74
87
|
| "code"
|
|
75
88
|
| "heading"
|
|
@@ -78,8 +91,8 @@ export type MarkdownLineKind =
|
|
|
78
91
|
| "table"
|
|
79
92
|
| "thematicBreak"
|
|
80
93
|
|
|
81
|
-
export type
|
|
82
|
-
kind:
|
|
94
|
+
export type MarkdownEditorLine = {
|
|
95
|
+
kind: MarkdownEditorLineKind
|
|
83
96
|
spans: MarkdownSpan[]
|
|
84
97
|
}
|
|
85
98
|
|
|
@@ -352,7 +365,7 @@ const toTableSpans = (text: string): MarkdownSpan[] => {
|
|
|
352
365
|
return spans
|
|
353
366
|
}
|
|
354
367
|
|
|
355
|
-
const toBlockLine = (line: string):
|
|
368
|
+
const toBlockLine = (line: string): MarkdownEditorLine => {
|
|
356
369
|
const spans: MarkdownSpan[] = []
|
|
357
370
|
|
|
358
371
|
const indent = INDENT_PATTERN.exec(line)?.[0] ?? ""
|
|
@@ -440,7 +453,7 @@ const toBlockLine = (line: string): MarkdownLine => {
|
|
|
440
453
|
*/
|
|
441
454
|
export const toMarkdownLines = (
|
|
442
455
|
text: string,
|
|
443
|
-
):
|
|
456
|
+
): MarkdownEditorLine[] => {
|
|
444
457
|
let isInsideFence = false
|
|
445
458
|
|
|
446
459
|
return text.split("\n").map((line) => {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
import * as MarkdownLineStories from "./MarkdownLine.stories";
|
|
4
|
+
|
|
5
|
+
<Meta of={MarkdownLineStories} />
|
|
6
|
+
|
|
7
|
+
# MarkdownLine
|
|
8
|
+
|
|
9
|
+
**One line of markdown, drawn with its inline marks and nothing else.** Reach for it
|
|
10
|
+
wherever a stored string is a *name* rather than a document — a card title, a lane heading,
|
|
11
|
+
a table cell, a breadcrumb.
|
|
12
|
+
|
|
13
|
+
<Canvas of={MarkdownLineStories.Default} />
|
|
14
|
+
|
|
15
|
+
## Props
|
|
16
|
+
|
|
17
|
+
<Controls />
|
|
18
|
+
|
|
19
|
+
## Why marks belong in a title
|
|
20
|
+
|
|
21
|
+
A grid of task names set in one flat weight is a wall. The parts that tell two cards apart —
|
|
22
|
+
the path, the urgency, the thing that is already done — are exactly the parts markdown would
|
|
23
|
+
have marked, so the marks are doing the same job the priority bar and the project colour do
|
|
24
|
+
one layer up: making a list **scannable**.
|
|
25
|
+
|
|
26
|
+
<Canvas of={MarkdownLineStories.WhyItHelps} />
|
|
27
|
+
|
|
28
|
+
## It is inline-only, and it is one line
|
|
29
|
+
|
|
30
|
+
| Rendered | Literal text |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| `**bold**`, `__bold__` | `# heading` |
|
|
33
|
+
| `*italic*`, `_italic_` | `- list item`, `1. list item` |
|
|
34
|
+
| `` `code` `` | `> blockquote` |
|
|
35
|
+
| `~~strikethrough~~` | `\| table \| row \|` |
|
|
36
|
+
| `[text](url)`, `<url>`, bare `https://…` and `www.…` | `` |
|
|
37
|
+
|
|
38
|
+
A newline collapses to a space. Every surface that draws one of these clamps to one or two
|
|
39
|
+
lines anyway, so a hard break would only push the second half of a title out of a box that
|
|
40
|
+
was measured for one line.
|
|
41
|
+
|
|
42
|
+
## File names are not markup
|
|
43
|
+
|
|
44
|
+
The case this component is most often asked to get wrong.
|
|
45
|
+
|
|
46
|
+
<Canvas of={MarkdownLineStories.FileNames} />
|
|
47
|
+
|
|
48
|
+
`ingest_the_files.sh` keeps its underscores because CommonMark's flanking rules say an
|
|
49
|
+
underscore **inside a word** opens nothing — the rule that makes `_` and `*` genuinely
|
|
50
|
+
different characters, and the one a hand-rolled "strip the markdown" regex always misses.
|
|
51
|
+
`2 * 3 * 4` is arithmetic for the same reason: a delimiter followed by a space cannot open.
|
|
52
|
+
|
|
53
|
+
## `href` — a link inside a link
|
|
54
|
+
|
|
55
|
+
<Canvas of={MarkdownLineStories.AsALink} />
|
|
56
|
+
|
|
57
|
+
A card title is usually a link to the card, and a title may also *contain* a link. An anchor
|
|
58
|
+
inside an anchor is invalid HTML: the parser closes the outer one at the inner one's start
|
|
59
|
+
tag, so the text after the inner link silently stops opening the card. It looks completely
|
|
60
|
+
correct and half of it does not work.
|
|
61
|
+
|
|
62
|
+
So `href` is a prop rather than something you wrap the component in. Given one, the plain
|
|
63
|
+
runs become anchors to `href` and a markdown link stays a **sibling** anchor with its own
|
|
64
|
+
destination:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<MarkdownLine href={`/tasks/${task.id}`} value={task.title} />
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Consecutive runs sharing a destination are merged into one anchor, so `**Ship** it` is a
|
|
71
|
+
single tab stop and one entry in a screen reader's link list.
|
|
72
|
+
|
|
73
|
+
A markdown link **splits** the line into more than one anchor, and each is named by the words
|
|
74
|
+
it covers — `Ship`, `#53`, `tonight`. That is honest, and it is not good: `tonight` is a poor
|
|
75
|
+
name for a link to a task. Naming all three after the whole line was tried and is worse — two
|
|
76
|
+
links with the same name are ambiguous to anything driving the page by name, and this
|
|
77
|
+
library's own `expectAgentDrivable` refuses it. The conclusion is a design one rather than an
|
|
78
|
+
API one: **a link usually belongs in a task's description**, where it has a sentence around
|
|
79
|
+
it, rather than in its title.
|
|
80
|
+
|
|
81
|
+
<Canvas of={MarkdownLineStories.Routed} />
|
|
82
|
+
|
|
83
|
+
An in-app destination goes through the injected `RouterLinkProvider`; an off-origin one
|
|
84
|
+
does not, and opens in a new tab. A title that names a pull request is a reference, and
|
|
85
|
+
following it should not lose the board the reader was on.
|
|
86
|
+
|
|
87
|
+
## `isInsideLink` — when the card is already the link
|
|
88
|
+
|
|
89
|
+
<Canvas of={MarkdownLineStories.InsideACardLink} />
|
|
90
|
+
|
|
91
|
+
The third and last answer to *who owns the link here*. `href` says this component does;
|
|
92
|
+
neither says the markdown links do; `isInsideLink` says **nobody** — draw the marks and emit
|
|
93
|
+
no anchor at all.
|
|
94
|
+
|
|
95
|
+
Reach for it whenever the line sits inside a link the caller already owns, which is what a
|
|
96
|
+
whole-card link is. The trap it closes has teeth even when no author ever typed a link: a
|
|
97
|
+
**bare URL autolinks**, so a captured note whose title ends in `https://…` would nest an
|
|
98
|
+
anchor without anybody writing `[]()`.
|
|
99
|
+
|
|
100
|
+
## Anywhere a string is required
|
|
101
|
+
|
|
102
|
+
`aria-label`, `title`, `document.title`, a confirmation sentence — call `toPlainMarkdownText`
|
|
103
|
+
rather than writing a regex that strips the markup:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { toPlainMarkdownText } from "@charcuterie/ui"
|
|
107
|
+
|
|
108
|
+
document.title = `${toPlainMarkdownText(task.title)} — Docket`
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
It is the **same parse** the component renders from, so it cannot drift. A second stripper is
|
|
112
|
+
how a card comes to read `Ingest 53 movies` while its own tooltip reads
|
|
113
|
+
``Ingest 53 movies from `Downloads/MOVIES` ``.
|
|
114
|
+
|
|
115
|
+
## A refused URL keeps its source
|
|
116
|
+
|
|
117
|
+
<Canvas of={MarkdownLineStories.RefusedUrl} />
|
|
118
|
+
|
|
119
|
+
URLs cross `safeUrls.ts` — an allowlist of `http`, `https`, `mailto` and `tel` — before they
|
|
120
|
+
reach an `href`, the same module `MarkdownView` and the CodeMirror editor use. A scheme that
|
|
121
|
+
does not survive it produces **no anchor at all**, and every character of the source stays on
|
|
122
|
+
screen. A reader who can see `[click me](javascript:…)` is better served than one shown a
|
|
123
|
+
confident blue word that silently refuses to work.
|
|
124
|
+
|
|
125
|
+
## Not a small `MarkdownView`
|
|
126
|
+
|
|
127
|
+
`MarkdownView` renders a *document*, through CodeMirror, from the optional
|
|
128
|
+
`@charcuterie/ui/markdown-editor-codemirror` entry point. Neither half suits a name: an
|
|
129
|
+
editor state, a syntax tree and a decoration set per card is an absurd price for forty
|
|
130
|
+
characters, and the main entry may not reach the CodeMirror stack at all.
|
|
131
|
+
|
|
132
|
+
So this is a second parser, which is worth being nervous about — two parsers are two answers
|
|
133
|
+
to "what does this markdown mean". Three things keep them honest: this one is **inline-only**,
|
|
134
|
+
so there is no block level to disagree about; both call the **same** URL guard; and the
|
|
135
|
+
flanking rules here are CommonMark's rather than a convenient approximation.
|