@fabrika/canvas-kernel 0.1.45 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +33 -67
  2. package/index.ts +20 -83
  3. package/package.json +69 -72
  4. package/src/client/AnnotationProvider.tsx +218 -255
  5. package/src/client/AnnotationSidebar.tsx +393 -338
  6. package/src/client/FileIcon.tsx +105 -66
  7. package/src/client/PlanRenderer.tsx +607 -551
  8. package/src/client/RenderErrorContext.ts +7 -7
  9. package/src/client/ResponsePreview.tsx +192 -173
  10. package/src/client/annotationDom.ts +43 -97
  11. package/src/client/annotationDraft.ts +106 -96
  12. package/src/client/appContext.tsx +40 -40
  13. package/src/client/components/Callout.tsx +20 -20
  14. package/src/client/components/Checklist.tsx +27 -29
  15. package/src/client/components/Choice.tsx +178 -159
  16. package/src/client/components/CodeBlock.tsx +27 -26
  17. package/src/client/components/Diff.tsx +16 -17
  18. package/src/client/components/FilePreview.tsx +102 -74
  19. package/src/client/components/Image.tsx +41 -39
  20. package/src/client/components/Markdown.tsx +33 -32
  21. package/src/client/components/Mermaid.tsx +593 -542
  22. package/src/client/components/Note.tsx +8 -8
  23. package/src/client/components/Priority.tsx +10 -12
  24. package/src/client/components/ResponseNote.tsx +16 -15
  25. package/src/client/components/Section.tsx +28 -26
  26. package/src/client/components/Table.tsx +33 -30
  27. package/src/client/components/Task.tsx +58 -60
  28. package/src/client/components/UserInput.tsx +169 -152
  29. package/src/client/components/index.ts +16 -16
  30. package/src/client/components/renderMarkdownChildren.tsx +86 -68
  31. package/src/client/diffLines.ts +51 -39
  32. package/src/client/generateMarkdown.ts +197 -191
  33. package/src/client/hostContext.tsx +39 -34
  34. package/src/client/index.ts +44 -125
  35. package/src/client/pageGlobals.ts +25 -0
  36. package/src/client/persistenceSchemas.ts +41 -0
  37. package/src/client/preact-all.ts +4 -4
  38. package/src/client/runtime.ts +58 -111
  39. package/src/client/styles.css +1 -74
  40. package/src/engine/compiler.ts +411 -323
  41. package/src/engine/file-admission.ts +25 -0
  42. package/src/engine/paths.ts +44 -44
  43. package/src/engine/session.ts +515 -626
  44. package/src/engine/sessionSchemas.ts +71 -0
  45. package/src/engine/watcher.ts +70 -60
  46. package/src/engine/websocket.ts +134 -132
  47. package/annotate.ts +0 -94
  48. package/daemon.ts +0 -46
  49. package/src/client/AnnotationDraftFooter.tsx +0 -76
  50. package/src/client/AnnotationEditor.tsx +0 -320
  51. package/src/client/AnnotationList.tsx +0 -190
  52. package/src/client/Popover.tsx +0 -361
  53. package/src/client/SessionContext.tsx +0 -3
  54. package/src/client/annotationContext.ts +0 -130
  55. package/src/client/annotationMarkdown.ts +0 -102
  56. package/src/client/annotationTarget.ts +0 -73
  57. package/src/client/blockTarget.ts +0 -127
  58. package/src/client/highlightRange.ts +0 -262
  59. package/src/client/popoverPosition.ts +0 -46
  60. package/src/client/regionTarget.ts +0 -180
  61. package/src/client/testing/dom.ts +0 -74
  62. package/src/client/textTarget.ts +0 -31
  63. package/src/client/useRegionAnnotation.tsx +0 -188
  64. package/src/client/useTextAnnotation.tsx +0 -194
  65. package/src/client/utils.ts +0 -33
  66. package/src/engine/channel.ts +0 -278
  67. package/src/engine/http-utils.ts +0 -14
  68. package/src/engine/lifecycle.ts +0 -302
  69. package/src/engine/router.ts +0 -16
  70. package/src/engine/waiter.ts +0 -188
