@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.
- package/README.md +8 -0
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +19 -1
- package/dist/Field/Field.js.map +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.js +21 -8
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -1
- package/dist/MarkdownEditor/markdownCommands.d.ts +32 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -1
- package/dist/MarkdownEditor/markdownCommands.js +47 -0
- package/dist/MarkdownEditor/markdownCommands.js.map +1 -1
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -1
- package/dist/MarkdownEditor/markdownSpans.js +30 -0
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -1
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.d.ts +93 -0
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.d.ts.map +1 -0
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js +545 -0
- package/dist/markdownEditorCodeMirror/MarkdownEditorCodeMirror.js.map +1 -0
- package/dist/markdownEditorCodeMirror/index.d.ts +16 -0
- package/dist/markdownEditorCodeMirror/index.d.ts.map +1 -0
- package/dist/markdownEditorCodeMirror/index.js +14 -0
- package/dist/markdownEditorCodeMirror/index.js.map +1 -0
- package/dist/markdownEditorCodeMirror/livePreview.d.ts +40 -0
- package/dist/markdownEditorCodeMirror/livePreview.d.ts.map +1 -0
- package/dist/markdownEditorCodeMirror/livePreview.js +428 -0
- package/dist/markdownEditorCodeMirror/livePreview.js.map +1 -0
- package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts +128 -0
- package/dist/markdownEditorCodeMirror/livePreviewRanges.d.ts.map +1 -0
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js +345 -0
- package/dist/markdownEditorCodeMirror/livePreviewRanges.js.map +1 -0
- package/package.json +36 -1
- package/src/Accordion/Accordion.stories.tsx +1 -1
- package/src/AdaptiveGrid/AdaptiveGrid.stories.tsx +1 -1
- package/src/Alert/Alert.stories.tsx +1 -1
- package/src/Badge/Badge.stories.tsx +1 -1
- package/src/Board/Board.stories.tsx +1 -1
- package/src/Button/Button.stories.tsx +1 -1
- package/src/ButtonLink/ButtonLink.stories.tsx +1 -1
- package/src/Card/Card.stories.tsx +1 -1
- package/src/Checkbox/Checkbox.stories.tsx +1 -1
- package/src/ColorSchemeSwitcher/ColorSchemeSwitcher.stories.tsx +1 -1
- package/src/ColorSchemeToggle/ColorSchemeToggle.stories.tsx +1 -1
- package/src/Combobox/Combobox.stories.tsx +1 -1
- package/src/DataTable/DataTable.stories.tsx +1 -1
- package/src/DatePicker/DatePicker.stories.tsx +1 -1
- package/src/Dialog/Dialog.stories.tsx +1 -1
- package/src/EmptyState/EmptyState.stories.tsx +1 -1
- package/src/Field/Field.stories.tsx +1 -1
- package/src/Field/Field.test.tsx +35 -0
- package/src/Field/Field.tsx +18 -0
- package/src/FileDropZone/FileDropZone.stories.tsx +1 -1
- package/src/Header/Header.stories.tsx +1 -1
- package/src/IconButton/IconButton.stories.tsx +1 -1
- package/src/Lightbox/Lightbox.stories.tsx +1 -1
- package/src/Listbox/Listbox.stories.tsx +1 -1
- package/src/LiveStatusIndicator/LiveStatusIndicator.stories.tsx +1 -1
- package/src/LogViewer/LogViewer.stories.tsx +1 -1
- package/src/Main/Main.stories.tsx +1 -1
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +1 -1
- package/src/MarkdownEditor/MarkdownEditor.tsx +33 -7
- package/src/MarkdownEditor/markdownCommands.test.ts +55 -0
- package/src/MarkdownEditor/markdownCommands.ts +60 -0
- package/src/MarkdownEditor/markdownSpans.test.ts +65 -0
- package/src/MarkdownEditor/markdownSpans.ts +35 -0
- package/src/MediaTile/MediaTile.stories.tsx +1 -1
- package/src/Menu/Menu.stories.tsx +1 -1
- package/src/Modal/Modal.stories.tsx +1 -1
- package/src/Picker/Picker.stories.tsx +1 -1
- package/src/Popover/Popover.stories.tsx +1 -1
- package/src/ProgressBar/ProgressBar.stories.tsx +1 -1
- package/src/QueryBuilder/QueryBuilder.stories.tsx +1 -1
- package/src/RadioGroup/RadioGroup.stories.tsx +1 -1
- package/src/Rail/Rail.stories.tsx +1 -1
- package/src/SegmentedControl/SegmentedControl.stories.tsx +1 -1
- package/src/Select/Select.stories.tsx +17 -6
- package/src/Shell/Shell.stories.tsx +1 -1
- package/src/Skeleton/Skeleton.stories.tsx +1 -1
- package/src/SortableTableHeader/SortableTableHeader.stories.tsx +1 -1
- package/src/Spinner/Spinner.stories.tsx +1 -1
- package/src/Swatch/Swatch.stories.tsx +1 -1
- package/src/Switch/Switch.stories.tsx +1 -1
- package/src/Tabs/Tabs.stories.tsx +1 -1
- package/src/TextLink/TextLink.stories.tsx +1 -1
- package/src/Toast/Toast.stories.tsx +1 -1
- package/src/Toolbar/Toolbar.stories.tsx +1 -1
- package/src/Tooltip/Tooltip.stories.tsx +1 -1
- package/src/VisuallyHidden/VisuallyHidden.stories.tsx +1 -1
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.mdx +144 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.stories.tsx +147 -0
- package/src/markdownEditorCodeMirror/MarkdownEditorCodeMirror.tsx +866 -0
- package/src/markdownEditorCodeMirror/index.ts +26 -0
- package/src/markdownEditorCodeMirror/livePreview.ts +535 -0
- package/src/markdownEditorCodeMirror/livePreviewRanges.test.ts +431 -0
- package/src/markdownEditorCodeMirror/livePreviewRanges.ts +581 -0
- package/src/sourceRules.test.ts +20 -0
|
@@ -10,7 +10,7 @@ import { FileDropZone } from "./FileDropZone.tsx"
|
|
|
10
10
|
const noop = () => undefined
|
|
11
11
|
|
|
12
12
|
const meta = {
|
|
13
|
-
title: "Components/FileDropZone",
|
|
13
|
+
title: "Components/Controls/FileDropZone",
|
|
14
14
|
component: FileDropZone,
|
|
15
15
|
parameters: { layout: "padded" },
|
|
16
16
|
args: {
|
|
@@ -27,7 +27,7 @@ import { IconButton } from "./IconButton.tsx"
|
|
|
27
27
|
const UNDO_ICON = <UndoIcon />
|
|
28
28
|
|
|
29
29
|
const meta = {
|
|
30
|
-
title: "Components/IconButton",
|
|
30
|
+
title: "Components/Actions/IconButton",
|
|
31
31
|
component: IconButton,
|
|
32
32
|
parameters: { layout: "padded" },
|
|
33
33
|
argTypes: { size: controlSizeArgType },
|
|
@@ -24,7 +24,7 @@ const CONNECTION_STATUSES = [
|
|
|
24
24
|
] as const
|
|
25
25
|
|
|
26
26
|
const meta = {
|
|
27
|
-
title: "Components/LiveStatusIndicator",
|
|
27
|
+
title: "Components/Feedback/LiveStatusIndicator",
|
|
28
28
|
component: LiveStatusIndicator,
|
|
29
29
|
parameters: { layout: "padded" },
|
|
30
30
|
argTypes: { status: connectionStatusArgType },
|
|
@@ -18,12 +18,14 @@ import {
|
|
|
18
18
|
indentLines,
|
|
19
19
|
insertLink,
|
|
20
20
|
insertText,
|
|
21
|
+
isLinkPaste,
|
|
21
22
|
MARKDOWN_LINE_PREFIXES,
|
|
22
23
|
outdentLines,
|
|
23
24
|
toggleHeading,
|
|
24
25
|
toggleInlineMarker,
|
|
25
26
|
toggleLinePrefix,
|
|
26
27
|
toMarkdownImage,
|
|
28
|
+
wrapSelectionInLink,
|
|
27
29
|
} from "./markdownCommands.ts"
|
|
28
30
|
import type { MarkdownSpanKind } from "./markdownSpans.ts"
|
|
29
31
|
import {
|
|
@@ -464,17 +466,41 @@ export const MarkdownEditor = ({
|
|
|
464
466
|
pasteEvent.clipboardData.files,
|
|
465
467
|
).filter(isImageFile)
|
|
466
468
|
|
|
467
|
-
if (files.length
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
469
|
+
if (files.length > 0 && onUploadImage) {
|
|
470
|
+
pasteEvent.preventDefault()
|
|
471
|
+
|
|
472
|
+
uploadImages(files)
|
|
473
|
+
|
|
472
474
|
return
|
|
473
475
|
}
|
|
474
476
|
|
|
475
|
-
|
|
477
|
+
const pasted =
|
|
478
|
+
pasteEvent.clipboardData.getData("text/plain")
|
|
476
479
|
|
|
477
|
-
|
|
480
|
+
const selection = readSelection()
|
|
481
|
+
|
|
482
|
+
// A URL pasted over selected text becomes a link, the way it
|
|
483
|
+
// does in every editor anyone uses. Reading `text/plain` here
|
|
484
|
+
// is not a hole in the no-HTML guarantee — that flavour is the
|
|
485
|
+
// only one a `<textarea>` would have taken anyway, and what
|
|
486
|
+
// goes back in is markdown built by a command.
|
|
487
|
+
if (
|
|
488
|
+
isLinkPaste(pasted) &&
|
|
489
|
+
selection.selectionStart !== selection.selectionEnd
|
|
490
|
+
) {
|
|
491
|
+
pasteEvent.preventDefault()
|
|
492
|
+
|
|
493
|
+
applyEdit(
|
|
494
|
+
wrapSelectionInLink(selection, pasted.trim()),
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Everything else is left to the browser on purpose. A
|
|
501
|
+
// `<textarea>` pastes `text/plain` and nothing else, so the
|
|
502
|
+
// no-HTML guarantee needs no code here — intercepting the paste
|
|
503
|
+
// to "sanitise" it would be the version that can have a bug.
|
|
478
504
|
}
|
|
479
505
|
|
|
480
506
|
const handleDrop = (
|
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
insertImage,
|
|
8
8
|
insertLink,
|
|
9
9
|
insertText,
|
|
10
|
+
isLinkPaste,
|
|
10
11
|
MARKDOWN_LINE_PREFIXES,
|
|
11
12
|
outdentLines,
|
|
12
13
|
toggleHeading,
|
|
13
14
|
toggleInlineMarker,
|
|
14
15
|
toggleLinePrefix,
|
|
15
16
|
toMarkdownImage,
|
|
17
|
+
wrapSelectionInLink,
|
|
16
18
|
} from "./markdownCommands.ts"
|
|
17
19
|
|
|
18
20
|
const at = (text: string, marker = "|") => {
|
|
@@ -382,3 +384,56 @@ test("the minimal edit is the smallest range that explains the change", () => {
|
|
|
382
384
|
text: "",
|
|
383
385
|
})
|
|
384
386
|
})
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Pasting a URL over selected text is what every editor the owner
|
|
390
|
+
* uses already does, and its absence is why a link pasted into a
|
|
391
|
+
* Docket description stayed flat text.
|
|
392
|
+
*/
|
|
393
|
+
test("a URL pasted over a selection wraps it", () => {
|
|
394
|
+
expect(
|
|
395
|
+
wrapSelectionInLink(
|
|
396
|
+
at("see |the docs| now"),
|
|
397
|
+
"https://example.com",
|
|
398
|
+
),
|
|
399
|
+
).toEqual({
|
|
400
|
+
// After the whole insertion, not inside the destination: the
|
|
401
|
+
// URL is already known, so parking the caret in it would ask
|
|
402
|
+
// the user to replace what they just pasted.
|
|
403
|
+
selectionEnd: 35,
|
|
404
|
+
selectionStart: 35,
|
|
405
|
+
text: "see [the docs](https://example.com) now",
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test("a URL pasted with no selection is inserted plainly", () => {
|
|
410
|
+
// Autolink renders a bare URL as a link anyway, so wrapping it
|
|
411
|
+
// with no text would produce `[](url)` — an empty link.
|
|
412
|
+
expect(
|
|
413
|
+
wrapSelectionInLink(
|
|
414
|
+
at("see | now"),
|
|
415
|
+
"https://example.com",
|
|
416
|
+
),
|
|
417
|
+
).toEqual({
|
|
418
|
+
selectionEnd: 23,
|
|
419
|
+
selectionStart: 23,
|
|
420
|
+
text: "see https://example.com now",
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
test.each([
|
|
425
|
+
["https://example.com", true],
|
|
426
|
+
["http://example.com/a?b=c#d", true],
|
|
427
|
+
[" https://example.com ", true],
|
|
428
|
+
["not a url", false],
|
|
429
|
+
["", false],
|
|
430
|
+
// Deliberately strict: a paste that is only *mostly* a URL is
|
|
431
|
+
// left alone, because mangling a literal paste is worse than
|
|
432
|
+
// not linkifying one.
|
|
433
|
+
["see https://example.com", false],
|
|
434
|
+
["https://a.com https://b.com", false],
|
|
435
|
+
["mailto:someone@example.com", false],
|
|
436
|
+
["www.example.com", false],
|
|
437
|
+
])("isLinkPaste(%j) is %s", (pasted, isExpected) => {
|
|
438
|
+
expect(isLinkPaste(pasted)).toBe(isExpected)
|
|
439
|
+
})
|
|
@@ -394,6 +394,66 @@ export const insertLink = (
|
|
|
394
394
|
})
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Does this pasted string want to become a link?
|
|
399
|
+
*
|
|
400
|
+
* Deliberately strict. A paste that is *mostly* a URL — a sentence
|
|
401
|
+
* with one in it, two URLs on two lines — is left alone, because
|
|
402
|
+
* the cost of guessing wrong is silently mangling something the
|
|
403
|
+
* user meant to paste literally. One absolute `http(s)` URL, no
|
|
404
|
+
* inner whitespace, nothing either side of it.
|
|
405
|
+
*
|
|
406
|
+
* `mailto:` and bare `www.` are excluded on the same principle:
|
|
407
|
+
* they are the cases where "did they mean a link?" genuinely has
|
|
408
|
+
* two answers.
|
|
409
|
+
*/
|
|
410
|
+
export const isLinkPaste = (pasted: string): boolean =>
|
|
411
|
+
/^https?:\/\/[^\s<>]+$/.test(pasted.trim())
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Paste a URL over selected text and get `[selected](url)`.
|
|
415
|
+
*
|
|
416
|
+
* Every editor the owner uses does this — Outline, Notion, Slack,
|
|
417
|
+
* GitHub — and its absence is why a link pasted into Docket stayed
|
|
418
|
+
* flat text nobody could click. It is the one paste behaviour worth
|
|
419
|
+
* intercepting, and it is still markdown in and markdown out.
|
|
420
|
+
*
|
|
421
|
+
* Unlike `insertLink`, the caret lands **after** the whole
|
|
422
|
+
* insertion rather than inside the destination: the URL is already
|
|
423
|
+
* known, so parking the selection in it would ask the user to
|
|
424
|
+
* replace what they just pasted.
|
|
425
|
+
*
|
|
426
|
+
* With an empty selection this is a plain insertion of the URL —
|
|
427
|
+
* autolink renders a bare URL as a link anyway, so wrapping it in
|
|
428
|
+
* `[](…)` with no text would produce an empty link instead.
|
|
429
|
+
*/
|
|
430
|
+
export const wrapSelectionInLink = (
|
|
431
|
+
state: MarkdownSelection,
|
|
432
|
+
url: string,
|
|
433
|
+
): MarkdownSelection => {
|
|
434
|
+
const selected = state.text.slice(
|
|
435
|
+
state.selectionStart,
|
|
436
|
+
state.selectionEnd,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if (selected === "") {
|
|
440
|
+
return insertText(state, url)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const insertion = `[${selected}](${url})`
|
|
444
|
+
|
|
445
|
+
const caret = state.selectionStart + insertion.length
|
|
446
|
+
|
|
447
|
+
return replaceRange({
|
|
448
|
+
end: state.selectionEnd,
|
|
449
|
+
insertion,
|
|
450
|
+
selectionEnd: caret,
|
|
451
|
+
selectionStart: caret,
|
|
452
|
+
start: state.selectionStart,
|
|
453
|
+
text: state.text,
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
|
|
397
457
|
/**
|
|
398
458
|
* What Enter should do inside a list, a task list or a blockquote —
|
|
399
459
|
* or `null` when it should do the ordinary thing and the component
|
|
@@ -215,3 +215,68 @@ test("the caret's line is found from its offset", () => {
|
|
|
215
215
|
|
|
216
216
|
expect(toLineIndex(text, 9999)).toBe(2)
|
|
217
217
|
})
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Gap 2 of Docket's markdown handoff. A URL pasted into a
|
|
221
|
+
* description used to tokenize as prose — no `url` span, nothing to
|
|
222
|
+
* paint, and indistinguishable from the words around it.
|
|
223
|
+
*/
|
|
224
|
+
test("a bare URL paints as a URL without being marked up", () => {
|
|
225
|
+
expect(
|
|
226
|
+
toInlineSpans("see https://example.com now"),
|
|
227
|
+
).toEqual([
|
|
228
|
+
{ kind: "plain", text: "see " },
|
|
229
|
+
{ kind: "url", text: "https://example.com" },
|
|
230
|
+
{ kind: "plain", text: " now" },
|
|
231
|
+
])
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test("a sentence's full stop is not part of the URL", () => {
|
|
235
|
+
expect(
|
|
236
|
+
toInlineSpans("read https://example.com/a."),
|
|
237
|
+
).toEqual([
|
|
238
|
+
{ kind: "plain", text: "read " },
|
|
239
|
+
{ kind: "url", text: "https://example.com/a" },
|
|
240
|
+
{ kind: "plain", text: "." },
|
|
241
|
+
])
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test("a parenthesised URL does not swallow the paren", () => {
|
|
245
|
+
expect(
|
|
246
|
+
toInlineSpans("(see https://example.com)"),
|
|
247
|
+
).toEqual([
|
|
248
|
+
{ kind: "plain", text: "(see " },
|
|
249
|
+
{ kind: "url", text: "https://example.com" },
|
|
250
|
+
{ kind: "plain", text: ")" },
|
|
251
|
+
])
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The destination inside an explicit link is already a `url` span
|
|
256
|
+
* emitted by the link rule, and the link rule runs first. If
|
|
257
|
+
* autolink won here, the `](` and `)` would stop being markers.
|
|
258
|
+
*/
|
|
259
|
+
test("an explicit link still tokenizes as a link", () => {
|
|
260
|
+
expect(
|
|
261
|
+
toInlineSpans("[docs](https://example.com)"),
|
|
262
|
+
).toEqual([
|
|
263
|
+
{ kind: "marker", text: "[" },
|
|
264
|
+
{ kind: "link", text: "docs" },
|
|
265
|
+
{ kind: "marker", text: "](" },
|
|
266
|
+
{ kind: "url", text: "https://example.com" },
|
|
267
|
+
{ kind: "marker", text: ")" },
|
|
268
|
+
])
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Autolink runs before emphasis, which fixes a bug that predates
|
|
273
|
+
* it: the stretch between two underscores in a URL used to paint
|
|
274
|
+
* as italic text.
|
|
275
|
+
*/
|
|
276
|
+
test("underscores inside a URL are not emphasis", () => {
|
|
277
|
+
expect(
|
|
278
|
+
toInlineSpans("https://example.com/a_b_c"),
|
|
279
|
+
).toEqual([
|
|
280
|
+
{ kind: "url", text: "https://example.com/a_b_c" },
|
|
281
|
+
])
|
|
282
|
+
})
|
|
@@ -121,8 +121,22 @@ const TASK_MARKER_PATTERN = /^\[[ xX]\][ \t]*/
|
|
|
121
121
|
* - **Image before link**, because `` starts with a
|
|
122
122
|
* valid link at index 1.
|
|
123
123
|
* - **Strong before emphasis**, because `**` starts with `*`.
|
|
124
|
+
* - **Link before autolink**, because the `https://…` inside
|
|
125
|
+
* `[text](https://…)` is a destination, not a bare URL, and
|
|
126
|
+
* matching it as one would paint the parentheses as link text.
|
|
124
127
|
*/
|
|
125
128
|
const INLINE_PATTERNS = {
|
|
129
|
+
/**
|
|
130
|
+
* A bare URL, as GFM autolinks it.
|
|
131
|
+
*
|
|
132
|
+
* Trailing punctuation is excluded from the match rather than
|
|
133
|
+
* trimmed after: `see https://example.com.` ends a sentence, and
|
|
134
|
+
* the full stop belongs to the prose. The closing paren is
|
|
135
|
+
* likewise left out so `(see https://example.com)` does not
|
|
136
|
+
* swallow it — at the cost of clipping a URL that genuinely ends
|
|
137
|
+
* in one, which is the rarer and less annoying failure.
|
|
138
|
+
*/
|
|
139
|
+
autolink: /https?:\/\/[^\s<>[\]()]+[^\s<>[\]().,;:!?'"]/y,
|
|
126
140
|
code: /(`+)([^\n]*?)\1/y,
|
|
127
141
|
emphasis: /([*_])(?=[^\s*_])([^\n*_]*?[^\s*_])\1/y,
|
|
128
142
|
escape: /\\([\\`*_{}[\]()#+\-.!>~|])/y,
|
|
@@ -243,6 +257,27 @@ export const toInlineSpans = (
|
|
|
243
257
|
continue
|
|
244
258
|
}
|
|
245
259
|
|
|
260
|
+
INLINE_PATTERNS.autolink.lastIndex = index
|
|
261
|
+
|
|
262
|
+
const autolinked = INLINE_PATTERNS.autolink.exec(text)
|
|
263
|
+
|
|
264
|
+
if (autolinked) {
|
|
265
|
+
// One span, no markers — there is no syntax to dim, because
|
|
266
|
+
// the user typed none. It paints as a URL so that a pasted
|
|
267
|
+
// link reads as a link, which is the whole complaint this
|
|
268
|
+
// answers: a URL dropped into a description used to be
|
|
269
|
+
// indistinguishable from prose.
|
|
270
|
+
//
|
|
271
|
+
// Running before `emphasis` also fixes a bug nobody filed:
|
|
272
|
+
// a URL with two underscores in it used to italicise the
|
|
273
|
+
// stretch between them.
|
|
274
|
+
pushSpan(spans, "url", autolinked[0])
|
|
275
|
+
|
|
276
|
+
index += autolinked[0].length
|
|
277
|
+
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
|
|
246
281
|
INLINE_PATTERNS.strong.lastIndex = index
|
|
247
282
|
|
|
248
283
|
const strong = INLINE_PATTERNS.strong.exec(text)
|
|
@@ -25,7 +25,7 @@ const toPosterSrc = (label: string) =>
|
|
|
25
25
|
const MISSING_SRC = "/charcuterie-missing-poster.png"
|
|
26
26
|
|
|
27
27
|
const meta = {
|
|
28
|
-
title: "Components/MediaTile",
|
|
28
|
+
title: "Components/Data/MediaTile",
|
|
29
29
|
component: MediaTile,
|
|
30
30
|
parameters: { layout: "padded" },
|
|
31
31
|
args: { aspectRatio: "poster" },
|
|
@@ -24,7 +24,7 @@ const COLUMN_ITEMS: SegmentedItem[] = [
|
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
const meta = {
|
|
27
|
-
title: "Components/SegmentedControl",
|
|
27
|
+
title: "Components/Controls/SegmentedControl",
|
|
28
28
|
component: SegmentedControl,
|
|
29
29
|
parameters: { layout: "padded" },
|
|
30
30
|
argTypes: { size: controlSizeArgType },
|
|
@@ -41,13 +41,24 @@ const GROUPED: SelectItem[] = [
|
|
|
41
41
|
]
|
|
42
42
|
|
|
43
43
|
const meta = {
|
|
44
|
-
|
|
45
|
-
// moving it: the sidebar is the first place an agent looks for a
|
|
46
|
-
// picker, and a `Select` sitting between `Rail` and `Shell` reads
|
|
47
|
-
// as a component in good standing. Nothing new gets one — `Picker`
|
|
48
|
-
// is the drop-in. See the 2026-08-20 deprecation record.
|
|
49
|
-
title: "Deprecated/Select",
|
|
44
|
+
title: "Components/Controls/Select",
|
|
50
45
|
component: Select,
|
|
46
|
+
/**
|
|
47
|
+
* Deprecated — `Picker` is the drop-in. This is a **label, not a
|
|
48
|
+
* relocation**: the component still lives beside the other
|
|
49
|
+
* controls, where somebody looking for a picker will actually
|
|
50
|
+
* find it, and the sidebar paints a badge on it there. A
|
|
51
|
+
* `Deprecated/` section instead of a label hides the warning from
|
|
52
|
+
* everyone who does not already know to look for it, which is
|
|
53
|
+
* exactly backwards.
|
|
54
|
+
*
|
|
55
|
+
* Manager entries take the **intersection** of their children's
|
|
56
|
+
* tags, so tagging the meta puts the badge on the `Select` node
|
|
57
|
+
* itself, not just on each story underneath it.
|
|
58
|
+
* `sidebar.renderLabel` in `@charcuterie/storybook-config/manager`
|
|
59
|
+
* is what draws it.
|
|
60
|
+
*/
|
|
61
|
+
tags: ["deprecated"],
|
|
51
62
|
parameters: { layout: "padded" },
|
|
52
63
|
argTypes: { size: controlSizeArgType },
|
|
53
64
|
args: {
|
|
@@ -10,7 +10,7 @@ import { Card } from "../Card/Card.tsx"
|
|
|
10
10
|
import { Skeleton } from "./Skeleton.tsx"
|
|
11
11
|
|
|
12
12
|
const meta = {
|
|
13
|
-
title: "Components/Skeleton",
|
|
13
|
+
title: "Components/Feedback/Skeleton",
|
|
14
14
|
component: Skeleton,
|
|
15
15
|
parameters: { layout: "padded" },
|
|
16
16
|
args: { lineCount: 1, shape: "block" },
|
|
@@ -30,7 +30,7 @@ const CELL_CLASS =
|
|
|
30
30
|
* every table gets wrong the same way.
|
|
31
31
|
*/
|
|
32
32
|
const meta = {
|
|
33
|
-
title: "Components/SortableTableHeader",
|
|
33
|
+
title: "Components/Data/SortableTableHeader",
|
|
34
34
|
component: SortableTableHeader,
|
|
35
35
|
parameters: { layout: "padded" },
|
|
36
36
|
args: { onSort: noop },
|
|
@@ -20,7 +20,7 @@ const noop = () => undefined
|
|
|
20
20
|
* is the documented way to hold one for an action anyway.
|
|
21
21
|
*/
|
|
22
22
|
const meta = {
|
|
23
|
-
title: "Components/Toast",
|
|
23
|
+
title: "Components/Overlays/Toast",
|
|
24
24
|
component: Toast,
|
|
25
25
|
parameters: { layout: "padded" },
|
|
26
26
|
argTypes: { intent: intentArgType },
|
|
@@ -11,7 +11,7 @@ import { SettingsIcon } from "../icons.storyHelpers.tsx"
|
|
|
11
11
|
import { Tooltip } from "./Tooltip.tsx"
|
|
12
12
|
|
|
13
13
|
const meta = {
|
|
14
|
-
title: "Components/Tooltip",
|
|
14
|
+
title: "Components/Overlays/Tooltip",
|
|
15
15
|
component: Tooltip,
|
|
16
16
|
parameters: { layout: "centered" },
|
|
17
17
|
argTypes: { placement: placementArgType },
|