@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/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
|
/**
|
|
@@ -1131,6 +1132,9 @@ export interface ElementContextExportOptions {
|
|
|
1131
1132
|
/** Emit the live-app route line. The grouped export already prints a
|
|
1132
1133
|
* `## Page:` heading, so it passes false; a single copied entry passes true. */
|
|
1133
1134
|
includeRoute?: boolean;
|
|
1135
|
+
/** Print the identity lines WITHOUT the fenced outline, for model turns where
|
|
1136
|
+
* the 600-char outline is the expensive part. Default true. */
|
|
1137
|
+
includeOutline?: boolean;
|
|
1134
1138
|
}
|
|
1135
1139
|
|
|
1136
1140
|
/** The agent-facing element block for a raw-HTML / live-app pinpoint: a
|
|
@@ -1141,8 +1145,9 @@ export interface ElementContextExportOptions {
|
|
|
1141
1145
|
export const elementContextExportBlock = (ann: any, opts: ElementContextExportOptions = {}): string => {
|
|
1142
1146
|
const context = ann?.elementContext;
|
|
1143
1147
|
if (!context || typeof context !== 'object' || typeof context.tag !== 'string') return '';
|
|
1148
|
+
const includeOutline = opts.includeOutline ?? true;
|
|
1144
1149
|
let block = '';
|
|
1145
|
-
if (typeof context.outline === 'string' && context.outline.trim()) {
|
|
1150
|
+
if (includeOutline && typeof context.outline === 'string' && context.outline.trim()) {
|
|
1146
1151
|
// Fence at 4 backticks; the boundary already defuses 3+ runs inside the
|
|
1147
1152
|
// outline, and a 4-run here cannot be closed by anything the page wrote.
|
|
1148
1153
|
const outline = context.outline.replace(/`{3,}/g, "'''").trim();
|
|
@@ -1205,11 +1210,18 @@ const additionalTargetsExportBlock = (ann: any): string => {
|
|
|
1205
1210
|
// Element identity for the agent, one line per extra target: the
|
|
1206
1211
|
// selector and the path (a full context block per target would swamp
|
|
1207
1212
|
// the comment; the rest of the context stays persisted, not exported).
|
|
1213
|
+
//
|
|
1214
|
+
// Gated on the target carrying element context, which is what makes the
|
|
1215
|
+
// element-context work additive in the literal sense it claims: a target
|
|
1216
|
+
// captured before it existed (or restored from an older draft) has an
|
|
1217
|
+
// anchor but no context, and exports byte-identically to before.
|
|
1208
1218
|
const locators: string[] = [];
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1219
|
+
if (target?.context && typeof target.context === 'object') {
|
|
1220
|
+
const selector = target?.anchor?.selector;
|
|
1221
|
+
if (typeof selector === 'string' && selector) locators.push(`\`${safeInline(selector, 300)}\``);
|
|
1222
|
+
const path = target?.context?.path;
|
|
1223
|
+
if (typeof path === 'string' && path) locators.push(`\`${safeInline(path, 512)}\``);
|
|
1224
|
+
}
|
|
1213
1225
|
block += `- ${label}"${clipped}"${locators.length ? ` — ${locators.join(' · ')}` : ''}\n`;
|
|
1214
1226
|
});
|
|
1215
1227
|
return block;
|
|
@@ -1238,10 +1250,14 @@ export const exportAnnotationEntry = (ann: any, opts: ElementContextExportOption
|
|
|
1238
1250
|
output += `[${ann.text}] ${commentHeadingLine(ann)}\n`;
|
|
1239
1251
|
if (ann.quickLabelTip) output += `> ${ann.quickLabelTip}\n`;
|
|
1240
1252
|
} else {
|
|
1241
|
-
output += `${commentHeadingLine(ann)}\n> ${ann?.text ?? ''}\n`;
|
|
1253
|
+
output += `${commentHeadingLine(ann)}\n${diagramLocationExportLine(ann)}> ${ann?.text ?? ''}\n`;
|
|
1242
1254
|
}
|
|
1243
1255
|
}
|
|
1244
|
-
|
|
1256
|
+
const resolvedOpts: ElementContextExportOptions = {
|
|
1257
|
+
includeRoute: opts.includeRoute ?? true,
|
|
1258
|
+
...(opts.includeOutline !== undefined ? { includeOutline: opts.includeOutline } : {}),
|
|
1259
|
+
};
|
|
1260
|
+
output += elementContextExportBlock(ann, resolvedOpts);
|
|
1245
1261
|
output += additionalTargetsExportBlock(ann);
|
|
1246
1262
|
if (Array.isArray(ann?.images) && ann.images.length > 0) {
|
|
1247
1263
|
output += `**Attached images:**\n`;
|
|
@@ -1252,6 +1268,16 @@ export const exportAnnotationEntry = (ann: any, opts: ElementContextExportOption
|
|
|
1252
1268
|
return output;
|
|
1253
1269
|
};
|
|
1254
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
|
+
|
|
1255
1281
|
const lineLabelForAnnotation = (blocks: Block[], ann: any): string | null => {
|
|
1256
1282
|
if (!ann.blockId || ann.type === 'GLOBAL_COMMENT') return null;
|
|
1257
1283
|
if (typeof ann.blockId === 'string' && ann.blockId.startsWith('diff-block-')) return null;
|
|
@@ -1420,11 +1446,13 @@ export const exportAnnotations = (
|
|
|
1420
1446
|
case 'COMMENT':
|
|
1421
1447
|
if (ann.isQuickLabel) {
|
|
1422
1448
|
output += `[${ann.text}] ${commentHeadingLine(ann)}\n`;
|
|
1449
|
+
output += diagramLocationExportLine(ann);
|
|
1423
1450
|
if (ann.quickLabelTip) {
|
|
1424
1451
|
output += `> ${ann.quickLabelTip}\n`;
|
|
1425
1452
|
}
|
|
1426
1453
|
} else {
|
|
1427
1454
|
output += `${commentHeadingLine(ann)}\n`;
|
|
1455
|
+
output += diagramLocationExportLine(ann);
|
|
1428
1456
|
output += `> ${ann.text}\n`;
|
|
1429
1457
|
}
|
|
1430
1458
|
break;
|
|
@@ -1539,6 +1567,7 @@ export const exportLinkedDocAnnotations = (
|
|
|
1539
1567
|
|
|
1540
1568
|
case 'COMMENT':
|
|
1541
1569
|
output += `${commentHeadingLine(ann)}\n`;
|
|
1570
|
+
output += diagramLocationExportLine(ann);
|
|
1542
1571
|
output += `> ${ann.text}\n`;
|
|
1543
1572
|
break;
|
|
1544
1573
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time gate for the terminal-tools announcement (Plannotator TUI and
|
|
3
|
+
* Herdr Annotate). Cookie-backed like the other announcement gates, so a
|
|
4
|
+
* dismissal survives Plannotator's random localhost ports, and shared by the
|
|
5
|
+
* plan editor, the annotate surfaces and the code review editor: dismissing it
|
|
6
|
+
* anywhere retires it everywhere.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { storage } from './storage';
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'plannotator-announce-tui-herdr-seen';
|
|
12
|
+
// Bump to re-announce after a meaningful revision.
|
|
13
|
+
const CURRENT_VERSION = '1';
|
|
14
|
+
|
|
15
|
+
export function needsTerminalToolsAnnouncement(): boolean {
|
|
16
|
+
return storage.getItem(STORAGE_KEY) !== CURRENT_VERSION;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function markTerminalToolsAnnouncementSeen(): void {
|
|
20
|
+
storage.setItem(STORAGE_KEY, CURRENT_VERSION);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TerminalToolsAnnouncementGateState {
|
|
24
|
+
/**
|
|
25
|
+
* Latched at mount from needsTerminalToolsAnnouncement(). Latched rather than
|
|
26
|
+
* read per render so a dismissal cannot unmount the dialog before its own
|
|
27
|
+
* click handler finishes.
|
|
28
|
+
*/
|
|
29
|
+
readonly announcementPending: boolean;
|
|
30
|
+
/** The app has not finished loading its initial payload. */
|
|
31
|
+
readonly isLoading: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The session has no author to address: archive browsing, a read-only shared
|
|
34
|
+
* plan, and any session with no Plannotator server behind it (the share
|
|
35
|
+
* portal's root and its demo plan included — those are not "shared
|
|
36
|
+
* sessions", so the host must fold that in). Telling a viewer about a CLI
|
|
37
|
+
* they did not open is noise, and the cookie is deliberately NOT consumed,
|
|
38
|
+
* so the next authoring session still shows it.
|
|
39
|
+
*/
|
|
40
|
+
readonly readOnlySession: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Plannotator's compact touch shell. The panel is desktop-shaped (install
|
|
43
|
+
* commands to copy, four outbound links) and a phone is not where anyone
|
|
44
|
+
* installs a terminal tool. Also deferred rather than consumed.
|
|
45
|
+
*/
|
|
46
|
+
readonly compact: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Any other first-run dialog is on screen. The chain dialogs never stack.
|
|
49
|
+
*/
|
|
50
|
+
readonly otherFirstRunDialogVisible: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Chain gate for the announcement. It is LAST in each app's first-run dialog
|
|
55
|
+
* chain, after every dialog that asks the user to decide something (code
|
|
56
|
+
* review: guide intro, look-and-feel, review setup, edit mode, token hover
|
|
57
|
+
* cards; plan and annotate: look-and-feel, goal setup, permission mode).
|
|
58
|
+
*
|
|
59
|
+
* Last rather than first because none of those dialogs consume this cookie:
|
|
60
|
+
* a session that is busy asking questions defers the announcement to the next
|
|
61
|
+
* load instead of burning it. That also puts it in front of the right reader.
|
|
62
|
+
* Someone opening Plannotator for the first time is still learning this app;
|
|
63
|
+
* the people who should hear that it now runs in a terminal are the ones who
|
|
64
|
+
* already answered every setup question, and they see it on their next load.
|
|
65
|
+
*/
|
|
66
|
+
export function terminalToolsAnnouncementCanShow(
|
|
67
|
+
state: TerminalToolsAnnouncementGateState,
|
|
68
|
+
): boolean {
|
|
69
|
+
return (
|
|
70
|
+
state.announcementPending &&
|
|
71
|
+
!state.isLoading &&
|
|
72
|
+
!state.readOnlySession &&
|
|
73
|
+
!state.compact &&
|
|
74
|
+
!state.otherFirstRunDialogVisible
|
|
75
|
+
);
|
|
76
|
+
}
|
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
|
-
}
|