@@ -1,176 +1,193 @@
1
- import React, { useRef, useState } from "react";
2
- import { useAnnotations, useResponseRegistration } from "#canvas/runtime";
3
- import { ResponseNote } from "./ResponseNote";
4
- import { autoResizeTextarea } from "../utils";
1
+ import { useAnnotations, useResponseRegistration } from '#canvas/runtime'
2
+ import { autoResizeTextarea } from '@fabrika/annotations'
3
+ import { useRef, useState } from 'react'
4
+ import { ResponseNote } from './ResponseNote'
5
5
 
6
6
  /** Free text input from user */
7
7
  interface UserInputProps {
8
- id: string;
9
- label: string;
10
- placeholder?: string;
11
- multiline?: boolean;
12
- required?: boolean;
8
+ id: string
9
+ label: string
10
+ placeholder?: string
11
+ multiline?: boolean
12
+ required?: boolean
13
13
  }
14
14
 
15
15
  export function UserInput({ id, label, placeholder, multiline, required }: UserInputProps) {
16
- const { responses, setResponse } = useAnnotations();
17
- useResponseRegistration(id);
18
- const current = responses.get(id);
19
- const value = (current?.value as string) ?? "";
20
- const note = current?.note || "";
21
- const [showNote, setShowNote] = useState(false);
22
- const ref = useRef<HTMLTextAreaElement>(null);
16
+ const { responses, setResponse } = useAnnotations()
17
+ useResponseRegistration(id)
18
+ const current = responses.get(id)
19
+ const value = (current?.value as string) ?? ''
20
+ const note = current?.note || ''
21
+ const [showNote, setShowNote] = useState(false)
22
+ const ref = useRef<HTMLTextAreaElement>(null)
23
23
 
24
- if (!responses.has(id)) {
25
- setResponse(id, { id, type: "text", label, value: "", required });
26
- }
24
+ if (!responses.has(id)) {
25
+ setResponse(id, { id, type: 'text', label, value: '', required })
26
+ }
27
27
 
28
- const handleChange = (v: string) => {
29
- setResponse(id, { ...current!, value: v });
30
- };
28
+ const handleChange = (v: string) => {
29
+ setResponse(id, { ...current!, value: v })
30
+ }
31
31
 
32
- const autoResize = (el: HTMLTextAreaElement) => autoResizeTextarea(el, multiline ? 60 : 32);
32
+ const autoResize = (el: HTMLTextAreaElement) => autoResizeTextarea(el, multiline ? 60 : 32)
33
33
 
34
- const showError = current?.required && !value.trim();
34
+ const showError = current?.required && !value.trim()
35
35
 
36
- return (
37
- <div data-md="userinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
38
- <div className="flex items-baseline gap-2 mb-2">
39
- <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
40
- {required && <span className="text-[10px] text-accent-red font-body">*</span>}
41
- </div>
42
- <textarea
43
- ref={ref}
44
- value={value}
45
- onChange={(e) => { handleChange(e.currentTarget.value); autoResize(e.currentTarget); }}
46
- placeholder={placeholder || "Type your response..."}
47
- rows={multiline ? 3 : 1}
48
- className="w-full bg-bg-input text-[13px] font-body text-text-primary rounded-lg px-3 py-2 resize-none focus:outline-none border border-border-subtle focus:border-border-hover placeholder:text-text-tertiary transition-colors"
49
- onInput={(e) => autoResize(e.currentTarget)}
50
- onFocus={(e) => autoResize(e.currentTarget)}
51
- />
52
- <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
53
- {showError && <p className="text-[11px] text-accent-red font-body mt-1">This field is required.</p>}
54
- </div>
55
- );
36
+ return (
37
+ <div data-md="userinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
38
+ <div className="flex items-baseline gap-2 mb-2">
39
+ <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
40
+ {required && <span className="text-[10px] text-accent-red font-body">*</span>}
41
+ </div>
42
+ <textarea
43
+ ref={ref}
44
+ value={value}
45
+ onChange={(e) => {
46
+ handleChange(e.currentTarget.value)
47
+ autoResize(e.currentTarget)
48
+ }}
49
+ placeholder={placeholder || 'Type your response...'}
50
+ rows={multiline ? 3 : 1}
51
+ className="w-full bg-bg-input text-[13px] font-body text-text-primary rounded-lg px-3 py-2 resize-none focus:outline-none border border-border-subtle focus:border-border-hover placeholder:text-text-tertiary transition-colors"
52
+ onInput={(e) => autoResize(e.currentTarget)}
53
+ onFocus={(e) => autoResize(e.currentTarget)}
54
+ />
55
+ <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
56
+ {showError && <p className="text-[11px] text-accent-red font-body mt-1">This field is required.</p>}
57
+ </div>
58
+ )
56
59
  }
