@fabrika/canvas-kernel 0.1.44 → 0.2.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 (66) hide show
  1. package/README.md +33 -67
  2. package/index.ts +19 -83
  3. package/package.json +68 -72
  4. package/src/client/AnnotationProvider.tsx +227 -268
  5. package/src/client/AnnotationSidebar.tsx +392 -338
  6. package/src/client/FileIcon.tsx +106 -65
  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 +98 -92
  12. package/src/client/appContext.tsx +40 -40
  13. package/src/client/components/Callout.tsx +20 -20
  14. package/src/client/components/Checklist.tsx +28 -28
  15. package/src/client/components/Choice.tsx +178 -159
  16. package/src/client/components/CodeBlock.tsx +26 -26
  17. package/src/client/components/Diff.tsx +17 -17
  18. package/src/client/components/FilePreview.tsx +99 -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 +592 -542
  22. package/src/client/components/Note.tsx +8 -8
  23. package/src/client/components/Priority.tsx +11 -11
  24. package/src/client/components/ResponseNote.tsx +17 -15
  25. package/src/client/components/Section.tsx +28 -26
  26. package/src/client/components/Table.tsx +34 -29
  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 +34 -34
  34. package/src/client/index.ts +43 -125
  35. package/src/client/preact-all.ts +4 -4
  36. package/src/client/runtime.ts +58 -111
  37. package/src/client/styles.css +1 -74
  38. package/src/engine/compiler.ts +316 -313
  39. package/src/engine/paths.ts +44 -44
  40. package/src/engine/session.ts +623 -618
  41. package/src/engine/watcher.ts +63 -60
  42. package/src/engine/websocket.ts +123 -121
  43. package/annotate.ts +0 -94
  44. package/daemon.ts +0 -46
  45. package/src/client/AnnotationDraftFooter.tsx +0 -76
  46. package/src/client/AnnotationEditor.tsx +0 -320
  47. package/src/client/AnnotationList.tsx +0 -190
  48. package/src/client/Popover.tsx +0 -361
  49. package/src/client/SessionContext.tsx +0 -3
  50. package/src/client/annotationContext.ts +0 -130
  51. package/src/client/annotationMarkdown.ts +0 -102
  52. package/src/client/annotationTarget.ts +0 -73
  53. package/src/client/blockTarget.ts +0 -127
  54. package/src/client/highlightRange.ts +0 -262
  55. package/src/client/popoverPosition.ts +0 -46
  56. package/src/client/regionTarget.ts +0 -180
  57. package/src/client/testing/dom.ts +0 -58
  58. package/src/client/textTarget.ts +0 -31
  59. package/src/client/useRegionAnnotation.tsx +0 -188
  60. package/src/client/useTextAnnotation.tsx +0 -194
  61. package/src/client/utils.ts +0 -33
  62. package/src/engine/channel.ts +0 -278
  63. package/src/engine/http-utils.ts +0 -14
  64. package/src/engine/lifecycle.ts +0 -302
  65. package/src/engine/router.ts +0 -16
  66. package/src/engine/waiter.ts +0 -188
@@ -1,178 +1,197 @@
1
- import React, { useEffect, useMemo, useRef, useState } from "react";
2
- import { useAnnotations, useResponseRegistration } from "#canvas/runtime";
3
- import { ResponseNote } from "./ResponseNote";
1
+ import { useAnnotations, useResponseRegistration } from '#canvas/runtime'
2
+ import React, { useEffect, useMemo, useRef, useState } from 'react'
3
+ import { ResponseNote } from './ResponseNote'
4
4
 
5
- export type ChoiceOption = string | { value: string; label?: string };
5
+ export type ChoiceOption = string | { value: string; label?: string }
6
6
 
7
- type NormalizedOption = { value: string; label: string };
7
+ type NormalizedOption = { value: string; label: string }
8
8
 
9
9
  function normalizeOptions(options: ChoiceOption[]): NormalizedOption[] {
10
- return options.map((opt) =>
11
- typeof opt === "string"
12
- ? { value: opt, label: opt }
13
- : { value: opt.value, label: opt.label ?? opt.value },
14
- );
10
+ return options.map((opt) =>
11
+ typeof opt === 'string'
12
+ ? { value: opt, label: opt }
13
+ : { value: opt.value, label: opt.label ?? opt.value }
14
+ )
15
15
  }
16
16
 
17
17
  /** Radio — pick one option */
