@elabs-ai/components-editor 4.0.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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/dist/chunk-LBC5VJBD.js +3748 -0
  4. package/dist/chunk-LBC5VJBD.js.map +1 -0
  5. package/dist/index.css +233 -0
  6. package/dist/index.css.map +1 -0
  7. package/dist/index.d.ts +199 -0
  8. package/dist/index.js +362 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/monaco-environment.d.ts +2 -0
  11. package/dist/lib/monaco-environment.js +44 -0
  12. package/dist/lib/monaco-environment.js.map +1 -0
  13. package/dist/markdown/frontmatter.d.ts +11 -0
  14. package/dist/markdown/frontmatter.js +33 -0
  15. package/dist/markdown/frontmatter.js.map +1 -0
  16. package/dist/markdown/index.css +233 -0
  17. package/dist/markdown/index.css.map +1 -0
  18. package/dist/markdown/index.d.ts +1486 -0
  19. package/dist/markdown/index.js +4882 -0
  20. package/dist/markdown/index.js.map +1 -0
  21. package/dist/markdown/parse.d.ts +25 -0
  22. package/dist/markdown/parse.js +14 -0
  23. package/dist/markdown/parse.js.map +1 -0
  24. package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
  25. package/package.json +101 -0
  26. package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
  27. package/src/ai-objects/ai-objects.stories.tsx +233 -0
  28. package/src/ai-objects/ai-objects.test.tsx +205 -0
  29. package/src/ai-objects/decision-card.tsx +178 -0
  30. package/src/ai-objects/directives.ts +136 -0
  31. package/src/ai-objects/entity.tsx +181 -0
  32. package/src/ai-objects/index.ts +34 -0
  33. package/src/ai-objects/knowledge-card.tsx +143 -0
  34. package/src/calc-block/calc-block.stories.tsx +297 -0
  35. package/src/calc-block/calc-block.test.tsx +164 -0
  36. package/src/calc-block/calc-block.tsx +398 -0
  37. package/src/calc-block/calc-editor-monaco.ts +214 -0
  38. package/src/calc-block/calc-editor-prose.ts +128 -0
  39. package/src/calc-block/calc-editor.css +80 -0
  40. package/src/calc-block/calc-editor.stories.tsx +272 -0
  41. package/src/calc-block/calc-editor.test.ts +228 -0
  42. package/src/calc-block/calc-editor.ts +270 -0
  43. package/src/calc-block/calc-inline.stories.tsx +44 -0
  44. package/src/calc-block/calc-inline.test.tsx +44 -0
  45. package/src/calc-block/calc-inline.tsx +75 -0
  46. package/src/calc-block/index.ts +20 -0
  47. package/src/calc-block/types.ts +164 -0
  48. package/src/code-editor/code-editor.stories.tsx +218 -0
  49. package/src/code-editor/code-editor.test.tsx +134 -0
  50. package/src/code-editor/code-editor.tsx +250 -0
  51. package/src/code-editor/index.ts +6 -0
  52. package/src/code-workspace/code-workspace.stories.tsx +51 -0
  53. package/src/code-workspace/code-workspace.test.tsx +146 -0
  54. package/src/code-workspace/code-workspace.tsx +253 -0
  55. package/src/code-workspace/index.ts +6 -0
  56. package/src/copy-button/copy-button.tsx +51 -0
  57. package/src/copy-button/index.ts +1 -0
  58. package/src/css.d.ts +3 -0
  59. package/src/diff-editor/diff-editor.stories.tsx +44 -0
  60. package/src/diff-editor/diff-editor.test.tsx +68 -0
  61. package/src/diff-editor/diff-editor.tsx +151 -0
  62. package/src/diff-editor/index.ts +1 -0
  63. package/src/editor-context-menu/editor-context-menu.tsx +120 -0
  64. package/src/editor-context-menu/index.ts +1 -0
  65. package/src/editor-toolbar/editor-toolbar.tsx +83 -0
  66. package/src/editor-toolbar/index.ts +1 -0
  67. package/src/index.ts +48 -0
  68. package/src/lib/editor-completions-monaco.test.ts +206 -0
  69. package/src/lib/editor-completions-monaco.ts +127 -0
  70. package/src/lib/editor-completions.test.ts +156 -0
  71. package/src/lib/editor-completions.ts +153 -0
  72. package/src/lib/editor-content-access-prose.test.ts +411 -0
  73. package/src/lib/editor-content-access-prose.ts +172 -0
  74. package/src/lib/editor-content-access.test.ts +182 -0
  75. package/src/lib/editor-content-access.ts +109 -0
  76. package/src/lib/languages.ts +32 -0
  77. package/src/lib/markdown/diff.test.ts +80 -0
  78. package/src/lib/markdown/diff.ts +194 -0
  79. package/src/lib/markdown/directives.ts +342 -0
  80. package/src/lib/markdown/frontmatter.ts +50 -0
  81. package/src/lib/markdown/markdown-scale.test.ts +65 -0
  82. package/src/lib/markdown/markdown-scale.ts +62 -0
  83. package/src/lib/markdown/merge.test.ts +64 -0
  84. package/src/lib/markdown/merge.ts +158 -0
  85. package/src/lib/markdown/slugify.test.ts +156 -0
  86. package/src/lib/markdown/slugify.ts +47 -0
  87. package/src/lib/monaco-environment.ts +103 -0
  88. package/src/lib/monaco-theme-bridge.ts +293 -0
  89. package/src/lib/use-data-theme.ts +61 -0
  90. package/src/markdown/frontmatter.ts +14 -0
  91. package/src/markdown/index.ts +323 -0
  92. package/src/markdown/parse.test.ts +60 -0
  93. package/src/markdown/parse.ts +38 -0
  94. package/src/markdown-academic/citations.test.ts +77 -0
  95. package/src/markdown-academic/citations.tsx +442 -0
  96. package/src/markdown-academic/footnotes.tsx +235 -0
  97. package/src/markdown-academic/index.ts +32 -0
  98. package/src/markdown-academic/math.tsx +163 -0
  99. package/src/markdown-academic/toc.tsx +88 -0
  100. package/src/markdown-editor/completions/completions-menu.tsx +96 -0
  101. package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
  102. package/src/markdown-editor/completions/completions-prose.ts +313 -0
  103. package/src/markdown-editor/completions/completions-widget.tsx +75 -0
  104. package/src/markdown-editor/completions/index.ts +37 -0
  105. package/src/markdown-editor/directive-nodes.ts +151 -0
  106. package/src/markdown-editor/directive-views.test.tsx +189 -0
  107. package/src/markdown-editor/directive-views.tsx +795 -0
  108. package/src/markdown-editor/exit-keymap.test.ts +88 -0
  109. package/src/markdown-editor/exit-keymap.ts +87 -0
  110. package/src/markdown-editor/index.ts +6 -0
  111. package/src/markdown-editor/markdown-editor.css +250 -0
  112. package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
  113. package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
  114. package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
  115. package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
  116. package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
  117. package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
  118. package/src/markdown-editor/markdown-editor.tsx +560 -0
  119. package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
  120. package/src/markdown-editor/milkdown-react/index.ts +8 -0
  121. package/src/markdown-editor/milkdown-react/types.ts +27 -0
  122. package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
  123. package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
  124. package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
  125. package/src/markdown-editor/paste-embed.ts +355 -0
  126. package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
  127. package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
  128. package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
  129. package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
  130. package/src/markdown-editor/slash/index.ts +107 -0
  131. package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
  132. package/src/markdown-editor/slash/insert-directive.ts +345 -0
  133. package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
  134. package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
  135. package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
  136. package/src/markdown-editor/slash/shortcut.test.ts +157 -0
  137. package/src/markdown-editor/slash/shortcut.ts +57 -0
  138. package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
  139. package/src/markdown-editor/slash/slash-menu.tsx +115 -0
  140. package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
  141. package/src/markdown-editor/slash/slash-widget.tsx +97 -0
  142. package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
  143. package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
  144. package/src/markdown-editor/table-view.tsx +249 -0
  145. package/src/markdown-iteration/directive.tsx +92 -0
  146. package/src/markdown-iteration/edit-context.ts +56 -0
  147. package/src/markdown-iteration/index.ts +24 -0
  148. package/src/markdown-iteration/iteration-block.test.tsx +138 -0
  149. package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
  150. package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
  151. package/src/markdown-iteration/iteration-builder.test.ts +252 -0
  152. package/src/markdown-iteration/iteration-builder.ts +303 -0
  153. package/src/markdown-iteration/iteration.test.ts +56 -0
  154. package/src/markdown-iteration/iteration.tsx +333 -0
  155. package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
  156. package/src/markdown-iteration/template-dialog.test.tsx +104 -0
  157. package/src/markdown-iteration/template-dialog.tsx +116 -0
  158. package/src/markdown-outline/document-outline.stories.tsx +76 -0
  159. package/src/markdown-outline/document-outline.tsx +83 -0
  160. package/src/markdown-outline/index.ts +2 -0
  161. package/src/markdown-outline/markdown-outline.test.ts +49 -0
  162. package/src/markdown-outline/markdown-outline.ts +60 -0
  163. package/src/markdown-preview/code-fence.tsx +193 -0
  164. package/src/markdown-preview/index.ts +8 -0
  165. package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
  166. package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
  167. package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
  168. package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
  169. package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
  170. package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
  171. package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
  172. package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
  173. package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
  174. package/src/markdown-preview/markdown-preview.test.tsx +411 -0
  175. package/src/markdown-preview/markdown-preview.tsx +1541 -0
  176. package/src/markdown-toolbar/index.ts +8 -0
  177. package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
  178. package/src/markdown-toolbar/markdown-commands.ts +104 -0
  179. package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
  180. package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
  181. package/src/markdown-workspace/focus-writing.test.ts +52 -0
  182. package/src/markdown-workspace/focus-writing.ts +38 -0
  183. package/src/markdown-workspace/index.ts +6 -0
  184. package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
  185. package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
  186. package/src/markdown-workspace/markdown-workspace.tsx +787 -0
  187. package/src/mermaid-diagram/index.ts +1 -0
  188. package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
  189. package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
  190. package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
  191. package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
  192. package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
  193. package/src/mermaid-diagram/real-parse.test.ts +42 -0
  194. package/src/mermaid-diagram/remediate.test.ts +60 -0
  195. package/src/mermaid-diagram/remediate.ts +124 -0
  196. package/src/mermaid-workspace/index.ts +1 -0
  197. package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
  198. package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
  199. package/src/metric-block/index.ts +1 -0
  200. package/src/metric-block/metric-block.stories.tsx +40 -0
  201. package/src/metric-block/metric-block.tsx +14 -0
  202. package/src/monaco-workers.d.ts +10 -0
  203. package/src/prose/index.ts +14 -0
  204. package/src/prose/prose.stories.tsx +44 -0
  205. package/src/prose/prose.test.tsx +44 -0
  206. package/src/prose/prose.tsx +21 -0
  207. package/src/timeline/index.ts +18 -0
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `markdown-academic` — the opt-in academic layer for `MarkdownPreview`:
3
+ * footnotes, citations + bibliography, math (KaTeX), and a generated TOC. Each
4
+ * piece is enabled by a prop on `MarkdownPreview` (`footnotes` / `resolveCitation`
5
+ * / `math` / `toc`); the standalone components + types below are exported for
6
+ * direct composition and consumer typing.
7
+ *
8
+ * The remark transforms + tag constants are wired internally by `MarkdownPreview`
9
+ * and are intentionally NOT part of the public surface (drive them with the props).
10
+ */
11
+
12
+ // Citations + bibliography.
13
+ export {
14
+ Bibliography,
15
+ collectCitations,
16
+ type BibliographyProps,
17
+ type CitationData,
18
+ type CitationStyle,
19
+ type CiteItem,
20
+ type CollectedCitations,
21
+ type ResolveCitation,
22
+ type ResolvedCitation,
23
+ } from "./citations";
24
+
25
+ // Math (KaTeX) — standalone renderers for direct use.
26
+ export { MathBlock, MathInline, type MathProps } from "./math";
27
+
28
+ // Footnotes — the branded definition section (standalone).
29
+ export { FootnoteList, type FootnoteListProps } from "./footnotes";
30
+
31
+ // Table of contents.
32
+ export { TableOfContents, type TableOfContentsProps } from "./toc";
@@ -0,0 +1,163 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Math via `remark-math` + KaTeX (`$inline$`, and `$$block$$` on its own lines).
5
+ *
6
+ * `remark-math` parses `$…$` / `$$…$$` into `inlineMath` / `math` mdast nodes,
7
+ * which have NO mdast→hast handler (they'd otherwise degrade to literal text). So
8
+ * `remarkBrandMath` rewrites them into our own `brand-math` / `brand-math-inline`
9
+ * elements carrying the raw TeX, and the React renderers turn that into KaTeX.
10
+ *
11
+ * SECURITY: the TeX is untrusted input, so KaTeX runs with `trust: false` (blocks
12
+ * `\href`/`\url`/class injection), a bounded `maxExpand` (caps macro expansion —
13
+ * the `\def`-bomb DoS guard), and `throwOnError: false` (a bad expression renders
14
+ * a contained error, never crashes the page). a11y: `output: "htmlAndMathml"`
15
+ * emits MathML (read by assistive tech) alongside the visual HTML.
16
+ */
17
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
18
+ import katex from "katex";
19
+ import { useMemo, type HTMLAttributes } from "react";
20
+ import { visit } from "unist-util-visit";
21
+
22
+ /** Block math element (`$$…$$`). */
23
+ export const MATH_BLOCK_TAG = "brand-math";
24
+ /** Inline math element (`$…$`). */
25
+ export const MATH_INLINE_TAG = "brand-math-inline";
26
+ /** hast property carrying the raw TeX (rendered as `data-tex`). */
27
+ export const MATH_PROP = "dataTex";
28
+ const MATH_ATTR = "data-tex";
29
+
30
+ /** Cap macro expansion — bounds `\def`-style expansion against untrusted input. */
31
+ const MAX_EXPAND = 1000;
32
+
33
+ interface MdNode {
34
+ type: string;
35
+ value?: string;
36
+ children?: MdNode[];
37
+ data?: { hName?: string; hProperties?: Record<string, unknown> };
38
+ }
39
+
40
+ /**
41
+ * Rewrite `inlineMath` / `math` (from remark-math) into branded elements carrying
42
+ * the raw TeX. Runs AFTER `remarkMath` in the plugin array.
43
+ */
44
+ export function remarkBrandMath() {
45
+ return (tree: unknown) => {
46
+ visit(tree as never, (node: MdNode, index: number | undefined, parent: MdNode | undefined) => {
47
+ if (node.type !== "inlineMath" && node.type !== "math") return;
48
+ if (!parent?.children || index == null) return;
49
+ const display = node.type === "math";
50
+ const tex = typeof node.value === "string" ? node.value : "";
51
+ parent.children[index] = {
52
+ type: display ? "brandMathBlock" : "brandMathInline",
53
+ data: {
54
+ hName: display ? MATH_BLOCK_TAG : MATH_INLINE_TAG,
55
+ hProperties: { [MATH_PROP]: tex },
56
+ },
57
+ };
58
+ });
59
+ };
60
+ }
61
+
62
+ /* ------------------------------------------------------------------ */
63
+ /* React renderers */
64
+ /* ------------------------------------------------------------------ */
65
+
66
+ type TagProps = { node?: unknown; children?: React.ReactNode } & Record<string, unknown>;
67
+
68
+ function readTex(rest: TagProps): string {
69
+ return (rest[MATH_ATTR] as string | undefined) ?? (rest[MATH_PROP] as string | undefined) ?? "";
70
+ }
71
+
72
+ /** Render TeX to a KaTeX HTML string (safe options); never throws. */
73
+ function renderKatex(tex: string, displayMode: boolean): { html: string; error: boolean } {
74
+ try {
75
+ return {
76
+ html: katex.renderToString(tex, {
77
+ displayMode,
78
+ throwOnError: false,
79
+ errorColor: "var(--destructive)",
80
+ trust: false,
81
+ maxExpand: MAX_EXPAND,
82
+ strict: "ignore",
83
+ output: "htmlAndMathml",
84
+ }),
85
+ error: false,
86
+ };
87
+ } catch {
88
+ return { html: "", error: true };
89
+ }
90
+ }
91
+
92
+ export interface MathProps extends Omit<HTMLAttributes<HTMLElement>, "children"> {
93
+ /** Raw TeX source. */
94
+ tex: string;
95
+ }
96
+
97
+ /** Inline math (`$…$`) → KaTeX, in the text flow. */
98
+ export function MathInline({ tex, className, ...props }: MathProps) {
99
+ const { html, error } = useMemo(() => renderKatex(tex, false), [tex]);
100
+ if (error) {
101
+ return (
102
+ <code
103
+ className={cn("text-destructive-text", className)}
104
+ aria-label={`Math (could not render): ${tex}`}
105
+ title="Could not render math"
106
+ {...props}
107
+ >
108
+ {tex}
109
+ </code>
110
+ );
111
+ }
112
+ return (
113
+ <span
114
+ role="math"
115
+ // The raw TeX is a universally-readable fallback name for AT that does not
116
+ // process the embedded MathML; MathML-capable AT reads the MathML instead.
117
+ aria-label={tex}
118
+ className={cn("inline-block align-middle", className)}
119
+ // KaTeX output is generated with trust:false + bounded maxExpand (safe).
120
+ dangerouslySetInnerHTML={{ __html: html }}
121
+ {...props}
122
+ />
123
+ );
124
+ }
125
+
126
+ /** Block math (`$$…$$`) → centered display KaTeX. */
127
+ export function MathBlock({ tex, className, ...props }: MathProps) {
128
+ const { html, error } = useMemo(() => renderKatex(tex, true), [tex]);
129
+ if (error) {
130
+ return (
131
+ <pre
132
+ className={cn(
133
+ "overflow-x-auto rounded-md bg-surface-muted p-3 text-destructive-text",
134
+ className,
135
+ )}
136
+ aria-label={`Math (could not render): ${tex}`}
137
+ title="Could not render math"
138
+ {...props}
139
+ >
140
+ <code>{tex}</code>
141
+ </pre>
142
+ );
143
+ }
144
+ return (
145
+ <div
146
+ role="math"
147
+ aria-label={tex}
148
+ className={cn("my-3 overflow-x-auto text-center", className)}
149
+ dangerouslySetInnerHTML={{ __html: html }}
150
+ {...props}
151
+ />
152
+ );
153
+ }
154
+
155
+ /** Components-map renderer for the inline math element. */
156
+ export function MathInlineTag({ node: _n, children: _c, ...rest }: TagProps) {
157
+ return <MathInline tex={readTex(rest)} />;
158
+ }
159
+
160
+ /** Components-map renderer for the block math element. */
161
+ export function MathBlockTag({ node: _n, children: _c, ...rest }: TagProps) {
162
+ return <MathBlock tex={readTex(rest)} />;
163
+ }
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Generated table of contents — an in-flow `::toc` block.
5
+ *
6
+ * Reuses `parseMarkdownOutline` (the same heading extractor `DocumentOutline`
7
+ * uses — no second parser) for slugs + levels, and renders a quiet nav list of
8
+ * same-page anchor links. `MarkdownPreview` provides the outline through
9
+ * `TocProvider`; it also stamps the matching `id` on each rendered heading
10
+ * (`useHeadingId`) so the links resolve.
11
+ */
12
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
13
+ import { createContext, forwardRef, useContext, type HTMLAttributes } from "react";
14
+
15
+ import type { MarkdownOutlineItem } from "../markdown-outline";
16
+
17
+ // Per-depth indent on the standard spacing scale (statically scannable so Tailwind
18
+ // keeps the classes). Index = heading depth relative to the shallowest in view.
19
+ const INDENT = ["ps-0", "ps-3", "ps-6", "ps-9", "ps-12", "ps-12"] as const;
20
+
21
+ interface TocState {
22
+ items: MarkdownOutlineItem[];
23
+ /** Heading slug keyed by 1-based start line (frontmatter-stripped coords). */
24
+ idByLine: Map<number, string>;
25
+ }
26
+
27
+ const TocContext = createContext<TocState | null>(null);
28
+
29
+ export interface TocProviderProps {
30
+ items: MarkdownOutlineItem[];
31
+ children: React.ReactNode;
32
+ }
33
+
34
+ export function TocProvider({ items, children }: TocProviderProps) {
35
+ const idByLine = new Map<number, string>();
36
+ for (const it of items) idByLine.set(it.line, it.id);
37
+ return <TocContext.Provider value={{ items, idByLine }}>{children}</TocContext.Provider>;
38
+ }
39
+
40
+ /** The heading `id` for a 1-based source line, or `undefined`. */
41
+ export function useHeadingId(line: number | undefined): string | undefined {
42
+ const ctx = useContext(TocContext);
43
+ if (line == null || !ctx) return undefined;
44
+ return ctx.idByLine.get(line);
45
+ }
46
+
47
+ export interface TableOfContentsProps extends Omit<HTMLAttributes<HTMLElement>, "children"> {
48
+ /** Heading outline; defaults to the outline collected by `MarkdownPreview`. */
49
+ items?: MarkdownOutlineItem[];
50
+ /** Section heading label. Default `"Contents"`. */
51
+ title?: string;
52
+ /** Deepest heading level to include (1–6). Default 3. */
53
+ maxLevel?: 1 | 2 | 3 | 4 | 5 | 6;
54
+ }
55
+
56
+ /**
57
+ * Quiet generated TOC. Standalone with an explicit `items` array, or fed from the
58
+ * preview context inside a `::toc` block. Indentation tracks heading depth; no
59
+ * fill or border — the indent + links are the only gestures.
60
+ */
61
+ export const TableOfContents = forwardRef<HTMLElement, TableOfContentsProps>(
62
+ function TableOfContents({ items, title = "Contents", maxLevel = 3, className, ...props }, ref) {
63
+ const ctx = useContext(TocContext);
64
+ const source = items ?? ctx?.items ?? [];
65
+ const list = source.filter((it) => it.level <= maxLevel);
66
+ if (list.length === 0) return null;
67
+
68
+ const minLevel = Math.min(...list.map((it) => it.level));
69
+
70
+ return (
71
+ <nav ref={ref} aria-label={title} className={cn("my-4 text-meta", className)} {...props}>
72
+ <p className="mb-2 font-medium text-muted-foreground">{title}</p>
73
+ <ol className="space-y-1">
74
+ {list.map((it) => (
75
+ <li key={it.id} className={INDENT[Math.min(it.level - minLevel, INDENT.length - 1)]}>
76
+ <a
77
+ href={`#${it.id}`}
78
+ className="text-muted-foreground underline hover:text-foreground hover:underline focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
79
+ >
80
+ {it.text}
81
+ </a>
82
+ </li>
83
+ ))}
84
+ </ol>
85
+ </nav>
86
+ );
87
+ },
88
+ );
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CompletionMenu — the token-styled listbox the WYSIWYG completions widget
5
+ * renders at the caret (`completions-widget.tsx`). Deliberately simpler than
6
+ * `../slash/slash-menu.tsx`'s `SlashMenu`: `EditorCompletionItem` carries only
7
+ * `label`/`detail`/`insertText` (no groups/icons), so there is nothing to group.
8
+ * Same visual grammar (bg-popover, accent selection) for consistency with the
9
+ * slash popup and Monaco's own themed suggest widget.
10
+ */
11
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
12
+ import { forwardRef, type HTMLAttributes } from "react";
13
+
14
+ import type { EditorCompletionItem } from "../../lib/editor-completions";
15
+
16
+ export interface CompletionMenuProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
17
+ /** The candidates to show (already resolved by the provider(s)). */
18
+ items: EditorCompletionItem[];
19
+ /** Index of the highlighted candidate. */
20
+ activeIndex: number;
21
+ /** Called when a candidate is chosen (click or the plugin's Enter/Tab). */
22
+ onSelect: (index: number) => void;
23
+ /** DOM id prefix so each option id is stable + unique. */
24
+ idPrefix?: string;
25
+ /** Shown while no candidates have resolved yet (or none matched). */
26
+ emptyLabel?: string;
27
+ }
28
+
29
+ /** Build the stable DOM id for an option element. */
30
+ export function completionOptionId(idPrefix: string, index: number): string {
31
+ return `${idPrefix}-${index}`;
32
+ }
33
+
34
+ export const CompletionMenu = forwardRef<HTMLDivElement, CompletionMenuProps>(
35
+ function CompletionMenu(
36
+ {
37
+ items,
38
+ activeIndex,
39
+ onSelect,
40
+ idPrefix = "brand-completions",
41
+ emptyLabel = "No suggestions",
42
+ className,
43
+ ...props
44
+ },
45
+ ref,
46
+ ) {
47
+ return (
48
+ <div
49
+ ref={ref}
50
+ role="listbox"
51
+ aria-label="Suggestions"
52
+ className={cn(
53
+ "max-h-[min(280px,50vh)] w-64 overflow-y-auto overflow-x-hidden rounded-md bg-popover p-1 text-popover-foreground shadow-ring-md",
54
+ className,
55
+ )}
56
+ {...props}
57
+ >
58
+ {items.length === 0 ? (
59
+ <div className="px-2 py-3 text-center text-caption text-muted-foreground">
60
+ {emptyLabel}
61
+ </div>
62
+ ) : (
63
+ items.map((item, index) => {
64
+ const selected = index === activeIndex;
65
+ return (
66
+ <div
67
+ key={`${item.label}-${String(index)}`}
68
+ id={completionOptionId(idPrefix, index)}
69
+ role="option"
70
+ aria-selected={selected}
71
+ data-selected={selected ? "true" : undefined}
72
+ // The editor keeps focus — select on mousedown (before the editor
73
+ // would steal focus back), mirroring `slash-menu.tsx`.
74
+ onMouseDown={(e) => {
75
+ e.preventDefault();
76
+ onSelect(index);
77
+ }}
78
+ className={cn(
79
+ "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-body outline-none transition-colors duration-fast",
80
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground",
81
+ )}
82
+ >
83
+ <span className="flex min-w-0 flex-col">
84
+ <span className="truncate">{item.label}</span>
85
+ {item.detail ? (
86
+ <span className="truncate text-meta text-muted-foreground">{item.detail}</span>
87
+ ) : null}
88
+ </span>
89
+ </div>
90
+ );
91
+ })
92
+ )}
93
+ </div>
94
+ );
95
+ },
96
+ );