57
60
 
58
61
  /** Numeric range / slider */
59
62
  interface RangeInputProps {
60
- id: string;
61
- label: string;
62
- min?: number;
63
- max?: number;
64
- step?: number;
65
- required?: boolean;
66
- minLabel?: string;
67
- maxLabel?: string;
63
+ id: string
64
+ label: string
65
+ min?: number
66
+ max?: number
67
+ step?: number
68
+ required?: boolean
69
+ minLabel?: string
70
+ maxLabel?: string
68
71
  }
69
72
 
70
73
  export function RangeInput({ id, label, min = 1, max = 10, step = 1, required, minLabel, maxLabel }: RangeInputProps) {
71
- const { responses, setResponse } = useAnnotations();
72
- useResponseRegistration(id);
73
- const current = responses.get(id);
74
- const value = current?.value as number | null;
75
- const note = current?.note || "";
76
- const [showNote, setShowNote] = useState(false);
74
+ const { responses, setResponse } = useAnnotations()
75
+ useResponseRegistration(id)
76
+ const current = responses.get(id)
77
+ const value = current?.value as number | null
78
+ const note = current?.note || ''
79
+ const [showNote, setShowNote] = useState(false)
77
80
 
78
- if (!responses.has(id)) {
79
- setResponse(id, { id, type: "range", label, value: null, required });
80
- }
81
+ if (!responses.has(id)) {
82
+ setResponse(id, { id, type: 'range', label, value: null, required })
83
+ }
81
84
 
82
- const handleChange = (v: number) => {
83
- setResponse(id, { ...current!, value: v });
84
- };
85
+ const handleChange = (v: number) => {
86
+ setResponse(id, { ...current!, value: v })
87
+ }
85
88
 
86
- const hasValue = value !== null && value !== undefined;
87
- const displayValue = hasValue ? value : Math.round((min + max) / 2);
88
- const pct = ((displayValue - min) / (max - min)) * 100;
89
- const stepCount = Math.round((max - min) / step);
90
- const showDots = stepCount <= 20;
89
+ const hasValue = value !== null && value !== undefined
90
+ const displayValue = hasValue ? value : Math.round((min + max) / 2)
91
+ const pct = ((displayValue - min) / (max - min)) * 100
92
+ const stepCount = Math.round((max - min) / step)
93
+ const showDots = stepCount <= 20
91
94
 
92
- const showError = current?.required && !hasValue;
95
+ const showError = current?.required && !hasValue
93
96
 
94
- return (
95
- <div data-md="rangeinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
96
- <div className="flex items-baseline justify-between mb-1">
97
- <div className="flex items-baseline gap-2">
98
- <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
99
- {required && <span className="text-[10px] text-accent-red font-body">*</span>}
100
- </div>
101
- <span className="flex items-center gap-1.5">
102
- <span className={`text-[13px] font-mono ${hasValue ? "text-accent-amber" : "text-text-disabled"}`}>
103
- {hasValue ? value : "—"}
104
- </span>
105
- {hasValue && (
106
- <button
107
- onClick={() => setResponse(id, { ...current!, value: null })}
108
- className="text-text-disabled hover:text-text-tertiary transition-colors text-[10px] leading-none"
109
- title="Clear"
110
- >
111
- &#x2715;
112
- </button>
113
- )}
114
- </span>
115
- </div>
116
- <div className="relative h-6 flex items-center">
117
- {/* Track background */}
118
- <div className="absolute left-0 right-0 h-1.5 rounded-full bg-border-medium">
119
- {hasValue && <div className="h-full rounded-full bg-accent-amber transition-all duration-150" style={{ width: `${pct}%` }} />}
120
- </div>
121
- {/* Discrete step dots */}
122
- {showDots && Array.from({ length: stepCount + 1 }, (_, i) => {
123
- const dotVal = min + i * step;
124
- const dotPct = ((dotVal - min) / (max - min)) * 100;
125
- const isSelected = hasValue && dotVal <= value!;
126
- const isActive = hasValue && dotVal === value;
127
- return (
128
- <div
129
- key={i}
130
- className={`absolute rounded-full pointer-events-none transition-all duration-150 ${
131
- isActive ? "w-2.5 h-2.5 bg-accent-amber" : isSelected ? "w-1.5 h-1.5 bg-accent-amber" : "w-1.5 h-1.5 bg-border-hover"
132
- }`}
133
- style={{ left: `${dotPct}%`, transform: "translate(-50%, -50%)", top: "50%" }}
134
- />
135
- );
136
- })}
137
- {/* Thumb indicator */}
138
- {hasValue ? (
139
- <div className="absolute h-5 w-5 rounded-full bg-accent-amber shadow-md pointer-events-none transition-all duration-150" style={{ left: `calc(${pct}% - 10px)`, borderWidth: "2px", borderStyle: "solid", borderColor: "var(--color-bg-base)" }} />
140
- ) : (
141
- <div className="absolute h-4 w-4 rounded-full bg-border-hover pointer-events-none opacity-50" style={{ left: "calc(50% - 8px)", borderWidth: "2px", borderStyle: "solid", borderColor: "var(--color-bg-base)" }} />
142
- )}
143
- <input
144
- type="range"
145
- min={min}
146
- max={max}
147
- step={step}
148
- value={displayValue}
149
- onChange={(e) => handleChange(Number(e.currentTarget.value))}
150
- className="absolute inset-0 w-full opacity-0 cursor-pointer"
151
- />
152
- </div>
153
- {/* Labels: show step values if few enough, otherwise just min/max */}
154
- {showDots && stepCount <= 10 ? (
155
- <div className="flex justify-between -mt-0.5 px-0">
156
- {Array.from({ length: stepCount + 1 }, (_, i) => {
157
- const dotVal = min + i * step;
158
- const isActive = hasValue && dotVal === value;
159
- return (
160
- <span key={i} className={`text-[10px] font-mono transition-colors ${isActive ? "text-accent-amber font-medium" : "text-text-disabled"}`} style={{ minWidth: "1ch", textAlign: "center" }}>
161
- {dotVal}
162
- </span>
163
- );
164
- })}
165
- </div>
166
- ) : (
167
- <div className="flex justify-between -mt-0.5">
168
- <span className="text-[11px] text-text-tertiary font-body">{minLabel || min}</span>
169
- <span className="text-[11px] text-text-tertiary font-body">{maxLabel || max}</span>
170
- </div>
171
- )}
172
- <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
173
- {showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select a value.</p>}
174
- </div>
175
- );
97
+ return (
98
+ <div data-md="rangeinput" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
99
+ <div className="flex items-baseline justify-between mb-1">
100
+ <div className="flex items-baseline gap-2">
101
+ <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
102
+ {required && <span className="text-[10px] text-accent-red font-body">*</span>}
103
+ </div>
104
+ <span className="flex items-center gap-1.5">
105
+ <span className={`text-[13px] font-mono ${hasValue ? 'text-accent-amber' : 'text-text-disabled'}`}>
106
+ {hasValue ? value : '—'}
107
+ </span>
108
+ {hasValue && (
109
+ <button
110
+ onClick={() => setResponse(id, { ...current!, value: null })}
111
+ className="text-text-disabled hover:text-text-tertiary transition-colors text-[10px] leading-none"
112
+ title="Clear"
113
+ >
114
+ &#x2715;
115
+ </button>
116
+ )}
117
+ </span>
118
+ </div>
119
+ <div className="relative h-6 flex items-center">
120
+ {/* Track background */}
121
+ <div className="absolute left-0 right-0 h-1.5 rounded-full bg-border-medium">
122
+ {hasValue && <div className="h-full rounded-full bg-accent-amber transition-all duration-150" style={{ width: `${pct}%` }} />}
123
+ </div>
124
+ {/* Discrete step dots */}
125
+ {showDots && Array.from({ length: stepCount + 1 }, (_, i) => {
126
+ const dotVal = min + i * step
127
+ const dotPct = ((dotVal - min) / (max - min)) * 100
128
+ const isSelected = hasValue && dotVal <= value!
129
+ const isActive = hasValue && dotVal === value
130
+ return (
131
+ <div
132
+ key={i}
133
+ className={`absolute rounded-full pointer-events-none transition-all duration-150 ${
134
+ isActive ? 'w-2.5 h-2.5 bg-accent-amber' : isSelected ? 'w-1.5 h-1.5 bg-accent-amber' : 'w-1.5 h-1.5 bg-border-hover'
135
+ }`}
136
+ style={{ left: `${dotPct}%`, transform: 'translate(-50%, -50%)', top: '50%' }}
137
+ />
138
+ )
139
+ })}
140
+ {/* Thumb indicator */}
141
+ {hasValue
142
+ ? (
143
+ <div
144
+ className="absolute h-5 w-5 rounded-full bg-accent-amber shadow-md pointer-events-none transition-all duration-150"
145
+ style={{ left: `calc(${pct}% - 10px)`, borderWidth: '2px', borderStyle: 'solid', borderColor: 'var(--color-bg-base)' }}
146
+ />
147
+ )
148
+ : (
149
+ <div
150
+ className="absolute h-4 w-4 rounded-full bg-border-hover pointer-events-none opacity-50"
151
+ style={{ left: 'calc(50% - 8px)', borderWidth: '2px', borderStyle: 'solid', borderColor: 'var(--color-bg-base)' }}
152
+ />
153
+ )}
154
+ <input
155
+ type="range"
156
+ min={min}
157
+ max={max}
158
+ step={step}
159
+ value={displayValue}
160
+ onChange={(e) => handleChange(Number(e.currentTarget.value))}
161
+ className="absolute inset-0 w-full opacity-0 cursor-pointer"
162
+ />
163
+ </div>
164
+ {/* Labels: show step values if few enough, otherwise just min/max */}
165
+ {showDots && stepCount <= 10
166
+ ? (
167
+ <div className="flex justify-between -mt-0.5 px-0">
168
+ {Array.from({ length: stepCount + 1 }, (_, i) => {
169
+ const dotVal = min + i * step
170
+ const isActive = hasValue && dotVal === value
171
+ return (
172
+ <span
173
+ key={i}
174
+ className={`text-[10px] font-mono transition-colors ${isActive ? 'text-accent-amber font-medium' : 'text-text-disabled'}`}
175
+ style={{ minWidth: '1ch', textAlign: 'center' }}
176
+ >
177
+ {dotVal}
178
+ </span>
179
+ )
180
+ })}
181
+ </div>
182
+ )
183
+ : (
184
+ <div className="flex justify-between -mt-0.5">
185
+ <span className="text-[11px] text-text-tertiary font-body">{minLabel || min}</span>
186
+ <span className="text-[11px] text-text-tertiary font-body">{maxLabel || max}</span>
187
+ </div>
188
+ )}
189
+ <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
190
+ {showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select a value.</p>}
191
+ </div>
192
+ )
176
193
  }
