@plannotator/ui 0.39.0 → 0.40.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/HANDOFF.md +99 -10
- package/README.md +3 -3
- package/components/AnnotationPanel.tsx +244 -13
- package/components/CommentPopover.tsx +91 -2
- package/components/HtmlSurfaceControls.tsx +188 -23
- package/components/ListMarker.tsx +10 -1
- package/components/MermaidBlock.tsx +39 -5
- package/components/TableOfContents.tsx +5 -1
- package/components/TerminalToolsAnnouncementDialog.tsx +454 -0
- package/components/Viewer.tsx +25 -1
- package/components/blocks/AlertBlock.tsx +7 -2
- package/components/html-viewer/HtmlViewer.tsx +32 -0
- package/components/html-viewer/bridge-script.asset.js +96 -9
- package/components/html-viewer/bridge-script.ts +96 -9
- package/components/html-viewer/useHtmlAnnotation.ts +44 -151
- package/hooks/useAnnotationHighlighter.ts +464 -14
- package/hooks/useLinkedDoc.ts +100 -9
- package/package.json +3 -3
- package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +21 -7
- package/styles.css +1 -1
- package/theme.css +62 -0
- package/utils/annotationScope.ts +159 -0
- package/utils/cssColor.ts +463 -0
- package/utils/htmlChrome.ts +70 -5
- package/utils/htmlLinkNavigation.ts +196 -0
- package/utils/mermaid-eager.ts +13 -11
- package/utils/mermaid.ts +19 -10
- package/utils/mermaidTheme.ts +732 -0
- package/utils/parser.ts +21 -6
- package/utils/terminalToolsAnnouncement.ts +76 -0
package/hooks/useLinkedDoc.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useState, useCallback, useRef } from "react";
|
|
10
|
+
import { normalizeBrowserPath } from "@plannotator/core/browser-paths";
|
|
10
11
|
import type { Annotation, ImageAttachment } from "../types";
|
|
11
12
|
import type { ViewerHandle } from "../components/Viewer";
|
|
12
13
|
import type { SidebarTab } from "./useSidebar";
|
|
@@ -136,13 +137,19 @@ export interface UseLinkedDocReturn {
|
|
|
136
137
|
error: string | null;
|
|
137
138
|
/** Whether a fetch is in progress */
|
|
138
139
|
isLoading: boolean;
|
|
139
|
-
/** Open a linked document by path (saves plan state, fetches doc, swaps)
|
|
140
|
-
|
|
140
|
+
/** Open a linked document by path (saves plan state, fetches doc, swaps).
|
|
141
|
+
* `revealSidebar: false` leaves the sidebar exactly as it was. */
|
|
142
|
+
open: (
|
|
143
|
+
docPath: string,
|
|
144
|
+
buildUrl?: (path: string) => string,
|
|
145
|
+
targetTab?: SidebarTab,
|
|
146
|
+
options?: { revealSidebar?: boolean },
|
|
147
|
+
) => Promise<void>;
|
|
141
148
|
/** Open an already-loaded linked document without refetching from disk */
|
|
142
149
|
openLoaded: (
|
|
143
150
|
doc: LinkedDocLoadData & { filepath: string },
|
|
144
151
|
targetTab?: SidebarTab,
|
|
145
|
-
options?: { notifyDocumentLoaded?: boolean },
|
|
152
|
+
options?: { notifyDocumentLoaded?: boolean; revealSidebar?: boolean },
|
|
146
153
|
) => void;
|
|
147
154
|
/** Return to the plan (caches doc annotations, restores plan state) */
|
|
148
155
|
back: () => void;
|
|
@@ -150,6 +157,20 @@ export interface UseLinkedDocReturn {
|
|
|
150
157
|
dismissError: () => void;
|
|
151
158
|
/** All linked doc annotations including the active doc's live state (keyed by filepath) */
|
|
152
159
|
getDocAnnotations: () => Map<string, CachedDocState>;
|
|
160
|
+
/**
|
|
161
|
+
* Replace the stored annotations of a document that is NOT the active one —
|
|
162
|
+
* a cached linked doc, or the stashed source document. This is what lets the
|
|
163
|
+
* panel's cross-file view edit and delete another file's comments without
|
|
164
|
+
* navigating to it; the active document's annotations are host state
|
|
165
|
+
* (`setAnnotations`) and are deliberately never touched here.
|
|
166
|
+
*
|
|
167
|
+
* Returns false when no stored document matches (including the active one),
|
|
168
|
+
* so a host can fall back to its own live-state path.
|
|
169
|
+
*/
|
|
170
|
+
updateStoredAnnotations: (
|
|
171
|
+
filepath: string,
|
|
172
|
+
update: (annotations: Annotation[]) => Annotation[],
|
|
173
|
+
) => boolean;
|
|
153
174
|
/** Snapshot the root document plus linked-doc cache for cross-document session swaps */
|
|
154
175
|
snapshotSession: () => LinkedDocSessionState;
|
|
155
176
|
/** Restore a root document plus linked-doc cache, closing any active linked document */
|
|
@@ -203,6 +224,11 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
203
224
|
const [error, setError] = useState<string | null>(null);
|
|
204
225
|
const [isLoading, setIsLoading] = useState(false);
|
|
205
226
|
const [docAnnotationCount, setDocAnnotationCount] = useState(0);
|
|
227
|
+
// Bumped whenever a stored (non-active) document's annotations change in
|
|
228
|
+
// place. docCache is a ref, so without this the memos keyed on
|
|
229
|
+
// getDocAnnotations' identity — panel groups, counts, the export — would
|
|
230
|
+
// keep serving the pre-mutation cache.
|
|
231
|
+
const [storeRevision, setStoreRevision] = useState(0);
|
|
206
232
|
|
|
207
233
|
// Stash plan state when navigating to a linked doc
|
|
208
234
|
const savedPlanState = useRef<SavedPlanState | null>(null);
|
|
@@ -281,10 +307,19 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
281
307
|
const activateDocument = useCallback((
|
|
282
308
|
data: LinkedDocLoadData & { filepath: string },
|
|
283
309
|
targetTab?: SidebarTab,
|
|
284
|
-
options: {
|
|
310
|
+
options: {
|
|
311
|
+
snapshotCurrent?: boolean;
|
|
312
|
+
notifyDocumentLoaded?: boolean;
|
|
313
|
+
revealSidebar?: boolean;
|
|
314
|
+
} = {},
|
|
285
315
|
) => {
|
|
286
316
|
const snapshotCurrent = options.snapshotCurrent ?? true;
|
|
287
317
|
const notifyDocumentLoaded = options.notifyDocumentLoaded ?? true;
|
|
318
|
+
// Opening a document reveals the sidebar so its "Viewing / Back to …"
|
|
319
|
+
// header is in reach. A host whose surface has its own way back (the
|
|
320
|
+
// raw-HTML header's Back control) passes false, and the sidebar is left
|
|
321
|
+
// exactly as the user had it — closed stays closed, open stays put.
|
|
322
|
+
const revealSidebar = options.revealSidebar ?? true;
|
|
288
323
|
if (snapshotCurrent) onBeforeNavigate?.();
|
|
289
324
|
|
|
290
325
|
// Backlink detection: if a linked doc links back to the source file (e.g.,
|
|
@@ -368,7 +403,7 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
368
403
|
versionInfo: diffBaseline.versionInfo,
|
|
369
404
|
});
|
|
370
405
|
setError(null);
|
|
371
|
-
sidebar.open(targetTab ?? "toc");
|
|
406
|
+
if (revealSidebar) sidebar.open(targetTab ?? "toc");
|
|
372
407
|
onDocumentActivated?.(data);
|
|
373
408
|
|
|
374
409
|
// Re-apply cached annotations after DOM settles
|
|
@@ -407,16 +442,22 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
407
442
|
const openLoaded = useCallback((
|
|
408
443
|
doc: LinkedDocLoadData & { filepath: string },
|
|
409
444
|
targetTab?: SidebarTab,
|
|
410
|
-
options?: { notifyDocumentLoaded?: boolean },
|
|
445
|
+
options?: { notifyDocumentLoaded?: boolean; revealSidebar?: boolean },
|
|
411
446
|
) => {
|
|
412
447
|
activateDocument(doc, targetTab, {
|
|
413
448
|
snapshotCurrent: true,
|
|
414
449
|
notifyDocumentLoaded: options?.notifyDocumentLoaded,
|
|
450
|
+
revealSidebar: options?.revealSidebar,
|
|
415
451
|
});
|
|
416
452
|
}, [activateDocument]);
|
|
417
453
|
|
|
418
454
|
const open = useCallback(
|
|
419
|
-
async (
|
|
455
|
+
async (
|
|
456
|
+
docPath: string,
|
|
457
|
+
buildUrl?: (path: string) => string,
|
|
458
|
+
targetTab?: SidebarTab,
|
|
459
|
+
options?: { revealSidebar?: boolean },
|
|
460
|
+
) => {
|
|
420
461
|
onBeforeNavigate?.();
|
|
421
462
|
setIsLoading(true);
|
|
422
463
|
setError(null);
|
|
@@ -438,7 +479,10 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
438
479
|
setError("Failed to load document");
|
|
439
480
|
return;
|
|
440
481
|
}
|
|
441
|
-
activateDocument({ ...data, filepath: data.filepath }, targetTab, {
|
|
482
|
+
activateDocument({ ...data, filepath: data.filepath }, targetTab, {
|
|
483
|
+
snapshotCurrent: false,
|
|
484
|
+
revealSidebar: options?.revealSidebar,
|
|
485
|
+
});
|
|
442
486
|
} catch {
|
|
443
487
|
setError("Failed to connect to server");
|
|
444
488
|
} finally {
|
|
@@ -527,6 +571,50 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
527
571
|
viewerRef,
|
|
528
572
|
]);
|
|
529
573
|
|
|
574
|
+
const updateStoredAnnotations = useCallback((
|
|
575
|
+
filepath: string,
|
|
576
|
+
update: (annotations: Annotation[]) => Annotation[],
|
|
577
|
+
): boolean => {
|
|
578
|
+
// Callers address documents by whatever spelling their list carries — the
|
|
579
|
+
// panel's groups are normalized (forward slashes, collapsed separators)
|
|
580
|
+
// while the cache is keyed by the raw server path. On Windows those differ
|
|
581
|
+
// (`C:/repo/a.md` vs `C:\repo\a.md`) and an unnormalized lookup missed
|
|
582
|
+
// every time, so a cross-file edit or delete silently did nothing.
|
|
583
|
+
const wanted = normalizeBrowserPath(filepath);
|
|
584
|
+
|
|
585
|
+
// The active document's annotations live in host state, not the cache — a
|
|
586
|
+
// write here would be silently overwritten the next time it is cached.
|
|
587
|
+
if (linkedDoc && wanted === normalizeBrowserPath(linkedDoc.filepath)) return false;
|
|
588
|
+
|
|
589
|
+
let cacheKey: string | undefined;
|
|
590
|
+
for (const key of docCache.current.keys()) {
|
|
591
|
+
if (normalizeBrowserPath(key) === wanted) { cacheKey = key; break; }
|
|
592
|
+
}
|
|
593
|
+
const cached = cacheKey === undefined ? undefined : docCache.current.get(cacheKey);
|
|
594
|
+
if (cached && cacheKey !== undefined) {
|
|
595
|
+
docCache.current.set(cacheKey, { ...cached, annotations: update([...cached.annotations]) });
|
|
596
|
+
} else if (savedPlanState.current && sourceFilePath && wanted === normalizeBrowserPath(sourceFilePath)) {
|
|
597
|
+
const saved = savedPlanState.current;
|
|
598
|
+
savedPlanState.current = { ...saved, annotations: update([...saved.annotations]) };
|
|
599
|
+
} else {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Same accounting as activateDocument/back: everything except the document
|
|
604
|
+
// that is active right now.
|
|
605
|
+
let total = 0;
|
|
606
|
+
for (const [fp, entry] of docCache.current.entries()) {
|
|
607
|
+
if (linkedDoc && fp === linkedDoc.filepath) continue;
|
|
608
|
+
total += entry.annotations.length + entry.globalAttachments.length;
|
|
609
|
+
}
|
|
610
|
+
if (linkedDoc && savedPlanState.current) {
|
|
611
|
+
total += savedPlanState.current.annotations.length + savedPlanState.current.globalAttachments.length;
|
|
612
|
+
}
|
|
613
|
+
setDocAnnotationCount(total);
|
|
614
|
+
setStoreRevision((r) => r + 1);
|
|
615
|
+
return true;
|
|
616
|
+
}, [linkedDoc, sourceFilePath]);
|
|
617
|
+
|
|
530
618
|
const getDocAnnotations = useCallback((): Map<string, CachedDocState> => {
|
|
531
619
|
const result = new Map(docCache.current);
|
|
532
620
|
// Include stashed original-file annotations when viewing a linked doc
|
|
@@ -547,7 +635,9 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
547
635
|
});
|
|
548
636
|
}
|
|
549
637
|
return result;
|
|
550
|
-
|
|
638
|
+
// storeRevision: cross-file edits mutate docCache in place, so the
|
|
639
|
+
// identity of this callback is what tells memoized readers to recompute.
|
|
640
|
+
}, [linkedDoc, annotations, globalAttachments, sourceFilePath, sourceConverted, getDocumentMarkdown, storeRevision]);
|
|
551
641
|
|
|
552
642
|
return {
|
|
553
643
|
isActive: linkedDoc !== null,
|
|
@@ -559,6 +649,7 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
559
649
|
back,
|
|
560
650
|
dismissError,
|
|
561
651
|
getDocAnnotations,
|
|
652
|
+
updateStoredAnnotations,
|
|
562
653
|
snapshotSession,
|
|
563
654
|
restoreSession,
|
|
564
655
|
docAnnotationCount,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plannotator/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./components/*": "./components/*.tsx",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@lezer/highlight": "^1.2.3",
|
|
75
75
|
"@pierre/diffs": "1.3.6",
|
|
76
76
|
"@plannotator/atomic-editor": "^0.8.0",
|
|
77
|
-
"@plannotator/core": "0.25.
|
|
77
|
+
"@plannotator/core": "0.25.3",
|
|
78
78
|
"@plannotator/markdown-editor": "^0.4.0",
|
|
79
79
|
"@plannotator/web-highlighter": "^0.8.1",
|
|
80
80
|
"@tanstack/react-table": "^8.21.3",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"katex": "^0.16.47",
|
|
87
87
|
"lucide-react": "^1.38.0",
|
|
88
88
|
"marked": "^17.0.6",
|
|
89
|
-
"mermaid": "
|
|
89
|
+
"mermaid": "12.0.0",
|
|
90
90
|
"motion": "^12.38.0",
|
|
91
91
|
"perfect-freehand": "^1.2.2",
|
|
92
92
|
"tailwind-merge": "^3.6.0",
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
import { defineShortcutScope } from '../core';
|
|
2
2
|
import { createShortcutScopeHook } from '../runtime';
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Chords for the HTML and live-app annotate surfaces. Both are mirrored inside
|
|
5
|
+
// the sandboxed iframe by the bridge (focus usually lives in there on live
|
|
6
|
+
// apps) and forwarded to the parent, so they work regardless of which document
|
|
7
|
+
// owns the keyboard. No bare-letter binding: bare letters belong to
|
|
8
|
+
// type-to-comment and the page itself.
|
|
9
|
+
//
|
|
10
|
+
// `Mod+Shift+X` for the tools rather than a mnemonic letter: every mnemonic
|
|
11
|
+
// candidate is claimed by a browser (H = Chrome's Home / Firefox's history
|
|
12
|
+
// library, E/I/J/K/C = devtools, B/O = bookmarks, V = paste-as-plain-text,
|
|
13
|
+
// T = reopen tab), and a chord the browser eats is worse than an arbitrary
|
|
14
|
+
// one. X is unassigned in Chrome, Firefox, Safari and Edge on every platform.
|
|
9
15
|
export const htmlAnnotateShortcuts = defineShortcutScope({
|
|
10
16
|
id: 'html-annotate',
|
|
11
17
|
title: 'HTML Annotate',
|
|
12
18
|
shortcuts: {
|
|
13
19
|
toggleAnnotateMode: {
|
|
14
|
-
description: 'Toggle
|
|
20
|
+
description: 'Toggle annotate mode',
|
|
15
21
|
bindings: ['Mod+Shift+A'],
|
|
16
22
|
section: 'Annotations',
|
|
17
|
-
hint: 'On HTML and live app surfaces: arm annotation capture, or hand clicks back to the page. Esc also exits Annotate.',
|
|
23
|
+
hint: 'On HTML and live app surfaces: arm annotation capture, or hand clicks back to the page. Esc also exits Annotate; this chord is the way back in.',
|
|
18
24
|
preventDefault: true,
|
|
19
25
|
displayOrder: 0,
|
|
20
26
|
},
|
|
27
|
+
toggleTools: {
|
|
28
|
+
description: 'Show or hide tools',
|
|
29
|
+
bindings: ['Mod+Shift+X'],
|
|
30
|
+
section: 'Annotations',
|
|
31
|
+
hint: 'On HTML and live app surfaces: the header eye — shows or removes all floating chrome over the page. HTML surfaces open with the tools hidden.',
|
|
32
|
+
preventDefault: true,
|
|
33
|
+
displayOrder: 1,
|
|
34
|
+
},
|
|
21
35
|
},
|
|
22
36
|
});
|
|
23
37
|
|