@fabrika/canvas-kernel 0.1.45 → 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.
- package/README.md +33 -67
- package/index.ts +19 -83
- package/package.json +68 -72
- package/src/client/AnnotationProvider.tsx +227 -268
- package/src/client/AnnotationSidebar.tsx +392 -338
- package/src/client/FileIcon.tsx +106 -65
- package/src/client/PlanRenderer.tsx +607 -551
- package/src/client/RenderErrorContext.ts +7 -7
- package/src/client/ResponsePreview.tsx +192 -173
- package/src/client/annotationDom.ts +43 -97
- package/src/client/annotationDraft.ts +98 -92
- package/src/client/appContext.tsx +40 -40
- package/src/client/components/Callout.tsx +20 -20
- package/src/client/components/Checklist.tsx +28 -28
- package/src/client/components/Choice.tsx +178 -159
- package/src/client/components/CodeBlock.tsx +26 -26
- package/src/client/components/Diff.tsx +17 -17
- package/src/client/components/FilePreview.tsx +99 -74
- package/src/client/components/Image.tsx +41 -39
- package/src/client/components/Markdown.tsx +33 -32
- package/src/client/components/Mermaid.tsx +592 -542
- package/src/client/components/Note.tsx +8 -8
- package/src/client/components/Priority.tsx +11 -11
- package/src/client/components/ResponseNote.tsx +17 -15
- package/src/client/components/Section.tsx +28 -26
- package/src/client/components/Table.tsx +34 -29
- package/src/client/components/Task.tsx +58 -60
- package/src/client/components/UserInput.tsx +169 -152
- package/src/client/components/index.ts +16 -16
- package/src/client/components/renderMarkdownChildren.tsx +86 -68
- package/src/client/diffLines.ts +51 -39
- package/src/client/generateMarkdown.ts +197 -191
- package/src/client/hostContext.tsx +34 -34
- package/src/client/index.ts +43 -125
- package/src/client/preact-all.ts +4 -4
- package/src/client/runtime.ts +58 -111
- package/src/client/styles.css +1 -74
- package/src/engine/compiler.ts +316 -313
- package/src/engine/paths.ts +44 -44
- package/src/engine/session.ts +623 -618
- package/src/engine/watcher.ts +63 -60
- package/src/engine/websocket.ts +123 -121
- package/annotate.ts +0 -94
- package/daemon.ts +0 -46
- package/src/client/AnnotationDraftFooter.tsx +0 -76
- package/src/client/AnnotationEditor.tsx +0 -320
- package/src/client/AnnotationList.tsx +0 -190
- package/src/client/Popover.tsx +0 -361
- package/src/client/SessionContext.tsx +0 -3
- package/src/client/annotationContext.ts +0 -130
- package/src/client/annotationMarkdown.ts +0 -102
- package/src/client/annotationTarget.ts +0 -73
- package/src/client/blockTarget.ts +0 -127
- package/src/client/highlightRange.ts +0 -262
- package/src/client/popoverPosition.ts +0 -46
- package/src/client/regionTarget.ts +0 -180
- package/src/client/testing/dom.ts +0 -74
- package/src/client/textTarget.ts +0 -31
- package/src/client/useRegionAnnotation.tsx +0 -188
- package/src/client/useTextAnnotation.tsx +0 -194
- package/src/client/utils.ts +0 -33
- package/src/engine/channel.ts +0 -278
- package/src/engine/http-utils.ts +0 -14
- package/src/engine/lifecycle.ts +0 -302
- package/src/engine/router.ts +0 -16
- package/src/engine/waiter.ts +0 -188
|
@@ -1,178 +1,197 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { ResponseNote } from
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
1
|
+
import React, { useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
interface CodeBlockProps {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
language?: string
|
|
5
|
+
children?: React.ReactNode
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function CodeBlock({ language, children }: CodeBlockProps) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const code = typeof children === 'string' ? children : String(children ?? '')
|
|
10
|
+
const codeRef = useRef<HTMLElement>(null)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
</pre>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
35
|
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { buildDiffLines } from
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { buildDiffLines } from '../diffLines'
|
|
3
3
|
|
|
4
4
|
interface DiffProps {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
before: string
|
|
6
|
+
after: string
|
|
7
|
+
language?: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function Diff({ before, after, language }: DiffProps) {
|
|
11
|
-
|
|
11
|
+
const diffLines = buildDiffLines(before, after)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
</pre>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
36
|
}
|