@@ -1,18 +1,18 @@
1
1
  // Components every canvas host provides. Hosts that ship extra components
2
2
  // compose their own `#canvas/components` barrel over this one.
3
- export { Section } from "./Section";
4
- export { Item, Task } from "./Task";
5
- export { CodeBlock } from "./CodeBlock";
6
- export { Callout } from "./Callout";
7
- export { Note } from "./Note";
8
- export { Table } from "./Table";
9
- export { Checklist } from "./Checklist";
10
- export { Priority } from "./Priority";
11
- export { FilePreview } from "./FilePreview";
12
- export { Mermaid } from "./Mermaid";
13
- export { Diff } from "./Diff";
14
- export { Choice, MultiChoice } from "./Choice";
15
- export { UserInput, RangeInput } from "./UserInput";
16
- export { ImageView } from "./Image";
17
- export { Markdown } from "./Markdown";
18
- export { useFeedback } from "#canvas/runtime";
3
+ export { useFeedback } from '#canvas/runtime'
4
+ export { Callout } from './Callout'
5
+ export { Checklist } from './Checklist'
6
+ export { Choice, MultiChoice } from './Choice'
7
+ export { CodeBlock } from './CodeBlock'
8
+ export { Diff } from './Diff'
9
+ export { FilePreview } from './FilePreview'
10
+ export { ImageView } from './Image'
11
+ export { Markdown } from './Markdown'
12
+ export { Mermaid } from './Mermaid'
13
+ export { Note } from './Note'
14
+ export { Priority } from './Priority'
15
+ export { Section } from './Section'
16
+ export { Table } from './Table'
17
+ export { Item, Task } from './Task'
18
+ export { RangeInput, UserInput } from './UserInput'
@@ -1,45 +1,63 @@
1
- import React from "react";
2
- import { marked } from "marked";
1
+ // biome-ignore-all lint/security/noDangerouslySetInnerHtml: Canvas modules already execute as code in this document.
2
+ import { marked } from 'marked'
3
+ import React from 'react'
3
4
 