18
18
  interface ChoiceProps {
19
- id: string;
20
- label: string;
21
- options: ChoiceOption[];
22
- required?: boolean;
19
+ id: string
20
+ label: string
21
+ options: ChoiceOption[]
22
+ required?: boolean
23
23
  }
24
24
 
25
25
  export function Choice({ id, label, options, required }: ChoiceProps) {
26
- const { responses, setResponse } = useAnnotations();
27
- useResponseRegistration(id);
28
- const normalized = useMemo(() => normalizeOptions(options), [options]);
29
- const current = responses.get(id);
30
- const selected = current?.value as string | undefined;
31
- const note = current?.note || "";
32
- const [showNote, setShowNote] = useState(false);
33
- const wrapperRef = useRef<HTMLDivElement>(null);
34
-
35
- if (!responses.has(id)) {
36
- setResponse(id, { id, type: "radio", label, value: null, options: normalized.map((o) => o.value), required });
37
- }
38
-
39
- const handleSelect = (value: string) => {
40
- setResponse(id, { ...current!, value });
41
- };
42
-
43
- // Keyboard selection via custom event from PlanRenderer
44
- useEffect(() => {
45
- const el = wrapperRef.current;
46
- if (!el) return;
47
- const handler = (e: Event) => {
48
- const target = (e.target as HTMLElement).closest("[data-md-label]");
49
- const labelAttr = target?.getAttribute("data-md-label");
50
- const match = normalized.find((o) => o.label === labelAttr);
51
- if (match) handleSelect(match.value);
52
- };
53
- el.addEventListener("kb-select", handler);
54
- return () => el.removeEventListener("kb-select", handler);
55
- }, [normalized, handleSelect]);
56
-
57
- const showError = current?.required && !selected;
58
-
59
- return (
60
- <div ref={wrapperRef} data-md="choice" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
61
- <div className="flex items-baseline gap-2 mb-2">
62
- <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
63
- {required && <span className="text-[10px] text-accent-red font-body">*</span>}
64
- </div>
65
- <div className="space-y-1">
66
- {normalized.map(({ value, label: optLabel }) => (
67
- <label
68
- key={value}
69
- data-md="choice-option"
70
- data-md-label={optLabel}
71
- onClick={() => handleSelect(value)}
72
- className={`flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 ${
73
- selected === value
74
- ? "bg-highlight-selected"
75
- : "hover:bg-bg-elevated"
76
- }`}
77
- >
78
- <span className={`w-4 h-4 rounded-full border-[1.5px] flex items-center justify-center flex-shrink-0 transition-all ${
79
- selected === value
80
- ? "border-accent-amber bg-accent-amber"
81
- : "border-border-strong"
82
- }`}>
83
- {selected === value && <span className="w-1.5 h-1.5 rounded-full bg-text-inverse" />}
84
- </span>
85
- <span className="text-[13px] font-body text-text-secondary">{optLabel}</span>
86
- </label>
87
- ))}
88
- </div>
89
- <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
90
- {showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select an option.</p>}
91
- </div>
92
- );
26
+ const { responses, setResponse } = useAnnotations()
27
+ useResponseRegistration(id)
28
+ const normalized = useMemo(() => normalizeOptions(options), [options])
29
+ const current = responses.get(id)
30
+ const selected = current?.value as string | undefined
31
+ const note = current?.note || ''
32
+ const [showNote, setShowNote] = useState(false)
33
+ const wrapperRef = useRef<HTMLDivElement>(null)
34
+
35
+ if (!responses.has(id)) {
36
+ setResponse(id, { id, type: 'radio', label, value: null, options: normalized.map((o) => o.value), required })
37
+ }
38
+
39
+ const handleSelect = (value: string) => {
40
+ setResponse(id, { ...current!, value })
41
+ }
42
+
43
+ // Keyboard selection via custom event from PlanRenderer
44
+ useEffect(() => {
45
+ const el = wrapperRef.current
46
+ if (!el) return
47
+ const handler = (e: Event) => {
48
+ const target = (e.target as HTMLElement).closest('[data-md-label]')
49
+ const labelAttr = target?.getAttribute('data-md-label')
50
+ const match = normalized.find((o) => o.label === labelAttr)
51
+ if (match) handleSelect(match.value)
52
+ }
53
+ el.addEventListener('kb-select', handler)
54
+ return () => el.removeEventListener('kb-select', handler)
55
+ }, [normalized, handleSelect])
56
+
57
+ const showError = current?.required && !selected
58
+
59
+ return (
60
+ <div
61
+ ref={wrapperRef}
62
+ data-md="choice"
63
+ data-md-label={label}
64
+ className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input"
65
+ >
66
+ <div className="flex items-baseline gap-2 mb-2">
67
+ <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
68
+ {required && <span className="text-[10px] text-accent-red font-body">*</span>}
69
+ </div>
70
+ <div className="space-y-1">
71
+ {normalized.map(({ value, label: optLabel }) => (
72
+ <label
73
+ key={value}
74
+ data-md="choice-option"
75
+ data-md-label={optLabel}
76
+ onClick={() => handleSelect(value)}
77
+ className={`flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 ${
78
+ selected === value
79
+ ? 'bg-highlight-selected'
80
+ : 'hover:bg-bg-elevated'
81
+ }`}
82
+ >
83
+ <span
84
+ className={`w-4 h-4 rounded-full border-[1.5px] flex items-center justify-center flex-shrink-0 transition-all ${
85
+ selected === value
86
+ ? 'border-accent-amber bg-accent-amber'
87
+ : 'border-border-strong'
88
+ }`}
89
+ >
90
+ {selected === value && <span className="w-1.5 h-1.5 rounded-full bg-text-inverse" />}
91
+ </span>
92
+ <span className="text-[13px] font-body text-text-secondary">{optLabel}</span>
93
+ </label>
94
+ ))}
95
+ </div>
96
+ <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
97
+ {showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select an option.</p>}
98
+ </div>
99
+ )
93
100
  }
