@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.
- 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 -58
- 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,11 +1,11 @@
|
|
|
1
|
-
import { createContext } from
|
|
1
|
+
import { createContext } from 'react'
|
|
2
2
|
|
|
3
3
|
export interface CanvasRenderError {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
revision: number
|
|
5
|
+
filename: string
|
|
6
|
+
message: string
|
|
7
|
+
stack?: string
|
|
8
|
+
componentStack?: string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export const RenderErrorContext = createContext<(error: CanvasRenderError) => void>(() => {})
|
|
11
|
+
export const RenderErrorContext = createContext<(error: CanvasRenderError) => void>(() => {})
|
|
@@ -1,186 +1,205 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { useAnnotations } from
|
|
4
|
-
import { generateMarkdown, getMissingRequiredLabels } from
|
|
1
|
+
import { marked } from 'marked'
|
|
2
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
+
import { useAnnotations } from './AnnotationProvider'
|
|
4
|
+
import { generateMarkdown, getMissingRequiredLabels } from './generateMarkdown'
|
|
5
5
|
|
|
6
6
|
interface ResponsePreviewProps {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
open: boolean
|
|
8
|
+
onClose: () => void
|
|
9
|
+
onSubmit: (feedback: string) => void
|
|
10
|
+
includedRemoteIds: Set<string>
|
|
11
|
+
onToggleRemoteId: (id: string) => void
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function ResponsePreview({ open, onClose, onSubmit, includedRemoteIds, onToggleRemoteId }: ResponsePreviewProps) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
15
|
+
const { annotations, generalNote, submittableResponses, feedbackEntries } = useAnnotations()
|
|
16
|
+
const [editedText, setEditedText] = useState('')
|
|
17
|
+
const [manuallyEdited, setManuallyEdited] = useState(false)
|
|
18
|
+
const [editMode, setEditMode] = useState(false)
|
|
19
|
+
const [validationError, setValidationError] = useState<string | null>(null)
|
|
20
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
21
|
+
const [prevOpen, setPrevOpen] = useState(false)
|
|
22
|
+
|
|
23
|
+
const remoteAnnotations = useMemo(
|
|
24
|
+
() => annotations.filter((a) => a.source === 'remote'),
|
|
25
|
+
[annotations],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if (open && !prevOpen) {
|
|
29
|
+
setManuallyEdited(false)
|
|
30
|
+
setEditMode(false)
|
|
31
|
+
setValidationError(null)
|
|
32
|
+
}
|
|
33
|
+
if (!open && prevOpen) {
|
|
34
|
+
setManuallyEdited(false)
|
|
35
|
+
setEditMode(false)
|
|
36
|
+
setValidationError(null)
|
|
37
|
+
}
|
|
38
|
+
if (open !== prevOpen) {
|
|
39
|
+
setPrevOpen(open)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const generatedText = useMemo(
|
|
43
|
+
() => generateMarkdown(annotations, generalNote, submittableResponses, feedbackEntries, includedRemoteIds),
|
|
44
|
+
[annotations, generalNote, submittableResponses, feedbackEntries, includedRemoteIds],
|
|
45
|
+
)
|
|
46
|
+
const text = manuallyEdited ? editedText : generatedText
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (open && editMode && textareaRef.current) {
|
|
50
|
+
textareaRef.current.focus()
|
|
51
|
+
}
|
|
52
|
+
}, [open, editMode])
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!open) return
|
|
56
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
57
|
+
if (e.key === 'Escape') onClose()
|
|
58
|
+
}
|
|
59
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
60
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
61
|
+
}, [open, onClose])
|
|
62
|
+
|
|
63
|
+
if (!open) return null
|
|
64
|
+
|
|
65
|
+
const hasContent = text.trim().length > 0
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-8" onClick={onClose}>
|
|
69
|
+
<div
|
|
70
|
+
className="bg-bg-surface border border-border-medium rounded-xl shadow-lg w-full max-w-2xl flex flex-col"
|
|
71
|
+
style={{ height: '80vh' }}
|
|
72
|
+
onClick={(e) => e.stopPropagation()}
|
|
73
|
+
>
|
|
74
|
+
{/* Header */}
|
|
75
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-border-subtle flex-shrink-0">
|
|
76
|
+
<div className="flex items-center gap-3">
|
|
77
|
+
<span className="text-[13px] font-medium text-text-primary font-body">Response preview</span>
|
|
78
|
+
{manuallyEdited && (
|
|
79
|
+
<button
|
|
80
|
+
onClick={() => {
|
|
81
|
+
setManuallyEdited(false)
|
|
82
|
+
setEditMode(false)
|
|
83
|
+
}}
|
|
84
|
+
className="text-[11px] text-text-tertiary hover:text-text-secondary font-body"
|
|
85
|
+
>
|
|
86
|
+
Reset
|
|
87
|
+
</button>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex items-center gap-2">
|
|
91
|
+
{/* Mode toggle */}
|
|
92
|
+
<div className="flex items-center bg-bg-base rounded-md p-0.5">
|
|
93
|
+
<button
|
|
94
|
+
onClick={() => setEditMode(false)}
|
|
95
|
+
className={`px-2.5 py-1 rounded text-[11px] font-body font-medium transition-colors ${
|
|
96
|
+
!editMode ? 'bg-bg-elevated text-text-primary shadow-sm' : 'text-text-tertiary hover:text-text-secondary'
|
|
97
|
+
}`}
|
|
98
|
+
>
|
|
99
|
+
Preview
|
|
100
|
+
</button>
|
|
101
|
+
<button
|
|
102
|
+
onClick={() => setEditMode(true)}
|
|
103
|
+
className={`px-2.5 py-1 rounded text-[11px] font-body font-medium transition-colors ${
|
|
104
|
+
editMode ? 'bg-bg-elevated text-text-primary shadow-sm' : 'text-text-tertiary hover:text-text-secondary'
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
Edit
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
<button
|
|
111
|
+
onClick={onClose}
|
|
112
|
+
className="text-text-tertiary hover:text-text-secondary w-7 h-7 flex items-center justify-center rounded-md hover:bg-border-subtle transition-colors"
|
|
113
|
+
>
|
|
114
|
+
✕
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Content */}
|
|
120
|
+
{editMode
|
|
121
|
+
? (
|
|
122
|
+
<textarea
|
|
123
|
+
ref={textareaRef}
|
|
124
|
+
value={text}
|
|
125
|
+
onChange={(e) => {
|
|
126
|
+
setEditedText(e.currentTarget.value)
|
|
127
|
+
setManuallyEdited(true)
|
|
128
|
+
}}
|
|
129
|
+
className="flex-1 w-full bg-bg-base p-5 text-[13px] font-mono text-text-code resize-none focus:outline-none leading-relaxed placeholder:text-text-disabled"
|
|
130
|
+
placeholder="Your feedback will appear here..."
|
|
131
|
+
/>
|
|
132
|
+
)
|
|
133
|
+
: (
|
|
134
|
+
<div className="flex-1 overflow-y-auto p-5">
|
|
135
|
+
<MarkdownPreview text={text} />
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Remote feedback summary */}
|
|
140
|
+
{remoteAnnotations.length > 0 && (
|
|
141
|
+
<div className="px-5 py-2 border-t border-border-subtle flex-shrink-0">
|
|
142
|
+
<span className="text-[12px] text-text-secondary font-body">
|
|
143
|
+
Reviewer feedback: {includedRemoteIds.size}/{remoteAnnotations.length} included
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{/* Actions */}
|
|
149
|
+
{validationError && (
|
|
150
|
+
<div className="px-5 py-2 text-[12px] text-accent-red font-body border-t border-border-subtle flex-shrink-0 flex items-center justify-between gap-2">
|
|
151
|
+
<span>{validationError}</span>
|
|
152
|
+
<button
|
|
153
|
+
onClick={() => {
|
|
154
|
+
setValidationError(null)
|
|
155
|
+
onSubmit(text)
|
|
156
|
+
}}
|
|
157
|
+
className="text-[11px] text-text-tertiary hover:text-text-secondary font-body whitespace-nowrap underline"
|
|
158
|
+
>
|
|
159
|
+
Submit anyway
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
<div className="flex justify-end gap-2 px-5 py-3 border-t border-border-subtle flex-shrink-0">
|
|
164
|
+
<button
|
|
165
|
+
onClick={onClose}
|
|
166
|
+
className="px-4 py-2 rounded-lg text-[13px] font-body text-text-tertiary hover:text-text-secondary transition-colors"
|
|
167
|
+
>
|
|
168
|
+
Cancel
|
|
169
|
+
</button>
|
|
170
|
+
<button
|
|
171
|
+
onClick={() => {
|
|
172
|
+
const allMissing = getMissingRequiredLabels(submittableResponses, feedbackEntries)
|
|
173
|
+
if (allMissing.length > 0) {
|
|
174
|
+
setValidationError(`Please answer: ${allMissing.join(', ')}`)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
setValidationError(null)
|
|
178
|
+
onSubmit(text)
|
|
179
|
+
}}
|
|
180
|
+
disabled={!hasContent}
|
|
181
|
+
className={`px-6 py-2 rounded-lg font-body text-[13px] font-medium transition-all ${
|
|
182
|
+
hasContent
|
|
183
|
+
? 'bg-btn-primary text-btn-primary-text hover:opacity-90'
|
|
184
|
+
: 'bg-bg-input text-text-disabled cursor-default'
|
|
185
|
+
}`}
|
|
186
|
+
>
|
|
187
|
+
Submit feedback
|
|
188
|
+
</button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
175
193
|
}
|
|
176
194
|
|
|
177
|
-
const UPLOADS_PATH_RE = /!\[([^\]]*)\]\((\/tmp\/agent-canvas\/uploads\/([^)]+))\)/g
|
|
195
|
+
const UPLOADS_PATH_RE = /!\[([^\]]*)\]\((\/tmp\/agent-canvas\/uploads\/([^)]+))\)/g
|
|
178
196
|
|
|
179
197
|
function rewriteUploadPaths(md: string): string {
|
|
180
|
-
|
|
198
|
+
return md.replace(UPLOADS_PATH_RE, (_match, alt, _fullPath, filename) => ``)
|
|
181
199
|
}
|
|
182
200
|
|
|
183
201
|
export function MarkdownPreview({ text }: { text: string }) {
|
|
184
|
-
|
|
185
|
-
|
|
202
|
+
const html = useMemo(() => marked.parse(rewriteUploadPaths(text), { async: false }) as string, [text])
|
|
203
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: Canvas modules already execute as code in this document.
|
|
204
|
+
return <div className="prose-canvas font-body text-body text-text-primary leading-relaxed" dangerouslySetInnerHTML={{ __html: html }} />
|
|
186
205
|
}
|
|
@@ -1,93 +1,39 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
4
|
-
import { blockTarget } from "./blockTarget";
|
|
5
|
-
import { regionTarget } from "./regionTarget";
|
|
6
|
-
import { textTarget } from "./textTarget";
|
|
7
|
-
import { fileAnnotationPath } from "./utils";
|
|
8
|
-
|
|
9
|
-
// The block selectors and the block snippet live with the block target now;
|
|
10
|
-
// re-exported here because this is the path every caller already imports.
|
|
11
|
-
export { ANNOTATABLE_SELECTOR, BLOCK_SELECTOR, getBlockSnippet } from "./blockTarget";
|
|
12
|
-
|
|
13
|
-
/** Every locator strategy the kernel knows, cheapest and most specific first.
|
|
14
|
-
* Text claims any snippet at all, so it stays last. */
|
|
15
|
-
export const ANNOTATION_TARGETS: SealedAnnotationTarget[] = [
|
|
16
|
-
sealTarget(regionTarget),
|
|
17
|
-
sealTarget(blockTarget),
|
|
18
|
-
sealTarget(textTarget),
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The element a snippet names inside `root`.
|
|
23
|
-
*
|
|
24
|
-
* Every target is asked, not just the first one that recognises the string: a
|
|
25
|
-
* file annotation on a line that happens to read like a block snippet must
|
|
26
|
-
* still fall through to the target that can actually resolve it.
|
|
27
|
-
*/
|
|
28
|
-
export function findSnippetElement(snippet: string, root: ParentNode): HTMLElement | null {
|
|
29
|
-
for (const target of ANNOTATION_TARGETS) {
|
|
30
|
-
const el = target.find(snippet, root);
|
|
31
|
-
if (el) return el;
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Put every annotation's own decoration back after a render — inline marks,
|
|
38
|
-
* region overlays. Each target is asked about each annotation; a snippet is not
|
|
39
|
-
* owned exclusively, and every restore is idempotent, so asking all of them is
|
|
40
|
-
* both correct and free.
|
|
41
|
-
*/
|
|
42
|
-
export function restoreAnnotationTargets(root: HTMLElement, annotations: readonly TargetAnnotation[]): void {
|
|
43
|
-
for (const ann of annotations) {
|
|
44
|
-
if (!ann.snippet) continue;
|
|
45
|
-
for (const target of ANNOTATION_TARGETS) {
|
|
46
|
-
target.restore(root, ann);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The snippet as a reader should see it. Only a target that encodes something
|
|
52
|
-
* other than readable text has anything to say; the rest already read fine. */
|
|
53
|
-
export function describeSnippet(snippet: string): string {
|
|
54
|
-
for (const target of ANNOTATION_TARGETS) {
|
|
55
|
-
const described = target.describe(snippet);
|
|
56
|
-
if (described !== null) return described;
|
|
57
|
-
}
|
|
58
|
-
return snippet;
|
|
59
|
-
}
|
|
1
|
+
import { fileAnnotationPath, findSnippetElement } from '@fabrika/annotations'
|
|
2
|
+
import type { Annotation } from './AnnotationProvider'
|
|
3
|
+
import type { ActiveView } from './appContext'
|
|
60
4
|
|
|
61
5
|
/**
|
|
62
6
|
* Find the DOM element for an annotation — the decoration this kernel drew for
|
|
63
7
|
* it, or failing that whatever its snippet names.
|
|
64
8
|
*/
|
|
65
9
|
export function findAnnotationElement(ann: Annotation): HTMLElement | null {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
10
|
+
// Decoration wins over the snippet: a mark or a region box names the exact
|
|
11
|
+
// run that was annotated, where a snippet only names something that reads the
|
|
12
|
+
// same today.
|
|
13
|
+
const decoration = document.querySelector(`[data-annotation-id="${ann.id}"]`)
|
|
14
|
+
if (decoration instanceof HTMLElement) return decoration
|
|
15
|
+
|
|
16
|
+
// A snippet only identifies its target within its own canvas, and the
|
|
17
|
+
// overview mounts every canvas at once — searching the whole document lets an
|
|
18
|
+
// identical block in a different canvas win on document order. No container
|
|
19
|
+
// for the canvas means it is not on screen, which is a miss, not a reason to
|
|
20
|
+
// look elsewhere.
|
|
21
|
+
const root = ann.canvasFile
|
|
22
|
+
? document.querySelector(`[data-canvas-file="${ann.canvasFile}"]`)
|
|
23
|
+
: document
|
|
24
|
+
if (!root) return null
|
|
25
|
+
|
|
26
|
+
return findSnippetElement(ann.snippet, root)
|
|
83
27
|
}
|
|
84
28
|
|
|
85
29
|
/** Flash/pulse the element briefly — blinks bg via CSS animation */
|
|
86
30
|
function flashElement(el: HTMLElement) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
31
|
+
el.classList.remove('ann-flash')
|
|
32
|
+
void el.offsetWidth // force reflow to restart
|
|
33
|
+
el.classList.add('ann-flash')
|
|
34
|
+
el.addEventListener('animationend', () => {
|
|
35
|
+
el.classList.remove('ann-flash')
|
|
36
|
+
}, { once: true })
|
|
91
37
|
}
|
|
92
38
|
|
|
93
39
|
/**
|
|
@@ -95,21 +41,21 @@ function flashElement(el: HTMLElement) {
|
|
|
95
41
|
* Handles file annotations by switching view first.
|
|
96
42
|
*/
|
|
97
43
|
export function scrollToAnnotation(ann: Annotation, setActiveView: (view: ActiveView) => void) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
44
|
+
const filePath = fileAnnotationPath(ann)
|
|
45
|
+
if (filePath) {
|
|
46
|
+
setActiveView({ type: 'file', path: filePath })
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
const el = findAnnotationElement(ann)
|
|
49
|
+
if (el) {
|
|
50
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
51
|
+
flashElement(el)
|
|
52
|
+
}
|
|
53
|
+
}, 150)
|
|
54
|
+
} else {
|
|
55
|
+
const el = findAnnotationElement(ann)
|
|
56
|
+
if (el) {
|
|
57
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
58
|
+
flashElement(el)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
115
61
|
}
|