4
- marked.setOptions({ gfm: true, breaks: false });
5
+ marked.setOptions({ gfm: true, breaks: false })
5
6
 
6
7
  /** Host tags that flow inline and must not break a prose run into separate blocks. */
7
8
  const INLINE_TAGS = new Set([
8
- "a", "abbr", "b", "br", "code", "del", "em", "i", "kbd", "mark",
9
- "q", "s", "small", "span", "strong", "sub", "sup", "time", "u",
10
- ]);
9
+ 'a',
10
+ 'abbr',
11
+ 'b',
12
+ 'br',
13
+ 'code',
14
+ 'del',
15
+ 'em',
16
+ 'i',
17
+ 'kbd',
18
+ 'mark',
19
+ 'q',
20
+ 's',
21
+ 'small',
22
+ 'span',
23
+ 'strong',
24
+ 'sub',
25
+ 'sup',
26
+ 'time',
27
+ 'u',
28
+ ])
11
29
 
12
30
  /** Custom components that render inline (e.g. <Priority>). */
13
- const INLINE_COMPONENTS = new Set(["Priority"]);
31
+ const INLINE_COMPONENTS = new Set(['Priority'])
14
32
 
15
33
  function isInlineNode(node: React.ReactNode): boolean {
16
- if (typeof node === "string" || typeof node === "number") return true;
17
- if (!React.isValidElement(node)) return false;
18
- // Read `type` structurally: preact/compat's isValidElement is not a type
19
- // guard, so the narrowing React's typings give here does not survive there.
20
- if (typeof node !== "object" || node === null || !("type" in node)) return false;
21
- const type: unknown = node.type;
22
- if (typeof type === "string") return INLINE_TAGS.has(type);
23
- if (typeof type === "function") {
24
- const name = "displayName" in type && typeof type.displayName === "string" ? type.displayName : type.name;
25
- return name ? INLINE_COMPONENTS.has(name) : false;
26
- }
27
- return false;
34
+ if (typeof node === 'string' || typeof node === 'number') return true
35
+ if (!React.isValidElement(node)) return false
36
+ // Read `type` structurally: preact/compat's isValidElement is not a type
37
+ // guard, so the narrowing React's typings give here does not survive there.
38
+ if (typeof node !== 'object' || node === null || !('type' in node)) return false
39
+ const type: unknown = node.type
40
+ if (typeof type === 'string') return INLINE_TAGS.has(type)
41
+ if (typeof type === 'function') {
42
+ const name = 'displayName' in type && typeof type.displayName === 'string' ? type.displayName : type.name
43
+ return name ? INLINE_COMPONENTS.has(name) : false
44
+ }
45
+ return false
28
46
  }
