cortena-ui 1.10.0 → 1.11.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 (35) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +114 -0
  3. package/dist/components/badge.d.ts +1 -1
  4. package/dist/components/button.d.ts +1 -1
  5. package/dist/components/markdown.js +32 -38
  6. package/dist/components/markdown.js.map +1 -1
  7. package/dist/components/rich-text-editor/extensions.d.ts +43 -0
  8. package/dist/components/rich-text-editor/extensions.js +278 -0
  9. package/dist/components/rich-text-editor/extensions.js.map +1 -0
  10. package/dist/components/rich-text-editor/index.d.ts +6 -0
  11. package/dist/components/rich-text-editor/markdown-bridge.d.ts +81 -0
  12. package/dist/components/rich-text-editor/markdown-bridge.js +618 -0
  13. package/dist/components/rich-text-editor/markdown-bridge.js.map +1 -0
  14. package/dist/components/rich-text-editor/rich-text-editor.d.ts +109 -0
  15. package/dist/components/rich-text-editor/rich-text-editor.js +357 -0
  16. package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -0
  17. package/dist/components/rich-text-editor/toolbar.d.ts +27 -0
  18. package/dist/components/rich-text-editor/toolbar.js +222 -0
  19. package/dist/components/rich-text-editor/toolbar.js.map +1 -0
  20. package/dist/index.d.ts +6 -1
  21. package/dist/index.js +5 -1
  22. package/dist/lib/prose.js +85 -0
  23. package/dist/lib/prose.js.map +1 -0
  24. package/dist/rich-text-editor.d.ts +7 -0
  25. package/dist/rich-text-editor.js +6 -0
  26. package/package.json +20 -2
  27. package/src/components/markdown.tsx +29 -172
  28. package/src/components/rich-text-editor/extensions.ts +328 -0
  29. package/src/components/rich-text-editor/index.ts +19 -0
  30. package/src/components/rich-text-editor/markdown-bridge.ts +791 -0
  31. package/src/components/rich-text-editor/rich-text-editor.tsx +607 -0
  32. package/src/components/rich-text-editor/toolbar.tsx +281 -0
  33. package/src/entries/rich-text-editor.ts +53 -0
  34. package/src/index.ts +2 -0
  35. package/src/lib/prose.ts +149 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,49 @@
3
3
  Notable changes per release. Versions before 1.6.0 are recorded in the git log
4
4
  and in `../../CONSUMING.md`; this file starts where the changelog does.
5
5
 
6
+ ## 1.11.0
7
+
8
+ ### Added
9
+
10
+ - **`RichTextEditor`** (DESIGN-54). Rich text editing, which nothing in the
11
+ fleet had. KeyStone needs it for vision documents, PRDs and requirement
12
+ documents; Tasks needs it to be able to replace a `<textarea>` it chose on
13
+ purpose. TipTap on ProseMirror, React bindings, value is Markdown (GFM) in
14
+ and out. Exported from the barrel and from its own subpath entry,
15
+ `cortena-ui/rich-text-editor` — TipTap and ProseMirror are 619 kB and MCP app
16
+ HTML is capped at 512 kB, so `test/bundle-probe.test.mjs` now asserts that no
17
+ other entry carries a byte of the editor engine (`button` from the barrel is
18
+ still 37.7 kB and pulls none of it).
19
+ - **The round trip is the feature.** An untouched document serialises
20
+ byte-identically — same line endings, same trailing whitespace — and
21
+ editing one paragraph changes one paragraph. Tasks uses a plain textarea
22
+ because a rich editor "would reformat text nobody touched and land it as a
23
+ spurious revision"; that constraint is a requirement of this component.
24
+ `markdown-bridge.ts` is mdast-based and source-preserving rather than
25
+ `tiptap-markdown`, which serialises through Turndown and renormalises
26
+ everything it touches. Asserted over an 18-file corpus in
27
+ `test/fixtures/rich-text/`.
28
+ - **Supported subset**: paragraphs, h2–h4, bold, italic, strikethrough,
29
+ inline code, links, ordered/bullet/task lists, blockquote, fenced code, GFM
30
+ tables, horizontal rule, images by URL. Anything outside it — front matter,
31
+ HTML blocks, footnotes, display maths, h1, h5, h6 — is preserved as a raw
32
+ Markdown block, never dropped.
33
+ - **Slots, not features**: `mentionProvider`, `selectionActions`,
34
+ `renderChip`. A mention is stored as `[@Priya](mention:user-42)`, so it is
35
+ still Markdown. Editor JSON and HTML never leave the component.
36
+ - **Controlled `value` with an explicit `version`**: a new revision arriving
37
+ while the author is focused does not clobber them; `onExternalChange` fires
38
+ with an `apply` for a conflict banner.
39
+ - Toolbar on the existing `Toolbar` primitive, keyboard shortcuts, and the
40
+ Markdown input rules (`## `, `- `, `1. `, `> `, backticks).
41
+
42
+ ### Changed
43
+
44
+ - The prose class map moved to `src/lib/prose.ts` and is now shared by
45
+ `Markdown` and `RichTextEditor`. No visual change — the strings are the same
46
+ ones — but the read view and the edit view can no longer drift apart, which
47
+ is what keeps a document from shifting when it enters edit mode.
48
+
6
49
  ## 1.10.0
7
50
 
8
51
  1.9.0 was never released: it was published by mistake and has been deprecated.
package/README.md CHANGED
@@ -36,6 +36,7 @@ consumer's bundler getting tree-shaking right:
36
36
  | `cortena-ui/a2ui` | the A2UI catalogue and renderer | most of the package, by design |
37
37
  | `cortena-ui/sortable-list` | `SortableList`, `SortableHandle`, `arrayMove` | dnd-kit |
38
38
  | `cortena-ui/agent-chat` | `AgentChatPopup`, `AgentChat`, `createAguiAgentChatClient` | the A2UI catalogue, plus `@ag-ui/client` (rxjs, zod 3, uuid, protobuf) |
39
+ | `cortena-ui/rich-text-editor` | `RichTextEditor`, `parseMarkdown`, `serializeMarkdown` | TipTap on ProseMirror, 619 kB — the heaviest entry in the package |
39
40
 
40
41
  Each component has exactly one home, so the barrel re-exports every entry
41
42
  without an ambiguous name. **Adding a component means adding it to its area
@@ -431,6 +432,119 @@ names the session (the key is still written, so one window stays on one
431
432
  session), `listSessions: false` for a host with its own session list, and
432
433
  `mintSessionKey` for a host that binds sessions some other way.
433
434
 