94
101
 
95
102
  /** Checkbox — pick multiple options */
96
103
  interface MultiChoiceProps {
97
- id: string;
98
- label: string;
99
- options: ChoiceOption[];
100
- required?: boolean;
104
+ id: string
105
+ label: string
106
+ options: ChoiceOption[]
107
+ required?: boolean
101
108
  }
102
109
 
103
110
  export function MultiChoice({ id, label, options, required }: MultiChoiceProps) {
104
- const { responses, setResponse } = useAnnotations();
105
- useResponseRegistration(id);
106
- const normalized = useMemo(() => normalizeOptions(options), [options]);
107
- const current = responses.get(id);
108
- const selected: string[] = (current?.value as string[]) || [];
109
- const note = current?.note || "";
110
- const [showNote, setShowNote] = useState(false);
111
- const wrapperRef = useRef<HTMLDivElement>(null);
112
-
113
- if (!responses.has(id)) {
114
- setResponse(id, { id, type: "checkbox", label, value: [], options: normalized.map((o) => o.value), required });
115
- }
116
-
117
- const toggle = (value: string) => {
118
- const next = selected.includes(value) ? selected.filter((o) => o !== value) : [...selected, value];
119
- setResponse(id, { ...current!, value: next });
120
- };
121
-
122
- // Keyboard selection via custom event from PlanRenderer
123
- useEffect(() => {
124
- const el = wrapperRef.current;
125
- if (!el) return;
126
- const handler = (e: Event) => {
127
- const target = (e.target as HTMLElement).closest("[data-md-label]");
128
- const labelAttr = target?.getAttribute("data-md-label");
129
- const match = normalized.find((o) => o.label === labelAttr);
130
- if (match) toggle(match.value);
131
- };
132
- el.addEventListener("kb-select", handler);
133
- return () => el.removeEventListener("kb-select", handler);
134
- }, [normalized, toggle]);
135
-
136
- const showError = current?.required && selected.length === 0;
137
-
138
- return (
139
- <div ref={wrapperRef} data-md="multichoice" data-md-label={label} className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input">
140
- <div className="flex items-baseline gap-2 mb-2">
141
- <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
142
- {required && <span className="text-[10px] text-accent-red font-body">*</span>}
143
- </div>
144
- <div className="space-y-1">
145
- {normalized.map(({ value, label: optLabel }) => {
146
- const checked = selected.includes(value);
147
- return (
148
- <label
149
- key={value}
150
- data-md="multichoice-option"
151
- data-md-label={optLabel}
152
- onClick={() => toggle(value)}
153
- className={`flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 ${
154
- checked ? "bg-highlight-selected" : "hover:bg-bg-elevated"
155
- }`}
156
- >
157
- <span className={`w-4 h-4 rounded flex items-center justify-center flex-shrink-0 transition-all ${
158
- checked
159
- ? "bg-accent-amber"
160
- : "border-[1.5px] border-border-strong"
161
- }`}>
162
- {checked && (
163
- <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
164
- <path d="M2.5 5L4.5 7L7.5 3" style={{ stroke: "var(--color-text-inverse)" }} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
165
- </svg>
166
- )}
167
- </span>
168
- <span className="text-[13px] font-body text-text-secondary">{optLabel}</span>
169
- </label>
170
- );
171
- })}
172
- </div>
173
- <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
174
- {showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select at least one option.</p>}
175
- </div>
176
- );
111
+ const { responses, setResponse } = useAnnotations()
112
+ useResponseRegistration(id)
113
+ const normalized = useMemo(() => normalizeOptions(options), [options])
114
+ const current = responses.get(id)
115
+ const selected: string[] = (current?.value as string[]) || []
116
+ const note = current?.note || ''
117
+ const [showNote, setShowNote] = useState(false)
118
+ const wrapperRef = useRef<HTMLDivElement>(null)
119
+
120
+ if (!responses.has(id)) {
121
+ setResponse(id, { id, type: 'checkbox', label, value: [], options: normalized.map((o) => o.value), required })
122
+ }
123
+
124
+ const toggle = (value: string) => {
125
+ const next = selected.includes(value) ? selected.filter((o) => o !== value) : [...selected, value]
126
+ setResponse(id, { ...current!, value: next })
127
+ }
128
+
129
+ // Keyboard selection via custom event from PlanRenderer
130
+ useEffect(() => {
131
+ const el = wrapperRef.current
132
+ if (!el) return
133
+ const handler = (e: Event) => {
134
+ const target = (e.target as HTMLElement).closest('[data-md-label]')
135
+ const labelAttr = target?.getAttribute('data-md-label')
136
+ const match = normalized.find((o) => o.label === labelAttr)
137
+ if (match) toggle(match.value)
138
+ }
139
+ el.addEventListener('kb-select', handler)
140
+ return () => el.removeEventListener('kb-select', handler)
141
+ }, [normalized, toggle])
142
+
143
+ const showError = current?.required && selected.length === 0
144
+
145
+ return (
146
+ <div
147
+ ref={wrapperRef}
148
+ data-md="multichoice"
149
+ data-md-label={label}
150
+ className="-mx-4 px-4 py-3 my-1 rounded-lg transition-colors duration-150 hover:bg-bg-input"
151
+ >
152
+ <div className="flex items-baseline gap-2 mb-2">
153
+ <span className="text-[13px] font-body font-medium text-text-primary">{label}</span>
154
+ {required && <span className="text-[10px] text-accent-red font-body">*</span>}
155
+ </div>
156
+ <div className="space-y-1">
157
+ {normalized.map(({ value, label: optLabel }) => {
158
+ const checked = selected.includes(value)
159
+ return (
160
+ <label
161
+ key={value}
162
+ data-md="multichoice-option"
163
+ data-md-label={optLabel}
164
+ onClick={() => toggle(value)}
165
+ className={`flex items-center gap-2.5 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 ${
166
+ checked ? 'bg-highlight-selected' : 'hover:bg-bg-elevated'
167
+ }`}
168
+ >
169
+ <span
170
+ className={`w-4 h-4 rounded flex items-center justify-center flex-shrink-0 transition-all ${
171
+ checked
172
+ ? 'bg-accent-amber'
173
+ : 'border-[1.5px] border-border-strong'
174
+ }`}
175
+ >
176
+ {checked && (
177
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
178
+ <path
179
+ d="M2.5 5L4.5 7L7.5 3"
180
+ style={{ stroke: 'var(--color-text-inverse)' }}
181
+ strokeWidth="1.5"
182
+ strokeLinecap="round"
183
+ strokeLinejoin="round"
184
+ />
185
+ </svg>
186
+ )}
187
+ </span>
188
+ <span className="text-[13px] font-body text-text-secondary">{optLabel}</span>
189
+ </label>
190
+ )
191
+ })}
192
+ </div>
193
+ <ResponseNote show={showNote} note={note} onToggle={() => setShowNote(!showNote)} onChange={(n) => setResponse(id, { ...current!, note: n })} />
194
+ {showError && <p className="text-[11px] text-accent-red font-body mt-1">Please select at least one option.</p>}
195
+ </div>
196
+ )
177
197
  }