29
47
 
30
48
  function renderTextAsMarkdown(text: string, key: string): React.ReactNode {
31
- const trimmed = text.trim();
32
- if (!trimmed) return null;
33
- const html = marked.parse(trimmed, { async: false }) as string;
34
- return (
35
- <div
36
- key={key}
37
- className="prose-canvas"
38
- data-md="markdown"
39
- data-md-source={trimmed}
40
- dangerouslySetInnerHTML={{ __html: html }}
41
- />
42
- );
49
+ const trimmed = text.trim()
50
+ if (!trimmed) return null
51
+ const html = marked.parse(trimmed, { async: false }) as string
52
+ return (
53
+ <div
54
+ key={key}
55
+ className="prose-canvas"
56
+ data-md="markdown"
57
+ data-md-source={trimmed}
58
+ dangerouslySetInnerHTML={{ __html: html }}
59
+ />
60
+ )
43
61
  }
44
62
 
45
63
  /**
@@ -48,17 +66,17 @@ function renderTextAsMarkdown(text: string, key: string): React.ReactNode {
48
66
  * No `data-md` attribute so the markdown exporter treats it as an inline container.
49
67
  */
50
68
  function renderInlineRun(run: React.ReactNode[], key: string): React.ReactNode {
51
- return (
52
- <div key={key} className="prose-canvas">
53
- {run.map((node, i) => {
54
- if (typeof node === "string" || typeof node === "number") {
55
- const html = marked.parseInline(String(node), { async: false }) as string;
56
- return <span key={i} dangerouslySetInnerHTML={{ __html: html }} />;
57
- }
58
- return node;
59
- })}
60
- </div>
61
- );
69
+ return (
70
+ <div key={key} className="prose-canvas">
71
+ {run.map((node, i) => {
72
+ if (typeof node === 'string' || typeof node === 'number') {
73
+ const html = marked.parseInline(String(node), { async: false }) as string
74
+ return <span key={i} dangerouslySetInnerHTML={{ __html: html }} />
75
+ }
76
+ return node
77
+ })}
78
+ </div>
79
+ )
62
80
  }
