@plannotator/ui 0.39.0 → 0.41.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 +140 -10
- package/README.md +9 -5
- package/components/AnnotationPanel.tsx +244 -13
- package/components/CommentPopover.tsx +91 -2
- package/components/DiagramBlock.tsx +376 -0
- package/components/GraphvizBlock.tsx +22 -597
- package/components/HtmlSurfaceControls.tsx +188 -23
- package/components/ListMarker.tsx +10 -1
- package/components/MermaidBlock.tsx +17 -630
- package/components/TableOfContents.tsx +5 -1
- package/components/TerminalToolsAnnouncementDialog.tsx +454 -0
- package/components/Viewer.tsx +67 -3
- package/components/blocks/AlertBlock.tsx +7 -2
- package/components/diagram/DiagramCanvas.tsx +431 -0
- package/components/diagram/DiagramComposer.tsx +135 -0
- package/components/diagram/DiagramOverlay.tsx +215 -0
- package/components/diagram/DiagramPopout.tsx +70 -0
- package/components/diagram/DiagramSourcePane.tsx +244 -0
- package/components/diagram/DiagramViewer.tsx +276 -0
- package/components/diagram/anchorClaims.ts +71 -0
- package/components/diagram/index.ts +36 -0
- package/components/diagram/useDiagramComments.ts +341 -0
- package/components/diagram/useDiagramRender.ts +91 -0
- package/components/diagram/useDiagramSourceDraft.ts +143 -0
- package/components/diagram/useDiagramViewport.ts +156 -0
- package/components/html-viewer/HtmlViewer.tsx +32 -0
- package/components/html-viewer/bridge-script.asset.js +121 -9
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +133 -9
- package/components/html-viewer/useHtmlAnnotation.ts +44 -151
- package/hooks/useAnnotationHighlighter.ts +469 -14
- package/hooks/useLinkedDoc.ts +100 -9
- package/package.json +6 -4
- package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +21 -7
- package/styles.css +1 -1
- package/theme.css +62 -0
- package/types.ts +5 -0
- package/utils/annotationScope.ts +159 -0
- package/utils/cssColor.ts +463 -0
- package/utils/diagram-anchor-graphviz.ts +143 -0
- package/utils/diagram-anchor.ts +401 -0
- package/utils/diagram-projection.ts +66 -0
- package/utils/diagram-render.ts +668 -0
- package/utils/graphviz.ts +93 -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 +36 -7
- package/utils/terminalToolsAnnouncement.ts +76 -0
- package/components/mermaidSvg.ts +0 -33
package/theme.css
CHANGED
|
@@ -243,6 +243,68 @@
|
|
|
243
243
|
animation: vim-announcement-dialog-in 200ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/* First-run terminal-tools announcement: same entrance as the Vim one, and
|
|
247
|
+
* under reduced motion the panel still fades in, it just does not travel. */
|
|
248
|
+
.terminal-tools-announcement-backdrop {
|
|
249
|
+
animation: vim-announcement-backdrop-in 160ms ease-out both;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.terminal-tools-announcement-dialog {
|
|
253
|
+
animation: vim-announcement-dialog-in 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@media (prefers-reduced-motion: reduce) {
|
|
257
|
+
.terminal-tools-announcement-dialog {
|
|
258
|
+
animation: vim-announcement-backdrop-in 160ms ease-out both;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* The "New · Watch:" corner block on the announcement footage, flush to the
|
|
263
|
+
* frame's top-left edge. Colors come from the dialog's own --announce-brand*
|
|
264
|
+
* variables (Herdr Annotate's badge palette, deliberately not app tokens); the
|
|
265
|
+
* periwinkle edge runs only along the two sides that meet the footage. The
|
|
266
|
+
* sheen is a slow diagonal highlight across the whole block every 4s, never a
|
|
267
|
+
* blink; the dialog drops the --sheen class under reduced motion and the media
|
|
268
|
+
* query below is the backstop. */
|
|
269
|
+
.terminal-tools-announcement-tag {
|
|
270
|
+
position: absolute;
|
|
271
|
+
overflow: hidden;
|
|
272
|
+
background: var(--announce-brand, #312b52);
|
|
273
|
+
color: var(--announce-brand-text, #c9c6f1);
|
|
274
|
+
border-right: 1px solid color-mix(in srgb, var(--announce-brand-accent, #B9C0FF) 55%, transparent);
|
|
275
|
+
border-bottom: 1px solid color-mix(in srgb, var(--announce-brand-accent, #B9C0FF) 55%, transparent);
|
|
276
|
+
box-shadow:
|
|
277
|
+
2px 2px 0 rgba(0, 0, 0, 0.18),
|
|
278
|
+
4px 6px 18px rgba(0, 0, 0, 0.32);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.terminal-tools-announcement-tag--sheen::after {
|
|
282
|
+
content: '';
|
|
283
|
+
position: absolute;
|
|
284
|
+
inset: -1px;
|
|
285
|
+
pointer-events: none;
|
|
286
|
+
background: linear-gradient(
|
|
287
|
+
115deg,
|
|
288
|
+
transparent 38%,
|
|
289
|
+
color-mix(in srgb, var(--announce-brand-accent, #B9C0FF) 34%, transparent) 50%,
|
|
290
|
+
transparent 62%
|
|
291
|
+
);
|
|
292
|
+
transform: translateX(-100%);
|
|
293
|
+
animation: terminal-tools-announcement-sheen 4s ease-in-out infinite;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
@keyframes terminal-tools-announcement-sheen {
|
|
297
|
+
0% { transform: translateX(-100%); }
|
|
298
|
+
45%, 100% { transform: translateX(100%); }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@media (prefers-reduced-motion: reduce) {
|
|
302
|
+
.terminal-tools-announcement-tag--sheen::after {
|
|
303
|
+
animation: none;
|
|
304
|
+
display: none;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
246
308
|
.vim-announcement-switch {
|
|
247
309
|
transition:
|
|
248
310
|
border-color 140ms ease,
|
package/types.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { DiagramAnchor } from '@plannotator/core/diagram-anchor';
|
|
2
|
+
|
|
3
|
+
export type { DiagramAnchor } from '@plannotator/core/diagram-anchor';
|
|
4
|
+
|
|
1
5
|
export enum AnnotationType {
|
|
2
6
|
DELETION = 'DELETION',
|
|
3
7
|
COMMENT = 'COMMENT',
|
|
@@ -84,6 +88,7 @@ export interface Annotation {
|
|
|
84
88
|
htmlAnchor?: HtmlElementAnchor; // raw-HTML pinpoint: serialized element anchor for reliable restoration
|
|
85
89
|
elementContext?: HtmlElementContext; // raw-HTML / live-app pinpoint: bounded agent-facing description of the primary element (never used by restore)
|
|
86
90
|
htmlAdditionalTargets?: HtmlAnnotationTarget[]; // raw-HTML shift-click multi-select: extra elements this one comment covers (primary stays htmlAnchor/originalText)
|
|
91
|
+
diagramAnchor?: DiagramAnchor; // a comment on a rendered diagram part (Mermaid / Graphviz fence): the part's own id, label and document source line; the highlighter skips it and the diagram overlay restores it (see @plannotator/core/diagram-anchor)
|
|
87
92
|
// web-highlighter metadata for cross-element selections
|
|
88
93
|
startMeta?: AnnotationTextMeta;
|
|
89
94
|
endMeta?: AnnotationTextMeta;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-file annotation scope (multi-document annotate sessions).
|
|
3
|
+
*
|
|
4
|
+
* A folder session spreads one body of feedback across many documents, but the
|
|
5
|
+
* annotations panel only ever showed the open file's comments. This module owns
|
|
6
|
+
* the pure half of the "All files" view: which documents have feedback, how
|
|
7
|
+
* they are ordered and labelled, and which scope the panel opens on.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is pure except the two preference accessors, which use the
|
|
10
|
+
* shared storage backend (cookies by default) like the other panel prefs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { normalizeBrowserPath, pathIsInsideDir } from '@plannotator/core/browser-paths';
|
|
14
|
+
import type { Annotation } from '../types';
|
|
15
|
+
import { getItem, setItem } from './storage';
|
|
16
|
+
|
|
17
|
+
/** `current` = only the open document (the incumbent view). `all` = every document. */
|
|
18
|
+
export type AnnotationScope = 'current' | 'all';
|
|
19
|
+
|
|
20
|
+
const ANNOTATION_SCOPE_KEY = 'plannotator-annotation-scope';
|
|
21
|
+
|
|
22
|
+
/** The user's explicit last choice, or null when they have never chosen. */
|
|
23
|
+
export function getAnnotationScopePreference(): AnnotationScope | null {
|
|
24
|
+
const raw = getItem(ANNOTATION_SCOPE_KEY);
|
|
25
|
+
return raw === 'all' || raw === 'current' ? raw : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function setAnnotationScopePreference(scope: AnnotationScope): void {
|
|
29
|
+
setItem(ANNOTATION_SCOPE_KEY, scope);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Group key for the open document when it has no path of its own — plan review,
|
|
34
|
+
* where the plan is the document and `sourceFilePath` is annotate-only. Without
|
|
35
|
+
* a key the root document forms no group at all, so the "All files" view hid the
|
|
36
|
+
* plan's own comments while listing every linked document's. It contains no path
|
|
37
|
+
* separator, so `normalizeBrowserPath` leaves it alone, and the angle brackets
|
|
38
|
+
* keep it from colliding with a real file path (illegal in Windows paths, and
|
|
39
|
+
* absent from the absolute paths these groups carry).
|
|
40
|
+
*/
|
|
41
|
+
export const ROOT_DOCUMENT_GROUP_KEY = '<plannotator-root-document>';
|
|
42
|
+
|
|
43
|
+
export interface AnnotationDocumentInput {
|
|
44
|
+
/** Absolute path of the document. */
|
|
45
|
+
path: string;
|
|
46
|
+
annotations: readonly Annotation[];
|
|
47
|
+
/** Display name override. Only the pathless root document needs one: every
|
|
48
|
+
* other group derives its label from its path. */
|
|
49
|
+
label?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AnnotationDocumentGroup {
|
|
53
|
+
path: string;
|
|
54
|
+
/** Path relative to the nearest session root, else the bare file name. */
|
|
55
|
+
label: string;
|
|
56
|
+
annotations: Annotation[];
|
|
57
|
+
isCurrent: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Display name for a document: its path relative to the deepest session root
|
|
62
|
+
* that contains it (the same `${dir}/${node.path}` shape the file browser
|
|
63
|
+
* renders), falling back to the bare file name when no root matches.
|
|
64
|
+
*/
|
|
65
|
+
export function documentLabel(path: string, roots: readonly string[] = []): string {
|
|
66
|
+
const normalized = normalizeBrowserPath(path);
|
|
67
|
+
let best = '';
|
|
68
|
+
for (const root of roots) {
|
|
69
|
+
if (!root) continue;
|
|
70
|
+
const normalizedRoot = normalizeBrowserPath(root);
|
|
71
|
+
if (!pathIsInsideDir(normalized, normalizedRoot)) continue;
|
|
72
|
+
if (normalizedRoot.length > best.length) best = normalizedRoot;
|
|
73
|
+
}
|
|
74
|
+
if (best && normalized.length > best.length) {
|
|
75
|
+
return normalized.slice(best.endsWith('/') ? best.length : best.length + 1);
|
|
76
|
+
}
|
|
77
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
78
|
+
return lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One group per document that actually carries feedback, the open document
|
|
83
|
+
* first and the rest ordered by path. Documents without annotations are
|
|
84
|
+
* dropped: the view answers "where is my feedback", not "what files exist".
|
|
85
|
+
*/
|
|
86
|
+
export function groupAnnotationsByDocument(
|
|
87
|
+
documents: Iterable<AnnotationDocumentInput>,
|
|
88
|
+
currentPath: string | null,
|
|
89
|
+
roots: readonly string[] = [],
|
|
90
|
+
): AnnotationDocumentGroup[] {
|
|
91
|
+
const normalizedCurrent = currentPath ? normalizeBrowserPath(currentPath) : null;
|
|
92
|
+
const groups: AnnotationDocumentGroup[] = [];
|
|
93
|
+
const seen = new Set<string>();
|
|
94
|
+
for (const doc of documents) {
|
|
95
|
+
const path = normalizeBrowserPath(doc.path);
|
|
96
|
+
if (!path || seen.has(path) || doc.annotations.length === 0) continue;
|
|
97
|
+
seen.add(path);
|
|
98
|
+
groups.push({
|
|
99
|
+
path,
|
|
100
|
+
label: doc.label ?? documentLabel(path, roots),
|
|
101
|
+
annotations: [...doc.annotations],
|
|
102
|
+
isCurrent: path === normalizedCurrent,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
groups.sort((a, b) => {
|
|
106
|
+
if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1;
|
|
107
|
+
return a.path.localeCompare(b.path);
|
|
108
|
+
});
|
|
109
|
+
return groups;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The panel's groups for a session: every cached document plus the OPEN one,
|
|
114
|
+
* whose live list (externals included) is the same set its "This file" timeline
|
|
115
|
+
* renders — the cache copy behind it can be stale.
|
|
116
|
+
*
|
|
117
|
+
* The open document is always contributed, under `current.key`. In plan review
|
|
118
|
+
* that key is {@link ROOT_DOCUMENT_GROUP_KEY}: the plan has no path, and keying
|
|
119
|
+
* groups by path alone dropped it from the list, so the "All files" view hid
|
|
120
|
+
* the reviewer's own comments on the document in front of them.
|
|
121
|
+
*/
|
|
122
|
+
export function buildAnnotationDocumentGroups(input: {
|
|
123
|
+
/** Documents held in the linked-doc cache, keyed by path. */
|
|
124
|
+
cached: Iterable<readonly [string, readonly Annotation[]]>;
|
|
125
|
+
current: { key: string; label?: string; annotations: readonly Annotation[] };
|
|
126
|
+
roots?: readonly string[];
|
|
127
|
+
}): AnnotationDocumentGroup[] {
|
|
128
|
+
const byPath = new Map<string, readonly Annotation[]>();
|
|
129
|
+
for (const [path, annotations] of input.cached) byPath.set(path, annotations);
|
|
130
|
+
byPath.set(input.current.key, input.current.annotations);
|
|
131
|
+
return groupAnnotationsByDocument(
|
|
132
|
+
Array.from(byPath, ([path, annotations]) => ({
|
|
133
|
+
path,
|
|
134
|
+
annotations,
|
|
135
|
+
label: path === input.current.key ? input.current.label : undefined,
|
|
136
|
+
})),
|
|
137
|
+
input.current.key,
|
|
138
|
+
input.roots ?? [],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Which scope the panel opens on for the document that just became active.
|
|
144
|
+
*
|
|
145
|
+
* The saved preference wins, with one exception that is the whole point of the
|
|
146
|
+
* feature: landing on a file with no feedback while feedback exists elsewhere
|
|
147
|
+
* would otherwise show "No annotations yet" next to a session full of comments.
|
|
148
|
+
*/
|
|
149
|
+
export function resolveInitialAnnotationScope(input: {
|
|
150
|
+
saved: AnnotationScope | null;
|
|
151
|
+
/** Annotations on the document that is open now. */
|
|
152
|
+
currentCount: number;
|
|
153
|
+
/** Annotations on every other document in the session. */
|
|
154
|
+
otherCount: number;
|
|
155
|
+
}): AnnotationScope {
|
|
156
|
+
if (input.saved === 'all') return 'all';
|
|
157
|
+
if (input.currentCount === 0 && input.otherCount > 0) return 'all';
|
|
158
|
+
return 'current';
|
|
159
|
+
}
|