@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,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 { 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,36 @@
1
- import React, { useEffect, useRef } from "react";
1
+ import React, { useEffect, useRef } from 'react'
2
+ import { pageHighlighter } from '../pageGlobals'
2
3
 
3
4
  interface CodeBlockProps {
4
- language?: string;
5
- children?: React.ReactNode;
5
+ language?: string
6
+ children?: React.ReactNode
6
7
  }
7
8
 
8
9
  export function CodeBlock({ language, children }: CodeBlockProps) {
9
- const code = typeof children === "string" ? children : String(children ?? "");
10
- const codeRef = useRef<HTMLElement>(null);
10
+ const code = typeof children === 'string' ? children : String(children ?? '')
11
+ const codeRef = useRef<HTMLElement>(null)
11
12
 
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]);
13
+ useEffect(() => {
14
+ const el = codeRef.current
15
+ const hljs = pageHighlighter()
16
+ if (!el || !hljs) return
17
+ // Reset previous highlighting
18
+ el.removeAttribute('data-highlighted')
19
+ el.className = language ? `language-${language}` : ''
20
+ el.textContent = code
21
+ hljs.highlightElement(el)
22
+ }, [code, language])
22
23
 
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">
24
+ return (
25
+ <div className="relative mt-3 group/code" data-md="codeblock" data-md-language={language || ''}>
26
+ {language && (
27
+ <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">
28
+ {language}
29
+ </span>
30
+ )}
31
+ <pre className="bg-bg-code rounded-md p-4 overflow-x-auto text-code font-mono leading-relaxed">
31
32
  <code ref={codeRef} className={language ? `language-${language}` : ""}>{code}</code>
32
- </pre>
33
- </div>
34
- );
33
+ </pre>
34
+ </div>
35
+ )
35
36
  }
@@ -1,23 +1,22 @@
1
- import React from "react";
2
- import { buildDiffLines } from "../diffLines";
1
+ import { buildDiffLines } from '../diffLines'
3
2
 
4
3
  interface DiffProps {
5
- before: string;
6
- after: string;
7
- language?: string;
4
+ before: string
5
+ after: string
6
+ language?: string
8
7
  }
9
8
 
10
9
  export function Diff({ before, after, language }: DiffProps) {
11
- const diffLines = buildDiffLines(before, after);
10
+ const diffLines = buildDiffLines(before, after)
12
11
 
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">
12
+ return (
13
+ <div className="mt-3 bg-bg-code rounded-md overflow-hidden group/diff" data-md="diff" data-md-language={language || ''}>
14
+ {language && (
15
+ <div className="px-4 py-2 text-meta font-mono text-text-tertiary opacity-0 group-hover/diff:opacity-60 transition-opacity">
16
+ diff ({language})
17
+ </div>
18
+ )}
19
+ <pre className="px-4 pb-3 overflow-x-auto text-code font-mono">
21
20
  {diffLines.map((d, i) => (
22
21
  <div key={i} className={
23
22
  d.type === "removed" ? "bg-accent-red-muted text-accent-red" :
@@ -30,7 +29,7 @@ export function Diff({ before, after, language }: DiffProps) {
30
29
  {d.line}
31
30
  </div>
32
31
  ))}
33
- </pre>
34
- </div>
35
- );
32
+ </pre>
33
+ </div>
34
+ )
36
35
  }