@aphrody/m3-ai 3.3.2 → 3.3.4

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 (48) hide show
  1. package/README.md +7 -7
  2. package/dist/A2APeerInspector.d.ts +41 -0
  3. package/dist/A2APeerInspector.d.ts.map +1 -0
  4. package/dist/A2ATaskTimeline.d.ts +28 -0
  5. package/dist/A2ATaskTimeline.d.ts.map +1 -0
  6. package/dist/AgUiEventStream.d.ts +87 -0
  7. package/dist/AgUiEventStream.d.ts.map +1 -0
  8. package/dist/GeminiAccountMenu.d.ts +55 -0
  9. package/dist/GeminiAccountMenu.d.ts.map +1 -0
  10. package/dist/GeminiPersonalContextEdit.d.ts +29 -0
  11. package/dist/GeminiPersonalContextEdit.d.ts.map +1 -0
  12. package/dist/GoogleSpotlightBar.d.ts +32 -0
  13. package/dist/GoogleSpotlightBar.d.ts.map +1 -0
  14. package/dist/Markdown.d.ts +2 -1
  15. package/dist/Markdown.d.ts.map +1 -1
  16. package/dist/McpAppCard.d.ts +24 -0
  17. package/dist/McpAppCard.d.ts.map +1 -0
  18. package/dist/ModelSelector.d.ts.map +1 -1
  19. package/dist/TaskRow.d.ts.map +1 -1
  20. package/dist/ToolCall.d.ts.map +1 -1
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3629 -1950
  24. package/dist/index.js.map +32 -1
  25. package/package.json +17 -20
  26. package/src/A2APeerInspector.tsx +378 -0
  27. package/src/A2ATaskTimeline.tsx +218 -0
  28. package/src/AgUiEventStream.ts +488 -0
  29. package/src/ChatComposer.tsx +239 -0
  30. package/src/GeminiAccountMenu.tsx +212 -0
  31. package/src/GeminiPersonalContextEdit.tsx +147 -0
  32. package/src/GoogleSpotlightBar.tsx +165 -0
  33. package/src/InlineBranchCard.tsx +97 -0
  34. package/src/InlineWorkerCard.tsx +182 -0
  35. package/src/Markdown.tsx +701 -0
  36. package/src/McpAppCard.tsx +161 -0
  37. package/src/MessageBubble.tsx +95 -0
  38. package/src/ModelSelector.tsx +300 -0
  39. package/src/TaskCreateForm.tsx +123 -0
  40. package/src/TaskDetail.tsx +224 -0
  41. package/src/TaskList.tsx +136 -0
  42. package/src/TaskPriorityIcon.tsx +39 -0
  43. package/src/TaskRow.tsx +177 -0
  44. package/src/TaskStatusIcon.tsx +39 -0
  45. package/src/ToolCall.tsx +639 -0
  46. package/src/index.ts +107 -0
  47. package/src/internal.tsx +107 -0
  48. package/src/types.ts +212 -0