178
-
@@ -1,35 +1,35 @@
1
- import React, { useEffect, useRef } from "react";
1
+ import React, { useEffect, useRef } from 'react'
2
2
 
3
3
  interface CodeBlockProps {
4
- language?: string;
5
- children?: React.ReactNode;
4
+ language?: string
5
+ children?: React.ReactNode
6
6
  }
7
7
 
8
8
  export function CodeBlock({ language, children }: CodeBlockProps) {
9
- const code = typeof children === "string" ? children : String(children ?? "");
10
- const codeRef = useRef<HTMLElement>(null);
9
+ const code = typeof children === 'string' ? children : String(children ?? '')
10
+ const codeRef = useRef<HTMLElement>(null)
11
11
 
12
- useEffect(() => {
13
- const el = codeRef.current;
14
- const hljs = (window as any).hljs;
15
- if (!el || !hljs) return;
16
- // Reset previous highlighting
17
- el.removeAttribute("data-highlighted");
18
- el.className = language ? `language-${language}` : "";
19
- el.textContent = code;
20
- hljs.highlightElement(el);
21
- }, [code, language]);
12
+ useEffect(() => {
13
+ const el = codeRef.current
14
+ const hljs = (window as any).hljs
15
+ if (!el || !hljs) return
16
+ // Reset previous highlighting
17
+ el.removeAttribute('data-highlighted')
18
+ el.className = language ? `language-${language}` : ''
19
+ el.textContent = code
20
+ hljs.highlightElement(el)
21
+ }, [code, language])
22
22
 
23
- return (
24
- <div className="relative mt-3 group/code" data-md="codeblock" data-md-language={language || ""}>
25
- {language && (
26
- <span className="absolute top-2 right-3 text-tiny text-text-tertiary opacity-0 group-hover/code:opacity-60 transition-opacity duration-150 z-10 font-mono">
27
- {language}
28
- </span>
29
- )}
30
- <pre className="bg-bg-code rounded-md p-4 overflow-x-auto text-code font-mono leading-relaxed">
23
+ return (
24
+ <div className="relative mt-3 group/code" data-md="codeblock" data-md-language={language || ''}>
25
+ {language && (
26
+ <span className="absolute top-2 right-3 text-tiny text-text-tertiary opacity-0 group-hover/code:opacity-60 transition-opacity duration-150 z-10 font-mono">
27
+ {language}
28
+ </span>
29
+ )}
30
+ <pre className="bg-bg-code rounded-md p-4 overflow-x-auto text-code font-mono leading-relaxed">
31
31
  <code ref={codeRef} className={language ? `language-${language}` : ""}>{code}</code>
32
- </pre>
33
- </div>
34
- );
32
+ </pre>
33
+ </div>
34
+ )
35
35
  }
