@plannotator/ui 0.40.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 +46 -5
- package/README.md +6 -2
- package/components/DiagramBlock.tsx +376 -0
- package/components/GraphvizBlock.tsx +22 -597
- package/components/MermaidBlock.tsx +17 -664
- package/components/Viewer.tsx +42 -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/bridge-script.asset.js +25 -0
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +37 -0
- package/hooks/useAnnotationHighlighter.ts +5 -0
- package/package.json +5 -3
- package/styles.css +1 -1
- package/types.ts +5 -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/parser.ts +15 -1
- package/components/mermaidSvg.ts +0 -33
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graphviz runtime slot — the same shape as `./mermaid`, one per engine so
|
|
3
|
+
* a host fills either independently.
|
|
4
|
+
*
|
|
5
|
+
* ONE code path feeds the renderer slot (`./diagram-render`):
|
|
6
|
+
* `loadGraphvizRuntime()`. It resolves at once from a filled slot and
|
|
7
|
+
* otherwise imports the engine lazily: `@viz-js/viz` (about 1.2 MB of
|
|
8
|
+
* Emscripten JS with the wasm inlined) is fetched on the first dot fence, a
|
|
9
|
+
* failed import is dropped from the memo so the next call issues a fresh
|
|
10
|
+
* `import()`, and the block re-attempts once and offers Retry. In
|
|
11
|
+
* Plannotator's single-file builds the import is inlined and resolves from
|
|
12
|
+
* the bundle; a host that bundles by route fetches it on demand.
|
|
13
|
+
*
|
|
14
|
+
* This module has NO static import of `@viz-js/viz`; the only place the
|
|
15
|
+
* dependency is named at runtime is the default loader's `import()`.
|
|
16
|
+
*/
|
|
17
|
+
import type { Viz } from '@viz-js/viz';
|
|
18
|
+
|
|
19
|
+
export type GraphvizRuntime = Viz;
|
|
20
|
+
|
|
21
|
+
/** Who filled the slot. */
|
|
22
|
+
export type GraphvizRuntimeSource = 'loader' | 'host';
|
|
23
|
+
|
|
24
|
+
export type GraphvizRuntimeLoader = () => Promise<Viz>;
|
|
25
|
+
|
|
26
|
+
/** Default lazy loader: import the engine and instantiate its wasm once. */
|
|
27
|
+
const defaultGraphvizLoader: GraphvizRuntimeLoader = () => import('@viz-js/viz').then((m) => m.instance());
|
|
28
|
+
|
|
29
|
+
let runtime: Viz | null = null;
|
|
30
|
+
let runtimeSource: GraphvizRuntimeSource | null = null;
|
|
31
|
+
let loader: GraphvizRuntimeLoader = defaultGraphvizLoader;
|
|
32
|
+
let pending: Promise<Viz> | null = null;
|
|
33
|
+
|
|
34
|
+
/** Delay before the one automatic re-attempt after a failed lazy import. */
|
|
35
|
+
let retryDelayMs = 750;
|
|
36
|
+
|
|
37
|
+
/** Current runtime, or `null` while the slot is empty. */
|
|
38
|
+
export function getGraphvizRuntime(): Viz | null {
|
|
39
|
+
return runtime;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** How the current runtime was registered, or `null` while the slot is empty. */
|
|
43
|
+
export function getGraphvizRuntimeSource(): GraphvizRuntimeSource | null {
|
|
44
|
+
return runtimeSource;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Register an already-instantiated engine (a host with its own import). */
|
|
48
|
+
export function setGraphvizRuntime(next: Viz, source: GraphvizRuntimeSource = 'host'): void {
|
|
49
|
+
runtime = next;
|
|
50
|
+
runtimeSource = source;
|
|
51
|
+
pending = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The renderer's retry delay for the lazy path. */
|
|
55
|
+
export function getGraphvizRetryDelayMs(): number {
|
|
56
|
+
return retryDelayMs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the engine: at once from a filled slot, otherwise through the
|
|
61
|
+
* loader. A rejected load is dropped from the memo so the next call (the
|
|
62
|
+
* automatic re-attempt, a later mount, or the Retry button) issues a fresh
|
|
63
|
+
* `import()` instead of replaying the cached rejection.
|
|
64
|
+
*/
|
|
65
|
+
export function loadGraphvizRuntime(): Promise<Viz> {
|
|
66
|
+
if (runtime) return Promise.resolve(runtime);
|
|
67
|
+
if (!pending) {
|
|
68
|
+
const attempt = loader().then(
|
|
69
|
+
(loaded) => {
|
|
70
|
+
setGraphvizRuntime(loaded, 'loader');
|
|
71
|
+
return loaded;
|
|
72
|
+
},
|
|
73
|
+
(err: unknown) => {
|
|
74
|
+
if (pending === attempt) pending = null;
|
|
75
|
+
throw err;
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
pending = attempt;
|
|
79
|
+
}
|
|
80
|
+
return pending;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Test hook: empty the slot, stand in for the lazy import, shorten the retry delay. */
|
|
84
|
+
export function __setGraphvizRuntimeLoaderForTests(
|
|
85
|
+
next: GraphvizRuntimeLoader | undefined,
|
|
86
|
+
options?: { retryDelayMs?: number },
|
|
87
|
+
): void {
|
|
88
|
+
runtime = null;
|
|
89
|
+
runtimeSource = null;
|
|
90
|
+
pending = null;
|
|
91
|
+
loader = next ?? defaultGraphvizLoader;
|
|
92
|
+
retryDelayMs = options?.retryDelayMs ?? 750;
|
|
93
|
+
}
|
package/utils/parser.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Block, Annotation, CodeAnnotation, EditorAnnotation, ImageAttachment } from '../types';
|
|
2
2
|
import { planDenyFeedback } from '@plannotator/core/feedback-templates';
|
|
3
3
|
import { resolveReplyParents } from '@plannotator/core/annotation-threads';
|
|
4
|
+
import { diagramAnchorLocationLine, parseDiagramAnchor } from '@plannotator/core/diagram-anchor';
|
|
4
5
|
import { skillReferenceExportBlock } from './skillReferences';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -1249,7 +1250,7 @@ export const exportAnnotationEntry = (ann: any, opts: ElementContextExportOption
|
|
|
1249
1250
|
output += `[${ann.text}] ${commentHeadingLine(ann)}\n`;
|
|
1250
1251
|
if (ann.quickLabelTip) output += `> ${ann.quickLabelTip}\n`;
|
|
1251
1252
|
} else {
|
|
1252
|
-
output += `${commentHeadingLine(ann)}\n> ${ann?.text ?? ''}\n`;
|
|
1253
|
+
output += `${commentHeadingLine(ann)}\n${diagramLocationExportLine(ann)}> ${ann?.text ?? ''}\n`;
|
|
1253
1254
|
}
|
|
1254
1255
|
}
|
|
1255
1256
|
const resolvedOpts: ElementContextExportOptions = {
|
|
@@ -1267,6 +1268,16 @@ export const exportAnnotationEntry = (ann: any, opts: ElementContextExportOption
|
|
|
1267
1268
|
return output;
|
|
1268
1269
|
};
|
|
1269
1270
|
|
|
1271
|
+
/** The location line under a comment made on a rendered diagram part:
|
|
1272
|
+
* `Diagram node Approve? (D), line 4` — the part's own id (what the agent
|
|
1273
|
+
* greps the fence for) and the DOCUMENT line that declares it. Emits
|
|
1274
|
+
* nothing for every other annotation, keeping their output byte-identical;
|
|
1275
|
+
* a malformed anchor (an older or foreign writer) is skipped, never thrown. */
|
|
1276
|
+
const diagramLocationExportLine = (ann: any): string => {
|
|
1277
|
+
const anchor = ann?.diagramAnchor === undefined ? null : parseDiagramAnchor(ann.diagramAnchor);
|
|
1278
|
+
return anchor === null ? '' : `${safeInline(diagramAnchorLocationLine(anchor), 600)}\n`;
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1270
1281
|
const lineLabelForAnnotation = (blocks: Block[], ann: any): string | null => {
|
|
1271
1282
|
if (!ann.blockId || ann.type === 'GLOBAL_COMMENT') return null;
|
|
1272
1283
|
if (typeof ann.blockId === 'string' && ann.blockId.startsWith('diff-block-')) return null;
|
|
@@ -1435,11 +1446,13 @@ export const exportAnnotations = (
|
|
|
1435
1446
|
case 'COMMENT':
|
|
1436
1447
|
if (ann.isQuickLabel) {
|
|
1437
1448
|
output += `[${ann.text}] ${commentHeadingLine(ann)}\n`;
|
|
1449
|
+
output += diagramLocationExportLine(ann);
|
|
1438
1450
|
if (ann.quickLabelTip) {
|
|
1439
1451
|
output += `> ${ann.quickLabelTip}\n`;
|
|
1440
1452
|
}
|
|
1441
1453
|
} else {
|
|
1442
1454
|
output += `${commentHeadingLine(ann)}\n`;
|
|
1455
|
+
output += diagramLocationExportLine(ann);
|
|
1443
1456
|
output += `> ${ann.text}\n`;
|
|
1444
1457
|
}
|
|
1445
1458
|
break;
|
|
@@ -1554,6 +1567,7 @@ export const exportLinkedDocAnnotations = (
|
|
|
1554
1567
|
|
|
1555
1568
|
case 'COMMENT':
|
|
1556
1569
|
output += `${commentHeadingLine(ann)}\n`;
|
|
1570
|
+
output += diagramLocationExportLine(ann);
|
|
1557
1571
|
output += `> ${ann.text}\n`;
|
|
1558
1572
|
break;
|
|
1559
1573
|
|
package/components/mermaidSvg.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// Pure SVG-markup helpers for MermaidBlock. Kept free of React and the
|
|
2
|
-
// mermaid library so they can be unit-tested without loading mermaid's
|
|
3
|
-
// browser-only `initialize()` (which throws in headless test environments).
|
|
4
|
-
|
|
5
|
-
// Bake sizing attrs into the SVG markup so they survive repeated
|
|
6
|
-
// dangerouslySetInnerHTML re-injection — imperative setAttribute gets wiped.
|
|
7
|
-
export function normalizeMermaidSvgMarkup(markup: string): string {
|
|
8
|
-
return markup.replace(/<svg\b([^>]*)>/i, (_match, attrs: string) => {
|
|
9
|
-
let next = attrs;
|
|
10
|
-
|
|
11
|
-
if (/\bstyle\s*=\s*"/i.test(next)) {
|
|
12
|
-
next = next.replace(/\bstyle\s*=\s*"([^"]*)"/i, (_m, styleVal: string) => {
|
|
13
|
-
const rules = styleVal
|
|
14
|
-
.split(';')
|
|
15
|
-
.map((s) => s.trim())
|
|
16
|
-
.filter((s) => s.length > 0 && !/^max-width\s*:/i.test(s));
|
|
17
|
-
rules.push('max-width: none');
|
|
18
|
-
return `style="${rules.join('; ')}"`;
|
|
19
|
-
});
|
|
20
|
-
} else {
|
|
21
|
-
next += ' style="max-width: none"';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!/\bpreserveAspectRatio\s*=/i.test(next)) {
|
|
25
|
-
next += ' preserveAspectRatio="xMidYMid meet"';
|
|
26
|
-
}
|
|
27
|
-
if (!/\bheight\s*=/i.test(next)) {
|
|
28
|
-
next += ' height="100%"';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return `<svg${next}>`;
|
|
32
|
-
});
|
|
33
|
-
}
|