435
+ ## RichTextEditor
436
+
437
+ WYSIWYG editing whose value is Markdown (GFM), in and out. KeyStone needs it for
438
+ vision documents, PRDs and requirement documents; nothing else in the fleet has
439
+ rich text at all.
440
+
441
+ ```tsx
442
+ import { RichTextEditor } from "cortena-ui/rich-text-editor";
443
+
444
+ <RichTextEditor
445
+ value={markdown}
446
+ version={revision}
447
+ aria-label="Vision document"
448
+ onChange={setMarkdown}
449
+ onExternalChange={({ apply }) => setConflict(() => apply)}
450
+ mentionProvider={({ trigger, query }) => search(trigger, query)}
451
+ selectionActions={({ text, editor }) => <AskAgent text={text} />}
452
+ />
453
+ ```
454
+
455
+ Editor JSON and HTML never leave the component. There is no `getHTML` escape
456
+ hatch on purpose: the moment a consumer stores ProseMirror JSON, the document
457
+ stops being a file that anything else can read.
458
+
459
+ ### The round trip is the feature
460
+
461
+ Tasks deliberately uses a plain `<textarea>` for its section editor, because a
462
+ rich editor "would reformat text nobody touched and land it as a spurious
463
+ revision". That constraint is a requirement of this component, not a nice-to-have:
464
+
465
+ - **An untouched document serialises byte-identically.** Same bytes, including
466
+ line endings, trailing whitespace and a byte order mark.
467
+ - **Editing one paragraph changes one paragraph.** Every other block is written
468
+ back from the source it was parsed from.
469
+
470
+ `test/rich-text-markdown.test.tsx` asserts both over an 18-file corpus in
471
+ `test/fixtures/rich-text/`, covering nested lists, fences containing fences,
472
+ tables with escaped pipes, links with titles, CRLF and trailing whitespace.
473
+
474
+ ### Why the bridge is written rather than adopted
475
+
476
+ `tiptap-markdown` is the obvious candidate and it fails the one requirement
477
+ above. It serialises by rendering the editor to HTML and running Turndown over
478
+ it, so the output is whatever Turndown's rules produce: bullets become `*`,
479
+ emphasis becomes `_`, tables are re-padded, and a document nobody edited comes
480
+ back different from how it went in. A component that cannot replace the Tasks
481
+ textarea has not delivered anything.
482
+
483
+ A plain mdast serializer does not fix it either. Any AST round trip normalises —
484
+ `mdast-util-to-markdown` has one way to write a bullet — and the moment the
485
+ author's file disagrees, every untouched paragraph in it is rewritten.
486
+
487
+ So `markdown-bridge.ts` is mdast-based **and** source-preserving. Parsing keeps
488
+ the exact source slice of every top-level block; a block whose ProseMirror JSON
489
+ is byte-for-byte what parsing produced is written back from that slice, and only
490
+ a block the user actually changed goes through `mdast-util-to-markdown`. The
491
+ dependencies are `mdast-util-from-markdown`, `mdast-util-to-markdown`,
492
+ `mdast-util-gfm` and `micromark-extension-frontmatter` (all MIT), which is the
493
+ same layer react-markdown already uses — `src/lib/prose.ts` is imported by both
494
+ views and reaches neither react-markdown nor ProseMirror.
495
+
496
+ ### The supported subset, and what happens outside it
497
+
498
+ Paragraphs; headings h2–h4 (h1 is reserved for the record's own title); bold,
499
+ italic, strikethrough, inline code; links; ordered, bullet and task lists;
500
+ blockquote; fenced code; GFM tables; horizontal rule; images by URL.
501
+
502
+ Anything else — front matter, HTML blocks, footnotes, display maths, h1, h5, h6 —
503
+ becomes a **raw block**: a code-like node holding the original Markdown as text,
504
+ editable as raw Markdown and serialised with no escaping. It is never dropped.
505
+ The rule applies at the block boundary, so a paragraph containing a footnote
506
+ reference goes raw whole rather than losing the reference from an otherwise
507
+ editable paragraph.
508
+
509
+ Not in scope: maths rendering, collaborative cursors, comments UI, image upload,
510
+ syntax highlighting in fences.
511
+
512
+ ### Slots, not features
513
+
514
+ `@` and `#` mean nothing to this component. `mentionProvider` is asked for items
515
+ and a chip is inserted; whether that resolves to a person, a task or a page is
516
+ the consumer's business. A mention is stored as `[@Priya](mention:user-42)`, so
517
+ the document is still ordinary Markdown and the editor reads the chip back on
518
+ the next open.
519
+
520
+ What survives in the **read** view is the label, not the link: `Markdown` passes
521
+ URLs through react-markdown's `urlTransform`, which drops any protocol it does
522
+ not know, so `[@Priya](mention:user-42)` renders as the text `@Priya` with no
523
+ href. A host that wants mentions to be clickable in the read view passes its own
524
+ `urlTransform` to `Markdown` and resolves `mention:` itself.
525
+
526
+ `selectionActions` renders whatever the host wants over the current selection,
527
+ and `renderChip` replaces how a mention looks in the menu.
528
+
529
+ ### The controlled value and `version`
530
+
531
+ `value` is the document and `version` is the revision it belongs to. A new
532
+ revision arriving while the author is focused does **not** replace what they are
533
+ writing: `onExternalChange` fires with `{ value, version, apply }` so the
534
+ consumer can show a conflict banner and adopt it on the author's say-so. When
535
+ the author is not in the editor it is adopted silently. `version` defaults to
536
+ `value`, so a consumer that simply echoes `onChange` back still behaves — an
537
+ echo of our own output is never treated as a conflict.
538
+
539
+ ### Styling
540
+
541
+ Prose comes from `src/lib/prose.ts`, the same class strings the `Markdown`
542
+ renderer paints the read view with, handed to TipTap as each node's
543
+ `HTMLAttributes.class`. That is the only reason a document does not shift when
544
+ it enters edit mode, and `test/rich-text-editor.test.tsx` compares the computed
545
+ font size, family, weight and line height of both components on the same source.
546
+ Changing a prose style means changing it in `prose.ts`, for both views at once.
547
+
434
548
  ## Column widths
435
549
 
436
550
  `DataTable` lays the grid out `table-fixed`: every column is drawn at its
@@ -3,7 +3,7 @@ import { VariantProps } from "class-variance-authority";
3
3
  import * as React from "react";
4
4
  //#region src/components/badge.d.ts
5
5
  declare const badgeVariants: (props?: ({
6
- variant?: "default" | "success" | "warn" | "destructive" | "danger" | "info" | "outline" | "warning" | "neutral" | "secondary" | null | undefined;
6
+ variant?: "default" | "success" | "warn" | "destructive" | "danger" | "info" | "warning" | "neutral" | "secondary" | "outline" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  export interface BadgeProps extends React.ComponentProps<"span">, VariantProps<typeof badgeVariants> {}
9
9
  declare function Badge({ className, variant, ...props }: BadgeProps): React.JSX.Element;
@@ -14,7 +14,7 @@ import { Button } from "@base-ui/react/button";
14
14
  * rendered element, which is Base UI's equivalent.
15
15
  */
16
16
  declare const buttonVariants: (props?: ({
17
- variant?: "link" | "destructive" | "outline" | "secondary" | "primary" | "ghost" | "soft" | null | undefined;
17
+ variant?: "link" | "destructive" | "secondary" | "outline" | "primary" | "ghost" | "soft" | null | undefined;
18
18
  size?: "sm" | "md" | "lg" | "icon" | "icon-sm" | null | undefined;
19
19
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
20
20
  export interface ButtonProps extends React.ComponentProps<typeof Button>, VariantProps<typeof buttonVariants> {}
@@ -2,6 +2,7 @@
2
2
  "use client";
3
3
  import { cn } from "../lib/cn.js";
4
4
  import { shakeable } from "../lib/shakeable.js";
5
+ import { prose } from "../lib/prose.js";
5
6
  import { Check, Copy } from "lucide-react";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
7
8
  import * as React from "react";
@@ -28,10 +29,6 @@ function dom(props) {
28
29
  const { node: _node, ...rest } = props;
29
30
  return rest;
30
31
  }
31
- const block = "my-3 first:mt-0 last:mb-0 group-data-[compact]/markdown:my-1.5";
32
- const body = /* @__PURE__ */ cn("text-[length:var(--ds-text-body-sm)] leading-[var(--ds-text-body-sm--line-height)] text-[color:var(--ds-foreground)]");
33
- const heading = /* @__PURE__ */ cn("font-[family-name:var(--ds-font-display)] text-[color:var(--ds-foreground)]", "mt-6 mb-2 first:mt-0 group-data-[compact]/markdown:mt-3 group-data-[compact]/markdown:mb-1");
34
- const cellClassName = "border border-[var(--ds-border)] px-3 py-1.5 align-top";
35
32
  function CopyButton({ text }) {
36
33
  const [copied, setCopied] = React.useState(false);
37
34
  React.useEffect(() => {
@@ -55,31 +52,31 @@ function buildComponents(codeRenderer) {
55
52
  return {
56
53
  h1: (p) => /* @__PURE__ */ jsx("h1", {
57
54
  ...dom(p),
58
- className: cn(heading, "text-[length:var(--ds-text-h1)] leading-[var(--ds-text-h1--line-height)] tracking-[var(--ds-text-h1--letter-spacing)] font-semibold", "group-data-[compact]/markdown:text-[length:var(--ds-text-h3)] group-data-[compact]/markdown:leading-[var(--ds-text-h3--line-height)]")
55
+ className: prose.h1
59
56
  }),
60
57
  h2: (p) => /* @__PURE__ */ jsx("h2", {
61
58
  ...dom(p),
62
- className: cn(heading, "text-[length:var(--ds-text-h2)] leading-[var(--ds-text-h2--line-height)] tracking-[var(--ds-text-h2--letter-spacing)] font-bold", "group-data-[compact]/markdown:text-[length:var(--ds-text-body-lg)] group-data-[compact]/markdown:leading-[var(--ds-text-body-lg--line-height)]")
59
+ className: prose.h2
63
60
  }),
64
61
  h3: (p) => /* @__PURE__ */ jsx("h3", {
65
62
  ...dom(p),
66
- className: cn(heading, "text-[length:var(--ds-text-h3)] leading-[var(--ds-text-h3--line-height)] font-semibold", "group-data-[compact]/markdown:text-[length:var(--ds-text-body-md)] group-data-[compact]/markdown:leading-[var(--ds-text-body-md--line-height)]")
63
+ className: prose.h3
67
64
  }),
68
65
  h4: (p) => /* @__PURE__ */ jsx("h4", {
69
66
  ...dom(p),
70
- className: cn(heading, "font-[family-name:var(--ds-font-body)] text-[length:var(--ds-text-body-md)] leading-[var(--ds-text-body-md--line-height)] font-semibold")
67
+ className: prose.h4
71
68
  }),
72
69
  h5: (p) => /* @__PURE__ */ jsx("h5", {
73
70
  ...dom(p),
74
- className: cn(heading, "font-[family-name:var(--ds-font-body)] text-[length:var(--ds-text-body)] leading-[var(--ds-text-body--line-height)] font-semibold")
71
+ className: prose.h5
75
72
  }),
76
73
  h6: (p) => /* @__PURE__ */ jsx("h6", {
77
74
  ...dom(p),
78
- className: cn(heading, "font-[family-name:var(--ds-font-body)] text-[length:var(--ds-text-eyebrow)] leading-[var(--ds-text-eyebrow--line-height)] tracking-[var(--ds-text-eyebrow--letter-spacing)] font-semibold uppercase text-[color:var(--ds-muted-foreground)]")
75
+ className: prose.h6
79
76
  }),
80
77
  p: (p) => /* @__PURE__ */ jsx("p", {
81
78
  ...dom(p),
82
- className: cn(block, body)
79
+ className: prose.p
83
80
  }),
84
81
  a: (p) => {
85
82
  const { href, ...rest } = dom(p);
@@ -89,24 +86,24 @@ function buildComponents(codeRenderer) {
89
86
  href,
90
87
  target: external ? "_blank" : void 0,
91
88
  rel: external ? "noopener noreferrer" : void 0,
92
- className: cn("text-[color:var(--ds-primary)] underline underline-offset-4 decoration-[color:var(--ds-primary)]/40", "hover:text-[color:var(--ds-primary-hover)] hover:decoration-[color:var(--ds-primary-hover)]", "outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40 rounded-[var(--ds-radius-sm)]")
89
+ className: prose.a
93
90
  });
94
91
  },
95
92
  strong: (p) => /* @__PURE__ */ jsx("strong", {
96
93
  ...dom(p),
97
- className: "font-semibold text-[color:var(--ds-foreground)]"
94
+ className: prose.strong
98
95
  }),
99
96
  em: (p) => /* @__PURE__ */ jsx("em", {
100
97
  ...dom(p),
101
- className: "italic"
98
+ className: prose.em
102
99
  }),
103
100
  del: (p) => /* @__PURE__ */ jsx("del", {
104
101
  ...dom(p),
105
- className: "line-through text-[color:var(--ds-muted-foreground)]"
102
+ className: prose.del
106
103
  }),
107
104
  code: (p) => /* @__PURE__ */ jsx("code", {
108
105
  ...dom(p),
109
- className: cn("rounded-[var(--ds-radius-sm)] bg-[var(--ds-muted)] px-1 py-0.5", "font-[family-name:var(--ds-font-mono)] text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-foreground)]", "border border-[var(--ds-border-subtle)]")
106
+ className: prose.code
110
107
  }),
111
108
  pre: (p) => {
112
109
  const codeNode = p.node?.children.find((c) => c.type === "element" && c.tagName === "code");
@@ -119,12 +116,12 @@ function buildComponents(codeRenderer) {
119
116
  return /* @__PURE__ */ jsxs("div", {
120
117
  "data-slot": "markdown-pre",
121
118
  "data-language": language,
122
- className: cn(block, "group/pre relative overflow-hidden", "rounded-[var(--ds-radius-md)] border border-[var(--ds-border-subtle)] bg-[var(--ds-muted)]"),
119
+ className: cn(prose.codeFrame, "group/pre relative overflow-hidden"),
123
120
  children: [codeRenderer ? codeRenderer({
124
121
  code,
125
122
  language
126
123
  }) : /* @__PURE__ */ jsx("pre", {
127
- className: cn("overflow-x-auto p-3 pr-10", "font-[family-name:var(--ds-font-mono)] text-[length:var(--ds-text-caption-lg)] leading-[var(--ds-text-caption-lg--line-height)] text-[color:var(--ds-foreground)]"),
124
+ className: cn("overflow-x-auto p-3 pr-10", prose.codeText),
128
125
  children: /* @__PURE__ */ jsx("code", {
129
126
  "data-language": language,
130
127
  children: code
@@ -134,15 +131,15 @@ function buildComponents(codeRenderer) {
134
131
  },
135
132
  blockquote: (p) => /* @__PURE__ */ jsx("blockquote", {
136
133
  ...dom(p),
137
- className: cn(block, "border-l-2 border-[var(--ds-border-strong)] pl-4 text-[color:var(--ds-muted-foreground)]", "[&>p]:text-[color:inherit]")
134
+ className: prose.blockquote
138
135
  }),
139
136
  ul: (p) => /* @__PURE__ */ jsx("ul", {
140
137
  ...dom(p),
141
- className: cn(block, body, "list-disc pl-6 marker:text-[color:var(--ds-text-tertiary)]", "[&_ul]:my-1 [&_ol]:my-1")
138
+ className: prose.ul
142
139
  }),
143
140
  ol: (p) => /* @__PURE__ */ jsx("ol", {
144
141
  ...dom(p),
145
- className: cn(block, body, "list-decimal pl-6 marker:text-[color:var(--ds-text-tertiary)]", "[&_ul]:my-1 [&_ol]:my-1")
142
+ className: prose.ol
146
143
  }),
147
144
  li: (p) => {
148
145
  const rest = dom(p);
@@ -150,49 +147,46 @@ function buildComponents(codeRenderer) {
150
147
  return /* @__PURE__ */ jsx("li", {
151
148
  ...rest,
152
149
  "data-task": task || void 0,
153
- className: cn("my-1 group-data-[compact]/markdown:my-0.5", task && "-ml-6 list-none")
154
- });
155
- },
156
- input: (p) => {
157
- const rest = dom(p);
158
- return /* @__PURE__ */ jsx("input", {
159
- ...rest,
160
- readOnly: true,
161
- className: cn("mr-2 size-3.5 translate-y-px accent-[var(--ds-primary)] align-baseline", "rounded-[var(--ds-radius-sm)]")
150
+ className: cn(prose.li, task && prose.liTask)
162
151
  });
163
152
  },
153
+ input: (p) => /* @__PURE__ */ jsx("input", {
154
+ ...dom(p),
155
+ readOnly: true,
156
+ className: prose.checkbox
157
+ }),
164
158
  table: (p) => /* @__PURE__ */ jsx("div", {
165
159
  "data-slot": "markdown-table",
166
- className: cn(block, "w-full overflow-x-auto"),
160
+ className: prose.tableWrap,
167
161
  children: /* @__PURE__ */ jsx("table", {
168
162
  ...dom(p),
169
- className: cn("w-full border-collapse text-[length:var(--ds-text-caption-lg)] leading-[var(--ds-text-caption-lg--line-height)] text-[color:var(--ds-foreground)]")
163
+ className: prose.table
170
164
  })
171
165
  }),
172
166
  thead: (p) => /* @__PURE__ */ jsx("thead", {
173
167
  ...dom(p),
174
- className: "bg-[var(--ds-muted)]"
168
+ className: prose.thead
175
169
  }),
176
170
  tbody: (p) => /* @__PURE__ */ jsx("tbody", {
177
171
  ...dom(p),
178
- className: "[&>tr:nth-child(even)]:bg-[var(--ds-muted)]"
172
+ className: prose.tbody
179
173
  }),
180
174
  tr: (p) => /* @__PURE__ */ jsx("tr", { ...dom(p) }),
181
175
  th: (p) => /* @__PURE__ */ jsx("th", {
182
176
  ...dom(p),
183
- className: cn(cellClassName, "text-left font-semibold text-[color:var(--ds-foreground)]")
177
+ className: prose.th
184
178
  }),
185
179
  td: (p) => /* @__PURE__ */ jsx("td", {
186
180
  ...dom(p),
187
- className: cellClassName
181
+ className: prose.td
188
182
  }),
189
183
  hr: (p) => /* @__PURE__ */ jsx("hr", {
190
184
  ...dom(p),
191
- className: "my-5 border-0 border-t border-[var(--ds-border)] group-data-[compact]/markdown:my-3"
185
+ className: prose.hr
192
186
  }),
193
187
  img: (p) => /* @__PURE__ */ jsx("img", {
194
188
  ...dom(p),
195
- className: "my-3 max-w-full rounded-[var(--ds-radius-md)] border border-[var(--ds-border-subtle)]"
189
+ className: prose.img
196
190
  })
197
191
  };
198
192
  }
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.js","names":[],"sources":["../../src/components/markdown.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, Copy } from \"lucide-react\";\nimport * as React from \"react\";\nimport ReactMarkdown, { type Components, type ExtraProps, type Options } from \"react-markdown\";\nimport rehypeSanitize, { defaultSchema, type Options as SanitizeSchema } from \"rehype-sanitize\";\nimport remarkGfm from \"remark-gfm\";\nimport { cn } from \"@/lib/cn\";\nimport { shakeable } from \"@/lib/shakeable\";\n\n/**\n * Markdown.\n *\n * react-markdown with GFM (tables, task lists, strikethrough, autolinks) and\n * rehype-sanitize's GitHub schema, so pasted or model-written HTML cannot run\n * scripts or leak styles. Every element is mapped through `components` onto\n * tokens: headings take the type scale with the display face, body copy is\n * `body-sm`, code is mono on the sunken surface, tables stripe with the\n * border colour and fenced blocks carry a copy button.\n *\n * `compact` halves the vertical rhythm and steps headings down for chat\n * bubbles. There is no syntax highlighting; pass `codeRenderer` to plug one in\n * (shiki, prism) for fenced blocks — it receives the raw text and language.\n */\n\ntype HastNode = NonNullable<ExtraProps[\"node\"]>;\ntype HastChild = HastNode[\"children\"][number];\n\nfunction hastText(node: HastChild | HastNode): string {\n if (node.type === \"text\") return node.value;\n if (\"children\" in node) return node.children.map(hastText).join(\"\");\n return \"\";\n}\n\n/** The GitHub schema, plus `class=\"language-*\"` on code and `checked` on task boxes. */\nconst markdownSanitizeSchema: SanitizeSchema = /* @__PURE__ */ shakeable(() => ({\n ...defaultSchema,\n attributes: {\n ...defaultSchema.attributes,\n code: [...(defaultSchema.attributes?.code ?? []), [\"className\", /^language-./]],\n input: [...(defaultSchema.attributes?.input ?? []), \"checked\"],\n },\n}));\n\nexport interface CodeRendererProps {\n /** The fenced block's text, without a trailing newline. */\n code: string;\n /** From the fence's info string, e.g. `ts` for ```ts. */\n language: string | undefined;\n}\n\n/** Strip react-markdown's `node` so the rest can be spread onto a DOM element. */\nfunction dom<T extends object>(props: T & ExtraProps): T {\n const { node: _node, ...rest } = props;\n return rest as T;\n}\n\nconst block = \"my-3 first:mt-0 last:mb-0 group-data-[compact]/markdown:my-1.5\";\n\nconst body = /* @__PURE__ */ cn(\n \"text-[length:var(--ds-text-body-sm)] leading-[var(--ds-text-body-sm--line-height)] text-[color:var(--ds-foreground)]\",\n);\n\nconst heading = /* @__PURE__ */ cn(\n \"font-[family-name:var(--ds-font-display)] text-[color:var(--ds-foreground)]\",\n \"mt-6 mb-2 first:mt-0 group-data-[compact]/markdown:mt-3 group-data-[compact]/markdown:mb-1\",\n);\n\nconst cellClassName = \"border border-[var(--ds-border)] px-3 py-1.5 align-top\";\n\nfunction CopyButton({ text }: { text: string }) {\n const [copied, setCopied] = React.useState(false);\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), 1500);\n return () => window.clearTimeout(id);\n }, [copied]);\n return (\n <button\n type=\"button\"\n data-slot=\"markdown-copy\"\n data-copied={copied || undefined}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n onClick={() => {\n void navigator.clipboard?.writeText(text).then(() => setCopied(true));\n }}\n className={cn(\n \"absolute top-1.5 right-1.5 inline-flex size-7 items-center justify-center\",\n \"rounded-[var(--ds-radius-sm)] text-[color:var(--ds-muted-foreground)]\",\n \"opacity-0 transition-opacity duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"group-hover/pre:opacity-100 focus-visible:opacity-100 data-[copied]:opacity-100\",\n \"hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n \"outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40\",\n \"data-[copied]:text-[color:var(--ds-success)]\",\n \"[&_svg]:size-3.5\",\n )}\n >\n {copied ? <Check aria-hidden /> : <Copy aria-hidden />}\n </button>\n );\n}\n\nfunction buildComponents(codeRenderer: MarkdownProps[\"codeRenderer\"]): Components {\n return {\n h1: (p) => (\n <h1\n {...dom(p)}\n className={cn(\n heading,\n \"text-[length:var(--ds-text-h1)] leading-[var(--ds-text-h1--line-height)] tracking-[var(--ds-text-h1--letter-spacing)] font-semibold\",\n \"group-data-[compact]/markdown:text-[length:var(--ds-text-h3)] group-data-[compact]/markdown:leading-[var(--ds-text-h3--line-height)]\",\n )}\n />\n ),\n h2: (p) => (\n <h2\n {...dom(p)}\n className={cn(\n heading,\n \"text-[length:var(--ds-text-h2)] leading-[var(--ds-text-h2--line-height)] tracking-[var(--ds-text-h2--letter-spacing)] font-bold\",\n \"group-data-[compact]/markdown:text-[length:var(--ds-text-body-lg)] group-data-[compact]/markdown:leading-[var(--ds-text-body-lg--line-height)]\",\n )}\n />\n ),\n h3: (p) => (\n <h3\n {...dom(p)}\n className={cn(\n heading,\n \"text-[length:var(--ds-text-h3)] leading-[var(--ds-text-h3--line-height)] font-semibold\",\n \"group-data-[compact]/markdown:text-[length:var(--ds-text-body-md)] group-data-[compact]/markdown:leading-[var(--ds-text-body-md--line-height)]\",\n )}\n />\n ),\n h4: (p) => (\n <h4\n {...dom(p)}\n className={cn(\n heading,\n \"font-[family-name:var(--ds-font-body)] text-[length:var(--ds-text-body-md)] leading-[var(--ds-text-body-md--line-height)] font-semibold\",\n )}\n />\n ),\n h5: (p) => (\n <h5\n {...dom(p)}\n className={cn(\n heading,\n \"font-[family-name:var(--ds-font-body)] text-[length:var(--ds-text-body)] leading-[var(--ds-text-body--line-height)] font-semibold\",\n )}\n />\n ),\n h6: (p) => (\n <h6\n {...dom(p)}\n className={cn(\n heading,\n \"font-[family-name:var(--ds-font-body)] text-[length:var(--ds-text-eyebrow)] leading-[var(--ds-text-eyebrow--line-height)] tracking-[var(--ds-text-eyebrow--letter-spacing)] font-semibold uppercase text-[color:var(--ds-muted-foreground)]\",\n )}\n />\n ),\n p: (p) => <p {...dom(p)} className={cn(block, body)} />,\n a: (p) => {\n const { href, ...rest } = dom(p);\n const external = typeof href === \"string\" && /^https?:\\/\\//.test(href);\n return (\n <a\n {...rest}\n href={href}\n target={external ? \"_blank\" : undefined}\n rel={external ? \"noopener noreferrer\" : undefined}\n className={cn(\n \"text-[color:var(--ds-primary)] underline underline-offset-4 decoration-[color:var(--ds-primary)]/40\",\n \"hover:text-[color:var(--ds-primary-hover)] hover:decoration-[color:var(--ds-primary-hover)]\",\n \"outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40 rounded-[var(--ds-radius-sm)]\",\n )}\n />\n );\n },\n strong: (p) => <strong {...dom(p)} className=\"font-semibold text-[color:var(--ds-foreground)]\" />,\n em: (p) => <em {...dom(p)} className=\"italic\" />,\n del: (p) => <del {...dom(p)} className=\"line-through text-[color:var(--ds-muted-foreground)]\" />,\n code: (p) => (\n <code\n {...dom(p)}\n className={cn(\n \"rounded-[var(--ds-radius-sm)] bg-[var(--ds-muted)] px-1 py-0.5\",\n \"font-[family-name:var(--ds-font-mono)] text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-foreground)]\",\n \"border border-[var(--ds-border-subtle)]\",\n )}\n />\n ),\n pre: (p) => {\n const codeNode = p.node?.children.find(\n (c): c is HastNode => c.type === \"element\" && c.tagName === \"code\",\n );\n const source = codeNode ? hastText(codeNode) : hastText(p.node ?? { type: \"text\", value: \"\" });\n const code = source.replace(/\\n$/, \"\");\n const classes = codeNode?.properties.className;\n const language = (Array.isArray(classes) ? classes : [classes])\n .map(String)\n .find((c) => c.startsWith(\"language-\"))\n ?.slice(\"language-\".length);\n return (\n <div\n data-slot=\"markdown-pre\"\n data-language={language}\n className={cn(\n block,\n \"group/pre relative overflow-hidden\",\n \"rounded-[var(--ds-radius-md)] border border-[var(--ds-border-subtle)] bg-[var(--ds-muted)]\",\n )}\n >\n {codeRenderer ? (\n codeRenderer({ code, language })\n ) : (\n <pre\n className={cn(\n \"overflow-x-auto p-3 pr-10\",\n \"font-[family-name:var(--ds-font-mono)] text-[length:var(--ds-text-caption-lg)] leading-[var(--ds-text-caption-lg--line-height)] text-[color:var(--ds-foreground)]\",\n )}\n >\n <code data-language={language}>{code}</code>\n </pre>\n )}\n <CopyButton text={code} />\n </div>\n );\n },\n blockquote: (p) => (\n <blockquote\n {...dom(p)}\n className={cn(\n block,\n \"border-l-2 border-[var(--ds-border-strong)] pl-4 text-[color:var(--ds-muted-foreground)]\",\n \"[&>p]:text-[color:inherit]\",\n )}\n />\n ),\n ul: (p) => (\n <ul\n {...dom(p)}\n className={cn(\n block,\n body,\n \"list-disc pl-6 marker:text-[color:var(--ds-text-tertiary)]\",\n \"[&_ul]:my-1 [&_ol]:my-1\",\n )}\n />\n ),\n ol: (p) => (\n <ol\n {...dom(p)}\n className={cn(\n block,\n body,\n \"list-decimal pl-6 marker:text-[color:var(--ds-text-tertiary)]\",\n \"[&_ul]:my-1 [&_ol]:my-1\",\n )}\n />\n ),\n li: (p) => {\n const rest = dom(p);\n const task = typeof rest.className === \"string\" && rest.className.includes(\"task-list-item\");\n return (\n <li\n {...rest}\n data-task={task || undefined}\n className={cn(\"my-1 group-data-[compact]/markdown:my-0.5\", task && \"-ml-6 list-none\")}\n />\n );\n },\n input: (p) => {\n const rest = dom(p);\n return (\n <input\n {...rest}\n readOnly\n className={cn(\n \"mr-2 size-3.5 translate-y-px accent-[var(--ds-primary)] align-baseline\",\n \"rounded-[var(--ds-radius-sm)]\",\n )}\n />\n );\n },\n table: (p) => (\n <div data-slot=\"markdown-table\" className={cn(block, \"w-full overflow-x-auto\")}>\n <table\n {...dom(p)}\n className={cn(\n \"w-full border-collapse text-[length:var(--ds-text-caption-lg)] leading-[var(--ds-text-caption-lg--line-height)] text-[color:var(--ds-foreground)]\",\n )}\n />\n </div>\n ),\n thead: (p) => <thead {...dom(p)} className=\"bg-[var(--ds-muted)]\" />,\n tbody: (p) => <tbody {...dom(p)} className=\"[&>tr:nth-child(even)]:bg-[var(--ds-muted)]\" />,\n tr: (p) => <tr {...dom(p)} />,\n th: (p) => (\n <th\n {...dom(p)}\n className={cn(cellClassName, \"text-left font-semibold text-[color:var(--ds-foreground)]\")}\n />\n ),\n td: (p) => <td {...dom(p)} className={cellClassName} />,\n hr: (p) => (\n <hr {...dom(p)} className=\"my-5 border-0 border-t border-[var(--ds-border)] group-data-[compact]/markdown:my-3\" />\n ),\n img: (p) => (\n <img\n {...dom(p)}\n className=\"my-3 max-w-full rounded-[var(--ds-radius-md)] border border-[var(--ds-border-subtle)]\"\n />\n ),\n };\n}\n\nexport interface MarkdownProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** The markdown source. */\n children: string | null | undefined;\n /** Tighter rhythm and smaller headings, for chat bubbles. */\n compact?: boolean;\n /** Renders fenced code blocks; the copy button is still added. Use for syntax highlighting. */\n codeRenderer?: (props: CodeRendererProps) => React.ReactNode;\n /** Override or extend the element map. */\n components?: Components;\n /** Extra remark plugins, appended after GFM. */\n remarkPlugins?: NonNullable<Options[\"remarkPlugins\"]>;\n /** Extra rehype plugins, appended after sanitize. */\n rehypePlugins?: NonNullable<Options[\"rehypePlugins\"]>;\n /**\n * Rehype plugins that must run BEFORE sanitize. The one legitimate use is\n * `rehype-raw` in a host that embeds trusted custom tags in markdown (the\n * webchat's `<cortena-*>` blocks): raw HTML has to become nodes before the\n * sanitizer sees it, and the sanitize schema then decides what survives.\n * Anything placed here widens what reaches the sanitizer, so keep the\n * schema strict when you use it.\n */\n rehypePluginsBeforeSanitize?: NonNullable<Options[\"rehypePlugins\"]>;\n /** Replace the sanitize schema; defaults to `markdownSanitizeSchema`. */\n sanitizeSchema?: SanitizeSchema;\n /** Rewrite URLs; defaults to react-markdown's, which drops non-http(s)/mailto/tel schemes. */\n urlTransform?: Options[\"urlTransform\"];\n}\n\nfunction Markdown({\n children,\n compact = false,\n codeRenderer,\n components,\n remarkPlugins,\n rehypePlugins,\n rehypePluginsBeforeSanitize,\n sanitizeSchema = markdownSanitizeSchema,\n urlTransform,\n className,\n ...props\n}: MarkdownProps) {\n const map = React.useMemo(\n () => ({ ...buildComponents(codeRenderer), ...components }),\n [codeRenderer, components],\n );\n const remark = React.useMemo(() => [remarkGfm, ...(remarkPlugins ?? [])], [remarkPlugins]);\n const rehype = React.useMemo(\n () => [...(rehypePluginsBeforeSanitize ?? []), [rehypeSanitize, sanitizeSchema] as const, ...(rehypePlugins ?? [])],\n [rehypePluginsBeforeSanitize, rehypePlugins, sanitizeSchema],\n );\n return (\n <div\n data-slot=\"markdown\"\n data-compact={compact || undefined}\n className={cn(\"group/markdown min-w-0 break-words text-[color:var(--ds-foreground)]\", className)}\n {...props}\n >\n <ReactMarkdown\n remarkPlugins={remark}\n rehypePlugins={rehype as NonNullable<Options[\"rehypePlugins\"]>}\n components={map}\n urlTransform={urlTransform}\n >\n {children ?? \"\"}\n </ReactMarkdown>\n </div>\n );\n}\n\nexport { Markdown, markdownSanitizeSchema };\nexport type { Components as MarkdownComponents };\n"],"mappings":"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"}
1
+ {"version":3,"file":"markdown.js","names":[],"sources":["../../src/components/markdown.tsx"],"sourcesContent":["\"use client\";\n\nimport { Check, Copy } from \"lucide-react\";\nimport * as React from \"react\";\nimport ReactMarkdown, { type Components, type ExtraProps, type Options } from \"react-markdown\";\nimport rehypeSanitize, { defaultSchema, type Options as SanitizeSchema } from \"rehype-sanitize\";\nimport remarkGfm from \"remark-gfm\";\nimport { cn } from \"@/lib/cn\";\nimport { prose } from \"@/lib/prose\";\nimport { shakeable } from \"@/lib/shakeable\";\n\n/**\n * Markdown.\n *\n * react-markdown with GFM (tables, task lists, strikethrough, autolinks) and\n * rehype-sanitize's GitHub schema, so pasted or model-written HTML cannot run\n * scripts or leak styles. Every element is mapped through `components` onto\n * tokens: headings take the type scale with the display face, body copy is\n * `body-sm`, code is mono on the sunken surface, tables stripe with the\n * border colour and fenced blocks carry a copy button.\n *\n * `compact` halves the vertical rhythm and steps headings down for chat\n * bubbles. There is no syntax highlighting; pass `codeRenderer` to plug one in\n * (shiki, prism) for fenced blocks — it receives the raw text and language.\n */\n\ntype HastNode = NonNullable<ExtraProps[\"node\"]>;\ntype HastChild = HastNode[\"children\"][number];\n\nfunction hastText(node: HastChild | HastNode): string {\n if (node.type === \"text\") return node.value;\n if (\"children\" in node) return node.children.map(hastText).join(\"\");\n return \"\";\n}\n\n/** The GitHub schema, plus `class=\"language-*\"` on code and `checked` on task boxes. */\nconst markdownSanitizeSchema: SanitizeSchema = /* @__PURE__ */ shakeable(() => ({\n ...defaultSchema,\n attributes: {\n ...defaultSchema.attributes,\n code: [...(defaultSchema.attributes?.code ?? []), [\"className\", /^language-./]],\n input: [...(defaultSchema.attributes?.input ?? []), \"checked\"],\n },\n}));\n\nexport interface CodeRendererProps {\n /** The fenced block's text, without a trailing newline. */\n code: string;\n /** From the fence's info string, e.g. `ts` for ```ts. */\n language: string | undefined;\n}\n\n/** Strip react-markdown's `node` so the rest can be spread onto a DOM element. */\nfunction dom<T extends object>(props: T & ExtraProps): T {\n const { node: _node, ...rest } = props;\n return rest as T;\n}\n\nfunction CopyButton({ text }: { text: string }) {\n const [copied, setCopied] = React.useState(false);\n React.useEffect(() => {\n if (!copied) return;\n const id = window.setTimeout(() => setCopied(false), 1500);\n return () => window.clearTimeout(id);\n }, [copied]);\n return (\n <button\n type=\"button\"\n data-slot=\"markdown-copy\"\n data-copied={copied || undefined}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n onClick={() => {\n void navigator.clipboard?.writeText(text).then(() => setCopied(true));\n }}\n className={cn(\n \"absolute top-1.5 right-1.5 inline-flex size-7 items-center justify-center\",\n \"rounded-[var(--ds-radius-sm)] text-[color:var(--ds-muted-foreground)]\",\n \"opacity-0 transition-opacity duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"group-hover/pre:opacity-100 focus-visible:opacity-100 data-[copied]:opacity-100\",\n \"hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n \"outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40\",\n \"data-[copied]:text-[color:var(--ds-success)]\",\n \"[&_svg]:size-3.5\",\n )}\n >\n {copied ? <Check aria-hidden /> : <Copy aria-hidden />}\n </button>\n );\n}\n\nfunction buildComponents(codeRenderer: MarkdownProps[\"codeRenderer\"]): Components {\n return {\n h1: (p) => <h1 {...dom(p)} className={prose.h1} />,\n h2: (p) => <h2 {...dom(p)} className={prose.h2} />,\n h3: (p) => <h3 {...dom(p)} className={prose.h3} />,\n h4: (p) => <h4 {...dom(p)} className={prose.h4} />,\n h5: (p) => <h5 {...dom(p)} className={prose.h5} />,\n h6: (p) => <h6 {...dom(p)} className={prose.h6} />,\n p: (p) => <p {...dom(p)} className={prose.p} />,\n a: (p) => {\n const { href, ...rest } = dom(p);\n const external = typeof href === \"string\" && /^https?:\\/\\//.test(href);\n return (\n <a\n {...rest}\n href={href}\n target={external ? \"_blank\" : undefined}\n rel={external ? \"noopener noreferrer\" : undefined}\n className={prose.a}\n />\n );\n },\n strong: (p) => <strong {...dom(p)} className={prose.strong} />,\n em: (p) => <em {...dom(p)} className={prose.em} />,\n del: (p) => <del {...dom(p)} className={prose.del} />,\n code: (p) => <code {...dom(p)} className={prose.code} />,\n pre: (p) => {\n const codeNode = p.node?.children.find(\n (c): c is HastNode => c.type === \"element\" && c.tagName === \"code\",\n );\n const source = codeNode ? hastText(codeNode) : hastText(p.node ?? { type: \"text\", value: \"\" });\n const code = source.replace(/\\n$/, \"\");\n const classes = codeNode?.properties.className;\n const language = (Array.isArray(classes) ? classes : [classes])\n .map(String)\n .find((c) => c.startsWith(\"language-\"))\n ?.slice(\"language-\".length);\n return (\n <div\n data-slot=\"markdown-pre\"\n data-language={language}\n // The frame clips to its own radius; the `<pre>` inside it scrolls.\n className={cn(prose.codeFrame, \"group/pre relative overflow-hidden\")}\n >\n {codeRenderer ? (\n codeRenderer({ code, language })\n ) : (\n <pre className={cn(\"overflow-x-auto p-3 pr-10\", prose.codeText)}>\n <code data-language={language}>{code}</code>\n </pre>\n )}\n <CopyButton text={code} />\n </div>\n );\n },\n blockquote: (p) => <blockquote {...dom(p)} className={prose.blockquote} />,\n ul: (p) => <ul {...dom(p)} className={prose.ul} />,\n ol: (p) => <ol {...dom(p)} className={prose.ol} />,\n li: (p) => {\n const rest = dom(p);\n const task = typeof rest.className === \"string\" && rest.className.includes(\"task-list-item\");\n return (\n <li {...rest} data-task={task || undefined} className={cn(prose.li, task && prose.liTask)} />\n );\n },\n input: (p) => <input {...dom(p)} readOnly className={prose.checkbox} />,\n table: (p) => (\n <div data-slot=\"markdown-table\" className={prose.tableWrap}>\n <table\n {...dom(p)}\n className={prose.table}\n />\n </div>\n ),\n thead: (p) => <thead {...dom(p)} className={prose.thead} />,\n tbody: (p) => <tbody {...dom(p)} className={prose.tbody} />,\n tr: (p) => <tr {...dom(p)} />,\n th: (p) => <th {...dom(p)} className={prose.th} />,\n td: (p) => <td {...dom(p)} className={prose.td} />,\n hr: (p) => <hr {...dom(p)} className={prose.hr} />,\n img: (p) => <img {...dom(p)} className={prose.img} />,\n };\n}\n\nexport interface MarkdownProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n /** The markdown source. */\n children: string | null | undefined;\n /** Tighter rhythm and smaller headings, for chat bubbles. */\n compact?: boolean;\n /** Renders fenced code blocks; the copy button is still added. Use for syntax highlighting. */\n codeRenderer?: (props: CodeRendererProps) => React.ReactNode;\n /** Override or extend the element map. */\n components?: Components;\n /** Extra remark plugins, appended after GFM. */\n remarkPlugins?: NonNullable<Options[\"remarkPlugins\"]>;\n /** Extra rehype plugins, appended after sanitize. */\n rehypePlugins?: NonNullable<Options[\"rehypePlugins\"]>;\n /**\n * Rehype plugins that must run BEFORE sanitize. The one legitimate use is\n * `rehype-raw` in a host that embeds trusted custom tags in markdown (the\n * webchat's `<cortena-*>` blocks): raw HTML has to become nodes before the\n * sanitizer sees it, and the sanitize schema then decides what survives.\n * Anything placed here widens what reaches the sanitizer, so keep the\n * schema strict when you use it.\n */\n rehypePluginsBeforeSanitize?: NonNullable<Options[\"rehypePlugins\"]>;\n /** Replace the sanitize schema; defaults to `markdownSanitizeSchema`. */\n sanitizeSchema?: SanitizeSchema;\n /** Rewrite URLs; defaults to react-markdown's, which drops non-http(s)/mailto/tel schemes. */\n urlTransform?: Options[\"urlTransform\"];\n}\n\nfunction Markdown({\n children,\n compact = false,\n codeRenderer,\n components,\n remarkPlugins,\n rehypePlugins,\n rehypePluginsBeforeSanitize,\n sanitizeSchema = markdownSanitizeSchema,\n urlTransform,\n className,\n ...props\n}: MarkdownProps) {\n const map = React.useMemo(\n () => ({ ...buildComponents(codeRenderer), ...components }),\n [codeRenderer, components],\n );\n const remark = React.useMemo(() => [remarkGfm, ...(remarkPlugins ?? [])], [remarkPlugins]);\n const rehype = React.useMemo(\n () => [...(rehypePluginsBeforeSanitize ?? []), [rehypeSanitize, sanitizeSchema] as const, ...(rehypePlugins ?? [])],\n [rehypePluginsBeforeSanitize, rehypePlugins, sanitizeSchema],\n );\n return (\n <div\n data-slot=\"markdown\"\n data-compact={compact || undefined}\n className={cn(\"group/markdown min-w-0 break-words text-[color:var(--ds-foreground)]\", className)}\n {...props}\n >\n <ReactMarkdown\n remarkPlugins={remark}\n rehypePlugins={rehype as NonNullable<Options[\"rehypePlugins\"]>}\n components={map}\n urlTransform={urlTransform}\n >\n {children ?? \"\"}\n </ReactMarkdown>\n </div>\n );\n}\n\nexport { Markdown, markdownSanitizeSchema };\nexport type { Components as MarkdownComponents };\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { Extension, Extensions, Node } from "@tiptap/core";
3
+ //#region src/components/rich-text-editor/extensions.d.ts
4
+ /** h1 is reserved for the record's own title; h5 and h6 are below the editable scale. */
5
+ export declare const HEADING_LEVELS: readonly [2, 3, 4];
6
+ /**
7
+ * A block the subset cannot represent, carried through untouched.
8
+ *
9
+ * Front matter, an HTML block, a footnote definition, an h1, a maths display.
10
+ * It behaves like a code block — plain text, no marks, newlines allowed — so an
11
+ * author can still edit the raw markdown, and it serialises as exactly its own
12
+ * text with no escaping applied. Dropping these instead would destroy a PRD's
13
+ * front matter on first save.
14
+ */
15
+ export declare const RawBlock: Node<any, any>;
16
+ export interface MentionChipAttributes {
17
+ id: string;
18
+ label: string;
19
+ trigger: string;
20
+ }
21
+ /**
22
+ * A mention, as an inline atom.
23
+ *
24
+ * It serialises as a link on the `mention:` protocol — `[@Priya](mention:u-1)` —
25
+ * so a mention survives a round trip through plain Markdown and through any
26
+ * other tool that reads the file. What `@` and `#` mean is the consumer's
27
+ * business: the component only asks `mentionProvider` for items and inserts
28
+ * what comes back.
29
+ */
30
+ export declare const MentionChip: Node<any, any>;
31
+ export interface RichTextExtensionOptions {
32
+ /** Shown on an empty document. */
33
+ placeholder?: string;
34
+ }
35
+ /**
36
+ * The extension list, and therefore the schema.
37
+ *
38
+ * Pure: `parseMarkdown` normalises documents through the schema this produces,
39
+ * so it must depend on nothing but its options.
40
+ */
41
+ export declare function richTextExtensions(options?: RichTextExtensionOptions): Extensions;
42
+ //#endregion
43
+ //# sourceMappingURL=extensions.d.ts.map