@@ -1,23 +1,23 @@
1
- import React from "react";
2
- import { buildDiffLines } from "../diffLines";
1
+ import React from 'react'
2
+ import { buildDiffLines } from '../diffLines'
3
3
 
4
4
  interface DiffProps {
5
- before: string;
6
- after: string;
7
- language?: string;
5
+ before: string
6
+ after: string
7
+ language?: string
8
8
  }
9
9
 
10
10
  export function Diff({ before, after, language }: DiffProps) {
11
- const diffLines = buildDiffLines(before, after);
11
+ const diffLines = buildDiffLines(before, after)
12
12
 
13
- return (
14
- <div className="mt-3 bg-bg-code rounded-md overflow-hidden group/diff" data-md="diff" data-md-language={language || ""}>
15
- {language && (
16
- <div className="px-4 py-2 text-meta font-mono text-text-tertiary opacity-0 group-hover/diff:opacity-60 transition-opacity">
17
- diff ({language})
18
- </div>
19
- )}
20
- <pre className="px-4 pb-3 overflow-x-auto text-code font-mono">
13
+ return (
14
+ <div className="mt-3 bg-bg-code rounded-md overflow-hidden group/diff" data-md="diff" data-md-language={language || ''}>
15
+ {language && (
16
+ <div className="px-4 py-2 text-meta font-mono text-text-tertiary opacity-0 group-hover/diff:opacity-60 transition-opacity">
17
+ diff ({language})
18
+ </div>
19
+ )}
20
+ <pre className="px-4 pb-3 overflow-x-auto text-code font-mono">
21
21
  {diffLines.map((d, i) => (
22
22
  <div key={i} className={
23
23
  d.type === "removed" ? "bg-accent-red-muted text-accent-red" :
@@ -30,7 +30,7 @@ export function Diff({ before, after, language }: DiffProps) {
30
30
  {d.line}
31
31
  </div>
32
32
  ))}
33
- </pre>
34
- </div>
35
- );
33
+ </pre>
34
+ </div>
35
+ )
36
36
  }