@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.
Files changed (52) hide show
  1. package/HANDOFF.md +140 -10
  2. package/README.md +9 -5
  3. package/components/AnnotationPanel.tsx +244 -13
  4. package/components/CommentPopover.tsx +91 -2
  5. package/components/DiagramBlock.tsx +376 -0
  6. package/components/GraphvizBlock.tsx +22 -597
  7. package/components/HtmlSurfaceControls.tsx +188 -23
  8. package/components/ListMarker.tsx +10 -1
  9. package/components/MermaidBlock.tsx +17 -630
  10. package/components/TableOfContents.tsx +5 -1
  11. package/components/TerminalToolsAnnouncementDialog.tsx +454 -0
  12. package/components/Viewer.tsx +67 -3
  13. package/components/blocks/AlertBlock.tsx +7 -2
  14. package/components/diagram/DiagramCanvas.tsx +431 -0
  15. package/components/diagram/DiagramComposer.tsx +135 -0
  16. package/components/diagram/DiagramOverlay.tsx +215 -0
  17. package/components/diagram/DiagramPopout.tsx +70 -0
  18. package/components/diagram/DiagramSourcePane.tsx +244 -0
  19. package/components/diagram/DiagramViewer.tsx +276 -0
  20. package/components/diagram/anchorClaims.ts +71 -0
  21. package/components/diagram/index.ts +36 -0
  22. package/components/diagram/useDiagramComments.ts +341 -0
  23. package/components/diagram/useDiagramRender.ts +91 -0
  24. package/components/diagram/useDiagramSourceDraft.ts +143 -0
  25. package/components/diagram/useDiagramViewport.ts +156 -0
  26. package/components/html-viewer/HtmlViewer.tsx +32 -0
  27. package/components/html-viewer/bridge-script.asset.js +121 -9
  28. package/components/html-viewer/bridge-script.lite.ts +1 -1
  29. package/components/html-viewer/bridge-script.ts +133 -9
  30. package/components/html-viewer/useHtmlAnnotation.ts +44 -151
  31. package/hooks/useAnnotationHighlighter.ts +469 -14
  32. package/hooks/useLinkedDoc.ts +100 -9
  33. package/package.json +6 -4
  34. package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +21 -7
  35. package/styles.css +1 -1
  36. package/theme.css +62 -0
  37. package/types.ts +5 -0
  38. package/utils/annotationScope.ts +159 -0
  39. package/utils/cssColor.ts +463 -0
  40. package/utils/diagram-anchor-graphviz.ts +143 -0
  41. package/utils/diagram-anchor.ts +401 -0
  42. package/utils/diagram-projection.ts +66 -0
  43. package/utils/diagram-render.ts +668 -0
  44. package/utils/graphviz.ts +93 -0
  45. package/utils/htmlChrome.ts +70 -5
  46. package/utils/htmlLinkNavigation.ts +196 -0
  47. package/utils/mermaid-eager.ts +13 -11
  48. package/utils/mermaid.ts +19 -10
  49. package/utils/mermaidTheme.ts +732 -0
  50. package/utils/parser.ts +36 -7
  51. package/utils/terminalToolsAnnouncement.ts +76 -0
  52. 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
- const selector = target?.anchor?.selector;
1210
- if (typeof selector === 'string' && selector) locators.push(`\`${safeInline(selector, 300)}\``);
1211
- const path = target?.context?.path;
1212
- if (typeof path === 'string' && path) locators.push(`\`${safeInline(path, 512)}\``);
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
- output += elementContextExportBlock(ann, opts);
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
+ }
@@ -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
- }