63
81
 
64
82
  /**
@@ -68,33 +86,33 @@ function renderInlineRun(run: React.ReactNode[], key: string): React.ReactNode {
68
86
  * keeps flowing instead of stacking one fragment per line.
69
87
  */
70
88
  export function renderMarkdownChildren(children: React.ReactNode): React.ReactNode {
71
- const kids = React.Children.toArray(children);
72
- if (kids.length === 0) return null;
89
+ const kids = React.Children.toArray(children)
90
+ if (kids.length === 0) return null
73
91
 
74
- const result: React.ReactNode[] = [];
75
- let segment: React.ReactNode[] = [];
76
- let segIdx = 0;
92
+ const result: React.ReactNode[] = []
93
+ let segment: React.ReactNode[] = []
94
+ let segIdx = 0
77
95
 
78
- const flush = () => {
79
- if (segment.length === 0) return;
80
- const hasInlineJsx = segment.some((n) => typeof n !== "string" && typeof n !== "number");
81
- if (hasInlineJsx) {
82
- result.push(renderInlineRun(segment, `md-${segIdx++}`));
83
- } else {
84
- const node = renderTextAsMarkdown(segment.join(""), `md-${segIdx++}`);
85
- if (node) result.push(node);
86
- }
87
- segment = [];
88
- };
96
+ const flush = () => {
97
+ if (segment.length === 0) return
98
+ const hasInlineJsx = segment.some((n) => typeof n !== 'string' && typeof n !== 'number')
99
+ if (hasInlineJsx) {
100
+ result.push(renderInlineRun(segment, `md-${segIdx++}`))
101
+ } else {
102
+ const node = renderTextAsMarkdown(segment.join(''), `md-${segIdx++}`)
103
+ if (node) result.push(node)
104
+ }
105
+ segment = []
106
+ }
89
107
 
90
- for (const child of kids) {
91
- if (isInlineNode(child)) {
92
- segment.push(child);
93
- } else {
94
- flush();
95
- result.push(child);
96
- }
97
- }
98
- flush();
99
- return result;
108
+ for (const child of kids) {
109
+ if (isInlineNode(child)) {
110
+ segment.push(child)
111
+ } else {
112
+ flush()
113
+ result.push(child)
114
+ }
115
+ }
116
+ flush()
117
+ return result
100
118
  }