@@ -0,0 +1,701 @@
1
+ // SPDX-License-Identifier: Apache-2.0 AND MIT
2
+ // Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/Markdown.tsx), MIT, rebuilt on Material Design 3.
3
+
4
+ "use client";
5
+
6
+ import { Icon } from "@aphrody/m3-primitives";
7
+ import { clsx } from "clsx";
8
+ import { forwardRef, isValidElement, useEffect, useRef, useState, type ReactNode } from "react";
9
+ import ReactMarkdown, { type Components } from "react-markdown";
10
+ import rehypeRaw from "rehype-raw";
11
+ import remarkGfm from "remark-gfm";
12
+
13
+ type MarkdownSize = "small" | "medium" | "large";
14
+
15
+ interface MarkdownProps {
16
+ content: string;
17
+ className?: string;
18
+ /** Body typescale of paragraphs and lists: `text-body-small|medium|large`. Default `medium`. */
19
+ size?: MarkdownSize;
20
+ /**
21
+ * Render raw HTML embedded in the markdown (rehype-raw). Off by default: only enable it for
22
+ * trusted content.
23
+ */
24
+ allowHtml?: boolean;
25
+ }
26
+
27
+ const bodyClass: Record<MarkdownSize, string> = {
28
+ small: "text-body-small",
29
+ medium: "text-body-medium",
30
+ large: "text-body-large",
31
+ };
32
+
33
+ function extractText(node: ReactNode): string {
34
+ if (typeof node === "string") return node;
35
+ if (typeof node === "number") return String(node);
36
+ if (Array.isArray(node)) return node.map(extractText).join("");
37
+ if (isValidElement(node)) return extractText((node.props as { children?: ReactNode }).children);
38
+ return "";
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Language detection & mapping
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const LANGUAGE_MAP: Record<string, string> = {
46
+ typescript: "TypeScript",
47
+ ts: "TypeScript",
48
+ tsx: "TSX",
49
+ javascript: "JavaScript",
50
+ js: "JavaScript",
51
+ jsx: "JSX",
52
+ python: "Python",
53
+ py: "Python",
54
+ rust: "Rust",
55
+ rs: "Rust",
56
+ bash: "Bash",
57
+ sh: "Bash",
58
+ shell: "Bash",
59
+ zsh: "Bash",
60
+ json: "JSON",
61
+ jsonc: "JSONC",
62
+ sql: "SQL",
63
+ html: "HTML",
64
+ css: "CSS",
65
+ scss: "SCSS",
66
+ sass: "Sass",
67
+ yaml: "YAML",
68
+ yml: "YAML",
69
+ markdown: "Markdown",
70
+ md: "Markdown",
71
+ go: "Go",
72
+ golang: "Go",
73
+ java: "Java",
74
+ c: "C",
75
+ cpp: "C++",
76
+ "c++": "C++",
77
+ csharp: "C#",
78
+ cs: "C#",
79
+ "c#": "C#",
80
+ ruby: "Ruby",
81
+ rb: "Ruby",
82
+ php: "PHP",
83
+ swift: "Swift",
84
+ kotlin: "Kotlin",
85
+ kt: "Kotlin",
86
+ xml: "XML",
87
+ toml: "TOML",
88
+ dockerfile: "Dockerfile",
89
+ docker: "Dockerfile",
90
+ graphql: "GraphQL",
91
+ gql: "GraphQL",
92
+ wasm: "WebAssembly",
93
+ lua: "Lua",
94
+ r: "R",
95
+ diff: "Diff",
96
+ plaintext: "Text",
97
+ text: "Text",
98
+ txt: "Text",
99
+ };
100
+
101
+ export function detectLanguage(raw?: string): { raw: string; label: string } {
102
+ if (!raw) return { raw: "", label: "Code" };
103
+ const cleaned = raw
104
+ .replace(/^language-|^lang-/, "")
105
+ .toLowerCase()
106
+ .trim();
107
+ if (LANGUAGE_MAP[cleaned]) {
108
+ return { raw: cleaned, label: LANGUAGE_MAP[cleaned] };
109
+ }
110
+ const label =
111
+ cleaned.length <= 3
112
+ ? cleaned.toUpperCase()
113
+ : cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
114
+ return { raw: cleaned, label };
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // CodeBlock component
119
+ // ---------------------------------------------------------------------------
120
+
121
+ export interface CodeBlockProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onCopy"> {
122
+ code?: string;
123
+ language?: string;
124
+ onCopy?: (code: string) => void;
125
+ children?: ReactNode;
126
+ }
127
+
128
+ export function CodeBlock({
129
+ code: explicitCode,
130
+ language: explicitLang,
131
+ onCopy,
132
+ className,
133
+ children,
134
+ ...restProps
135
+ }: CodeBlockProps) {
136
+ // Strip node passed by ReactMarkdown to avoid rendering node="[object Object]" in DOM
137
+ const { node: _node, ...props } = restProps as Record<string, any>;
138
+
139
+ const [copied, setCopied] = useState(false);
140
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
141
+
142
+ useEffect(() => {
143
+ return () => {
144
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
145
+ };
146
+ }, []);
147
+
148
+ const childElement = isValidElement(children) ? children : null;
149
+ const childProps = childElement
150
+ ? (childElement.props as { className?: string; children?: ReactNode })
151
+ : null;
152
+
153
+ const rawLangMatch =
154
+ explicitLang ??
155
+ className?.match(/language-([a-zA-Z0-9_+#-]+)/)?.[1] ??
156
+ childProps?.className?.match(/language-([a-zA-Z0-9_+#-]+)/)?.[1];
157
+
158
+ const { raw: rawLang, label: languageLabel } = detectLanguage(rawLangMatch);
159
+
160
+ const rawCode =
161
+ explicitCode ??
162
+ (childProps?.children ? extractText(childProps.children) : extractText(children));
163
+
164
+ const codeContent = rawCode;
165
+ const codeToCopy = rawCode.replace(/\n$/, "");
166
+
167
+ const handleCopy = async () => {
168
+ try {
169
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
170
+ await navigator.clipboard.writeText(codeToCopy);
171
+ }
172
+ } catch {
173
+ // Ignore clipboard API failures in non-secure or test environments
174
+ }
175
+ onCopy?.(codeToCopy);
176
+ setCopied(true);
177
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
178
+ timeoutRef.current = setTimeout(() => {
179
+ setCopied(false);
180
+ }, 2000);
181
+ };
182
+
183
+ return (
184
+ <div
185
+ className={clsx(
186
+ "my-3 overflow-hidden rounded-medium border border-outline-variant bg-surface-container-highest",
187
+ className,
188
+ )}
189
+ {...props}
190
+ >
191
+ <div className="flex items-center justify-between border-b border-outline-variant bg-surface-container px-3 py-1.5">
192
+ <span className="inline-flex items-center rounded-extra-small bg-surface-container-high px-2 py-0.5 text-label-small font-medium text-on-surface-variant border border-outline-variant/60">
193
+ {languageLabel}
194
+ </span>
195
+ <button
196
+ type="button"
197
+ onClick={handleCopy}
198
+ className={clsx(
199
+ "inline-flex items-center gap-1.5 rounded-small px-2 py-1 text-label-small font-medium transition-colors duration-200",
200
+ copied
201
+ ? "text-success"
202
+ : "text-on-surface-variant hover:bg-on-surface/8 hover:text-on-surface active:bg-on-surface/12",
203
+ "focus-visible:outline-2 focus-visible:outline-primary",
204
+ )}
205
+ aria-label={copied ? "Copied" : "Copy code"}
206
+ title={copied ? "Copied!" : "Copy code"}
207
+ >
208
+ <Icon
209
+ name={copied ? "check" : "content_copy"}
210
+ size={16}
211
+ className={copied ? "text-success" : undefined}
212
+ />
213
+ <span>{copied ? "Copied!" : "Copy"}</span>
214
+ </button>
215
+ </div>
216
+ <pre className="overflow-x-auto p-3 font-code text-body-small text-on-surface bg-surface-container-highest [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-[length:inherit]">
217
+ <code className={rawLang ? `language-${rawLang}` : undefined}>{codeContent}</code>
218
+ </pre>
219
+ </div>
220
+ );
221
+ }
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Math formula rendering & typography
225
+ // ---------------------------------------------------------------------------
226
+
227
+ const GREEK_AND_SYMBOLS: Record<string, string> = {
228
+ // Greek letters (lowercase)
229
+ alpha: "α",
230
+ beta: "β",
231
+ gamma: "γ",
232
+ delta: "δ",
233
+ epsilon: "ε",
234
+ zeta: "ζ",
235
+ eta: "η",
236
+ theta: "θ",
237
+ iota: "ι",
238
+ kappa: "κ",
239
+ lambda: "λ",
240
+ mu: "μ",
241
+ nu: "ν",
242
+ xi: "ξ",
243
+ pi: "π",
244
+ rho: "ρ",
245
+ sigma: "σ",
246
+ tau: "τ",
247
+ upsilon: "υ",
248
+ phi: "φ",
249
+ chi: "χ",
250
+ psi: "ψ",
251
+ omega: "ω",
252
+ // Greek letters (uppercase)
253
+ Gamma: "Γ",
254
+ Delta: "Δ",
255
+ Theta: "Θ",
256
+ Lambda: "Λ",
257
+ Xi: "Ξ",
258
+ Pi: "Π",
259
+ Sigma: "Σ",
260
+ Phi: "Φ",
261
+ Psi: "Ψ",
262
+ Omega: "Ω",
263
+ // Operators & Relations
264
+ times: "×",
265
+ div: "÷",
266
+ pm: "±",
267
+ mp: "∓",
268
+ le: "≤",
269
+ leq: "≤",
270
+ ge: "≥",
271
+ geq: "≥",
272
+ approx: "≈",
273
+ neq: "≠",
274
+ ne: "≠",
275
+ infty: "∞",
276
+ cdot: "·",
277
+ circ: "°",
278
+ partial: "∂",
279
+ nabla: "∇",
280
+ sum: "∑",
281
+ prod: "∏",
282
+ int: "∫",
283
+ sqrt: "√",
284
+ in: "∈",
285
+ notin: "∉",
286
+ subset: "⊂",
287
+ subseteq: "⊆",
288
+ cup: "∪",
289
+ cap: "∩",
290
+ to: "→",
291
+ rightarrow: "→",
292
+ leftarrow: "←",
293
+ leftrightarrow: "↔",
294
+ Rightarrow: "⇒",
295
+ Leftarrow: "⇐",
296
+ Leftrightarrow: "⇔",
297
+ forall: "∀",
298
+ exists: "∃",
299
+ };
300
+
301
+ function formatSubSup(text: string): ReactNode {
302
+ const parts = text.split(
303
+ /(\^\{[^}]+\}|\^[-+]?[a-zA-Z0-9\u0370-\u03FF\u2100-\u214F]|_\{[^}]+\}|_[-+]?[a-zA-Z0-9\u0370-\u03FF\u2100-\u214F])/g,
304
+ );
305
+ if (parts.length === 1) return text;
306
+
307
+ const occurrences = new Map<string, number>();
308
+ return parts.map((part) => {
309
+ const occurrence = occurrences.get(part) ?? 0;
310
+ occurrences.set(part, occurrence + 1);
311
+ const key = `${part}:${occurrence}`;
312
+ if (part.startsWith("^")) {
313
+ const val = part.startsWith("^{") && part.endsWith("}") ? part.slice(2, -1) : part.slice(1);
314
+ return (
315
+ <sup key={key} className="text-[0.75em] leading-none">
316
+ {val}
317
+ </sup>
318
+ );
319
+ }
320
+ if (part.startsWith("_")) {
321
+ const val = part.startsWith("_{") && part.endsWith("}") ? part.slice(2, -1) : part.slice(1);
322
+ return (
323
+ <sub key={key} className="text-[0.75em] leading-none">
324
+ {val}
325
+ </sub>
326
+ );
327
+ }
328
+ return part;
329
+ });
330
+ }
331
+
332
+ export function formatMathContent(math: string): ReactNode {
333
+ // Step 1: Replace LaTeX symbol commands \name (or \\name)
334
+ const text = math.replace(/\\+([a-zA-Z]+)/g, (match, name) => {
335
+ return GREEK_AND_SYMBOLS[name] ?? match;
336
+ });
337
+
338
+ // Step 2: Handle \frac{num}{den}
339
+ const fracRegex = /\\+frac\{([^{}]+)\}\{([^{}]+)\}/g;
340
+ if (fracRegex.test(text)) {
341
+ const parts: ReactNode[] = [];
342
+ let lastIndex = 0;
343
+ text.replace(fracRegex, (match, num, den, offset) => {
344
+ if (offset > lastIndex) {
345
+ parts.push(formatSubSup(text.slice(lastIndex, offset)));
346
+ }
347
+ parts.push(
348
+ <span
349
+ key={`frac-${offset}`}
350
+ className="inline-flex flex-col items-center align-middle mx-1 text-center text-[0.85em] leading-none not-italic"
351
+ >
352
+ <span className="border-b border-current px-0.5 pb-0.5">{formatSubSup(num)}</span>
353
+ <span className="px-0.5 pt-0.5">{formatSubSup(den)}</span>
354
+ </span>,
355
+ );
356
+ lastIndex = offset + match.length;
357
+ return match;
358
+ });
359
+ if (lastIndex < text.length) {
360
+ parts.push(formatSubSup(text.slice(lastIndex)));
361
+ }
362
+ return parts;
363
+ }
364
+
365
+ return formatSubSup(text);
366
+ }
367
+
368
+ export interface InlineMathProps extends React.HTMLAttributes<HTMLSpanElement> {
369
+ math: string;
370
+ }
371
+
372
+ export function InlineMath({ math, className, ...props }: InlineMathProps) {
373
+ return (
374
+ <span
375
+ className={clsx(
376
+ "inline-flex items-center px-1 font-serif italic font-medium text-primary",
377
+ className,
378
+ )}
379
+ {...props}
380
+ >
381
+ {formatMathContent(math)}
382
+ </span>
383
+ );
384
+ }
385
+
386
+ export interface BlockMathProps extends React.HTMLAttributes<HTMLDivElement> {
387
+ math: string;
388
+ }
389
+
390
+ export function BlockMath({ math, className, ...props }: BlockMathProps) {
391
+ return (
392
+ <div
393
+ className={clsx(
394
+ "my-3 flex justify-center overflow-x-auto rounded-medium bg-surface-container-low/40 px-4 py-2 text-title-medium font-serif italic font-medium text-primary",
395
+ className,
396
+ )}
397
+ {...props}
398
+ >
399
+ {formatMathContent(math)}
400
+ </div>
401
+ );
402
+ }
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // Remark plugin for recognizing $...$ and $$...$$ expressions
406
+ // ---------------------------------------------------------------------------
407
+
408
+ function transformBlocks(parent: any) {
409
+ if (!parent.children) return;
410
+ const newChildren: any[] = [];
411
+ for (const child of parent.children) {
412
+ if (child.type === "paragraph" && Array.isArray(child.children)) {
413
+ const hasBlockMath = child.children.some(
414
+ (c: any) => c.type === "text" && /\$\$[\s\S]+?\$\$/.test(c.value),
415
+ );
416
+ if (hasBlockMath) {
417
+ let currentParagraphChildren: any[] = [];
418
+ for (const item of child.children) {
419
+ if (item.type === "text" && /\$\$[\s\S]+?\$\$/.test(item.value)) {
420
+ const parts = item.value.split(/(\$\$[\s\S]+?\$\$)/g);
421
+ for (const part of parts) {
422
+ if (part.startsWith("$$") && part.endsWith("$$") && part.length >= 4) {
423
+ if (currentParagraphChildren.length > 0) {
424
+ newChildren.push({
425
+ type: "paragraph",
426
+ children: currentParagraphChildren,
427
+ });
428
+ currentParagraphChildren = [];
429
+ }
430
+ const mathVal = part.slice(2, -2).trim();
431
+ newChildren.push({
432
+ type: "mathBlock",
433
+ data: {
434
+ hName: "div",
435
+ hProperties: { className: "math-block", "data-math": mathVal },
436
+ hChildren: [{ type: "text", value: mathVal }],
437
+ },
438
+ children: [{ type: "text", value: mathVal }],
439
+ });
440
+ } else if (part.trim().length > 0) {
441
+ currentParagraphChildren.push({ type: "text", value: part });
442
+ }
443
+ }
444
+ } else {
445
+ currentParagraphChildren.push(item);
446
+ }
447
+ }
448
+ if (currentParagraphChildren.length > 0) {
449
+ newChildren.push({
450
+ type: "paragraph",
451
+ children: currentParagraphChildren,
452
+ });
453
+ }
454
+ continue;
455
+ }
456
+ }
457
+ transformBlocks(child);
458
+ newChildren.push(child);
459
+ }
460
+ parent.children = newChildren;
461
+ }
462
+
463
+ function transformInlines(node: any) {
464
+ if (!node.children) return;
465
+ const newChildren: any[] = [];
466
+ for (const child of node.children) {
467
+ if (child.type === "text" && typeof child.value === "string" && child.value.includes("$")) {
468
+ const parts = child.value.split(/((?<!\\)\$(?!\s)[^$\n]+?(?<!\s)\$(?!\d))/g);
469
+ for (const part of parts) {
470
+ if (
471
+ part.startsWith("$") &&
472
+ part.endsWith("$") &&
473
+ !part.startsWith("$$") &&
474
+ part.length > 2
475
+ ) {
476
+ const mathVal = part.slice(1, -1).trim();
477
+ newChildren.push({
478
+ type: "mathInline",
479
+ data: {
480
+ hName: "span",
481
+ hProperties: { className: "math-inline", "data-math": mathVal },
482
+ hChildren: [{ type: "text", value: mathVal }],
483
+ },
484
+ children: [{ type: "text", value: mathVal }],
485
+ });
486
+ } else if (part) {
487
+ newChildren.push({ type: "text", value: part });
488
+ }
489
+ }
490
+ } else {
491
+ transformInlines(child);
492
+ newChildren.push(child);
493
+ }
494
+ }
495
+ node.children = newChildren;
496
+ }
497
+
498
+ function transformMathTree(tree: any) {
499
+ transformBlocks(tree);
500
+ transformInlines(tree);
501
+ }
502
+
503
+ export function remarkMathPlugin() {
504
+ return transformMathTree;
505
+ }
506
+
507
+ // ---------------------------------------------------------------------------
508
+ // Element overrides mapping markdown onto M3 typescale and color roles
509
+ // ---------------------------------------------------------------------------
510
+
511
+ function m3Components(size: MarkdownSize): Components {
512
+ const body = bodyClass[size];
513
+ return {
514
+ h1: ({ node: _node, className, children, ...props }) => (
515
+ <h1
516
+ className={clsx("mt-4 mb-2 text-headline-small text-on-surface first:mt-0", className)}
517
+ {...props}
518
+ >
519
+ {children}
520
+ </h1>
521
+ ),
522
+ h2: ({ node: _node, className, children, ...props }) => (
523
+ <h2
524
+ className={clsx("mt-4 mb-2 text-title-large text-on-surface first:mt-0", className)}
525
+ {...props}
526
+ >
527
+ {children}
528
+ </h2>
529
+ ),
530
+ h3: ({ node: _node, className, children, ...props }) => (
531
+ <h3
532
+ className={clsx("mt-3 mb-1.5 text-title-medium text-on-surface first:mt-0", className)}
533
+ {...props}
534
+ >
535
+ {children}
536
+ </h3>
537
+ ),
538
+ h4: ({ node: _node, className, children, ...props }) => (
539
+ <h4
540
+ className={clsx("mt-3 mb-1 text-title-small text-on-surface first:mt-0", className)}
541
+ {...props}
542
+ >
543
+ {children}
544
+ </h4>
545
+ ),
546
+ h5: ({ node: _node, className, children, ...props }) => (
547
+ <h5
548
+ className={clsx("mt-3 mb-1 text-title-small text-on-surface-variant first:mt-0", className)}
549
+ {...props}
550
+ >
551
+ {children}
552
+ </h5>
553
+ ),
554
+ h6: ({ node: _node, className, children, ...props }) => (
555
+ <h6
556
+ className={clsx("mt-3 mb-1 text-label-large text-on-surface-variant first:mt-0", className)}
557
+ {...props}
558
+ >
559
+ {children}
560
+ </h6>
561
+ ),
562
+ p: ({ node: _node, className, ...props }) => (
563
+ <p className={clsx("my-2 first:mt-0 last:mb-0", body, className)} {...props} />
564
+ ),
565
+ a: ({ node: _node, className, children, ...props }) => (
566
+ <a
567
+ className={clsx(
568
+ "text-primary underline underline-offset-2 hover:decoration-2",
569
+ "rounded-extra-small outline-none focus-visible:outline-3 focus-visible:outline-secondary focus-visible:outline-offset-2",
570
+ className,
571
+ )}
572
+ {...props}
573
+ >
574
+ {children}
575
+ </a>
576
+ ),
577
+ strong: ({ node: _node, className, ...props }) => (
578
+ <strong className={clsx("font-semibold text-on-surface", className)} {...props} />
579
+ ),
580
+ ul: ({ node: _node, className, ...props }) => (
581
+ <ul
582
+ className={clsx("my-2 list-disc pl-6 marker:text-on-surface-variant", body, className)}
583
+ {...props}
584
+ />
585
+ ),
586
+ ol: ({ node: _node, className, ...props }) => (
587
+ <ol
588
+ className={clsx("my-2 list-decimal pl-6 marker:text-on-surface-variant", body, className)}
589
+ {...props}
590
+ />
591
+ ),
592
+ li: ({ node: _node, className, ...props }) => (
593
+ <li className={clsx("my-1", className)} {...props} />
594
+ ),
595
+ blockquote: ({ node: _node, className, ...props }) => (
596
+ <blockquote
597
+ className={clsx(
598
+ "my-3 border-l-4 border-outline-variant pl-4 text-on-surface-variant",
599
+ className,
600
+ )}
601
+ {...props}
602
+ />
603
+ ),
604
+ code: ({ node: _node, className, ...props }) => (
605
+ <code
606
+ className={clsx(
607
+ "rounded-extra-small bg-surface-container-highest px-1 py-0.5 font-code text-[0.9em] text-on-surface",
608
+ className,
609
+ )}
610
+ {...props}
611
+ />
612
+ ),
613
+ pre: ({ node: _node, children, className, style }) => (
614
+ <CodeBlock className={className} style={style}>
615
+ {children}
616
+ </CodeBlock>
617
+ ),
618
+ hr: ({ node: _node, className, ...props }) => (
619
+ <hr className={clsx("my-4 border-outline-variant", className)} {...props} />
620
+ ),
621
+ table: ({ node: _node, className, ...props }) => (
622
+ <div className="my-3 overflow-x-auto rounded-small border border-outline-variant">
623
+ <table className={clsx("w-full border-collapse", body, className)} {...props} />
624
+ </div>
625
+ ),
626
+ thead: ({ node: _node, className, ...props }) => (
627
+ <thead className={clsx("bg-surface-container", className)} {...props} />
628
+ ),
629
+ th: ({ node: _node, className, ...props }) => (
630
+ <th
631
+ className={clsx(
632
+ "border-b border-outline-variant px-3 py-2 text-left text-title-small text-on-surface",
633
+ className,
634
+ )}
635
+ {...props}
636
+ />
637
+ ),
638
+ td: ({ node: _node, className, ...props }) => (
639
+ <td
640
+ className={clsx("border-t border-outline-variant px-3 py-2 text-on-surface", className)}
641
+ {...props}
642
+ />
643
+ ),
644
+ img: ({ node: _node, className, alt, ...props }) => (
645
+ <img
646
+ className={clsx("my-2 max-w-full rounded-medium", className)}
647
+ alt={alt ?? ""}
648
+ {...props}
649
+ />
650
+ ),
651
+ input: ({ node: _node, className, ...props }) => (
652
+ <input className={clsx("mr-2 accent-primary", className)} {...props} />
653
+ ),
654
+ div: ({ node: _node, className, children, ...props }) => {
655
+ if (className?.includes("math-block")) {
656
+ const math = (props as Record<string, any>)["data-math"] ?? extractText(children);
657
+ return <BlockMath math={math} className={className} />;
658
+ }
659
+ return (
660
+ <div className={className} {...props}>
661
+ {children}
662
+ </div>
663
+ );
664
+ },
665
+ span: ({ node: _node, className, children, ...props }) => {
666
+ if (className?.includes("math-inline")) {
667
+ const math = (props as Record<string, any>)["data-math"] ?? extractText(children);
668
+ return <InlineMath math={math} className={className} />;
669
+ }
670
+ return (
671
+ <span className={className} {...props}>
672
+ {children}
673
+ </span>
674
+ );
675
+ },
676
+ };
677
+ }
678
+
679
+ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
680
+ ({ content, className, size = "medium", allowHtml = false }, ref) => {
681
+ return (
682
+ <div
683
+ ref={ref}
684
+ className={clsx("markdown min-w-0 text-on-surface", bodyClass[size], className)}
685
+ >
686
+ <ReactMarkdown
687
+ remarkPlugins={[remarkGfm, remarkMathPlugin]}
688
+ rehypePlugins={allowHtml ? [rehypeRaw] : undefined}
689
+ components={m3Components(size)}
690
+ >
691
+ {content}
692
+ </ReactMarkdown>
693
+ </div>
694
+ );
695
+ },
696
+ );
697
+
698
+ Markdown.displayName = "Markdown";
699
+
700
+ export { Markdown };
701
+ export type { MarkdownProps };