pmx-canvas 0.9.3 → 0.11.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 (237) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +9418 -2016
  3. package/CHANGELOG.md +54 -1
  4. package/dist/canvas/chunks/abnfDiagram-N423BO3Z-p8dtp6va.js +1 -0
  5. package/dist/canvas/chunks/architecture-TIHT7OUA-9tffvpsv.js +1 -0
  6. package/dist/canvas/chunks/architectureDiagram-T3A2C74G-5jhmxrzf.js +36 -0
  7. package/dist/canvas/chunks/blockDiagram-VBNYF7ZC-pw1fa9yb.js +132 -0
  8. package/dist/canvas/chunks/c4Diagram-5PPSVZJV-x07nq0tt.js +10 -0
  9. package/dist/canvas/chunks/classDiagram-JCYQIIEL-nc6g5x1r.js +1 -0
  10. package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-0qnbpwvh.js +1 -0
  11. package/dist/canvas/chunks/cose-bilkent-JH36ORCC-nm39npq0.js +1 -0
  12. package/dist/canvas/chunks/cynefin-VYW2F7L2-ys8p46ts.js +1 -0
  13. package/dist/canvas/chunks/cynefinDiagram-MW4NZA55-vdxrwahz.js +62 -0
  14. package/dist/canvas/chunks/dagre-VZM6K2ZE-bqdm7wpd.js +4 -0
  15. package/dist/canvas/chunks/diagram-7IWD3JNH-gs5gpr93.js +30 -0
  16. package/dist/canvas/chunks/diagram-B4RE2ZJO-cgwjg47s.js +3 -0
  17. package/dist/canvas/chunks/diagram-LBJQPF4R-w3vahpav.js +24 -0
  18. package/dist/canvas/chunks/diagram-Q27KOJAE-42837z6h.js +24 -0
  19. package/dist/canvas/chunks/diagram-UB23O5K3-4fz83q59.js +41 -0
  20. package/dist/canvas/chunks/ebnfDiagram-BXEA7PRR-zphtgegj.js +1 -0
  21. package/dist/canvas/chunks/erDiagram-JOGREHBK-zvbbdxp4.js +85 -0
  22. package/dist/canvas/chunks/eventmodeling-45OFAUF4-v9q6dn4c.js +1 -0
  23. package/dist/canvas/chunks/flowDiagram-UKHOOZJN-yrr5z8tc.js +1 -0
  24. package/dist/canvas/chunks/ganttDiagram-PKOTCBZU-282jzycm.js +292 -0
  25. package/dist/canvas/chunks/gitGraph-TEB2WS4Q-kdwb8m0a.js +1 -0
  26. package/dist/canvas/chunks/gitGraphDiagram-DS77QQ5N-vqqy6p1b.js +106 -0
  27. package/dist/canvas/chunks/index-0rk4gp1g.js +2 -0
  28. package/dist/canvas/chunks/index-194m3hc3.js +2 -0
  29. package/dist/canvas/chunks/index-1r72hdzb.js +301 -0
  30. package/dist/canvas/chunks/index-353m7b0p.js +2 -0
  31. package/dist/canvas/chunks/index-3cy1bzwh.js +2 -0
  32. package/dist/canvas/chunks/index-5tv8v1hk.js +1 -0
  33. package/dist/canvas/chunks/index-6ceya556.js +332 -0
  34. package/dist/canvas/chunks/index-6qmsnxwy.js +4 -0
  35. package/dist/canvas/chunks/index-d3b3gqe9.js +2 -0
  36. package/dist/canvas/chunks/index-fjtdggjn.js +2 -0
  37. package/dist/canvas/chunks/index-g3pwnqeb.js +2 -0
  38. package/dist/canvas/chunks/index-gf4e0et4.js +2 -0
  39. package/dist/canvas/chunks/index-hj99nz1f.js +1 -0
  40. package/dist/canvas/chunks/index-nprm96xp.js +16 -0
  41. package/dist/canvas/chunks/index-qj8fxwsf.js +2 -0
  42. package/dist/canvas/chunks/index-r2fyc9w1.js +89 -0
  43. package/dist/canvas/chunks/index-rft2hcz5.js +2 -0
  44. package/dist/canvas/chunks/index-rh7nv68n.js +2 -0
  45. package/dist/canvas/chunks/index-s4j678gj.js +2 -0
  46. package/dist/canvas/chunks/index-tb1wf525.js +179 -0
  47. package/dist/canvas/chunks/index-vv56h019.js +2 -0
  48. package/dist/canvas/chunks/index-w2mra17t.js +207 -0
  49. package/dist/canvas/chunks/index-wgv7en21.js +232 -0
  50. package/dist/canvas/chunks/index-yw8h9vha.js +157 -0
  51. package/dist/canvas/chunks/info-DKCQHKI2-wpzs6sz3.js +1 -0
  52. package/dist/canvas/chunks/infoDiagram-6WML65LV-v1yvggqj.js +2 -0
  53. package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-5hnq1a9p.js +70 -0
  54. package/dist/canvas/chunks/journeyDiagram-NVQOT4AX-zjcffveh.js +139 -0
  55. package/dist/canvas/chunks/kanban-definition-27J2QSJJ-dakqtfp1.js +89 -0
  56. package/dist/canvas/chunks/katex-dmg8k4zm.js +257 -0
  57. package/dist/canvas/chunks/mermaid.core-j2geswrd.js +12 -0
  58. package/dist/canvas/chunks/mindmap-definition-FAOFIHXS-142xhr3k.js +96 -0
  59. package/dist/canvas/chunks/packet-7NZHBO7P-xn439f0s.js +1 -0
  60. package/dist/canvas/chunks/pegDiagram-VL7TDLO6-5wvk7vsd.js +1 -0
  61. package/dist/canvas/chunks/pie-RZYD4A2V-0kf16rwh.js +1 -0
  62. package/dist/canvas/chunks/pieDiagram-7S7Q4E2Y-ztdhwycc.js +39 -0
  63. package/dist/canvas/chunks/quadrantDiagram-CIZ2JOQS-c5p9gmg6.js +7 -0
  64. package/dist/canvas/chunks/radar-I7S5WNFK-0g2pz36t.js +1 -0
  65. package/dist/canvas/chunks/railroad-3IZDKUUU-fcaran6j.js +1 -0
  66. package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-wgdya41q.js +1 -0
  67. package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-gy07a4ka.js +1 -0
  68. package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-ewk6wtkh.js +1 -0
  69. package/dist/canvas/chunks/railroadDiagram-AXF67PYL-xkmqkjwf.js +1 -0
  70. package/dist/canvas/chunks/requirementDiagram-LRYGKXZP-9wm2dyaw.js +84 -0
  71. package/dist/canvas/chunks/sankeyDiagram-W5VNT64P-2w19etgb.js +40 -0
  72. package/dist/canvas/chunks/sequenceDiagram-SI44F4Z6-0jn0vwmz.js +162 -0
  73. package/dist/canvas/chunks/sizeCapture-X5ZJPWSS-x75epwdq.js +1 -0
  74. package/dist/canvas/chunks/stateDiagram-OKZ733FA-schjpy6d.js +1 -0
  75. package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-k8sfp2e7.js +1 -0
  76. package/dist/canvas/chunks/swimlanes-SLNWSIFB-gkw4xce3.js +1 -0
  77. package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-nqysw67j.js +8 -0
  78. package/dist/canvas/chunks/timeline-definition-Z64GVDOM-x7nkcy01.js +120 -0
  79. package/dist/canvas/chunks/treeView-QDETBFTQ-cesw6gp0.js +1 -0
  80. package/dist/canvas/chunks/treemap-6X3UGDF4-zdv4knp9.js +1 -0
  81. package/dist/canvas/chunks/vennDiagram-T6HMQDX7-x8dqn9yf.js +34 -0
  82. package/dist/canvas/chunks/wardley-OPB4EBWU-44pdddez.js +1 -0
  83. package/dist/canvas/chunks/wardleyDiagram-T6FBY63Y-t1j52t9r.js +78 -0
  84. package/dist/canvas/chunks/xychartDiagram-ELKLHX3M-38t16pme.js +7 -0
  85. package/dist/canvas/global.css +389 -229
  86. package/dist/canvas/index.js +141 -211
  87. package/dist/canvas/mcp-app.html +4205 -554
  88. package/dist/canvas/mermaid-entry.js +338 -338
  89. package/dist/canvas/surface-theme.css +6 -6
  90. package/dist/json-render/index.css +1 -1
  91. package/dist/json-render/index.js +49 -49
  92. package/dist/types/client/canvas/BarHint.d.ts +5 -3
  93. package/dist/types/client/canvas/CanvasNode.d.ts +0 -11
  94. package/dist/types/client/canvas/EdgeLayer.d.ts +1 -2
  95. package/dist/types/client/canvas/auto-fit.d.ts +4 -5
  96. package/dist/types/client/mermaid-entry.d.ts +1 -2
  97. package/dist/types/client/mermaid-render.d.ts +1 -0
  98. package/dist/types/client/nodes/BoardNode.d.ts +1 -1
  99. package/dist/types/client/nodes/ExtAppFrame.d.ts +7 -90
  100. package/dist/types/client/nodes/McpAppNode.d.ts +1 -25
  101. package/dist/types/client/nodes/MermaidNode.d.ts +1 -8
  102. package/dist/types/client/nodes/ViewerFrame.d.ts +29 -0
  103. package/dist/types/client/nodes/ViewerHost.d.ts +6 -0
  104. package/dist/types/client/nodes/iframe-document-url.d.ts +3 -9
  105. package/dist/types/client/nodes/primitive-copy.d.ts +5 -0
  106. package/dist/types/client/nodes/use-iframe-content-height.d.ts +1 -1
  107. package/dist/types/client/nodes/use-surface-frame.d.ts +4 -6
  108. package/dist/types/client/nodes/viewer-runtime-state.d.ts +2 -1
  109. package/dist/types/client/state/canvas-store.d.ts +3 -1
  110. package/dist/types/client/state/intent-bridge.d.ts +1 -0
  111. package/dist/types/client/state/presentation.d.ts +17 -6
  112. package/dist/types/json-render/server.d.ts +2 -1
  113. package/dist/types/mcp/canvas-access.d.ts +1 -12
  114. package/dist/types/mcp/resource-subscriptions.d.ts +7 -0
  115. package/dist/types/mcp/server.d.ts +2 -0
  116. package/dist/types/server/ax-wait.d.ts +1 -0
  117. package/dist/types/server/canvas-operations.d.ts +1 -0
  118. package/dist/types/server/canvas-state.d.ts +2 -0
  119. package/dist/types/server/index.d.ts +11 -6
  120. package/dist/types/server/operations/http.d.ts +10 -0
  121. package/dist/types/server/operations/index.d.ts +2 -2
  122. package/dist/types/server/operations/invoker.d.ts +4 -3
  123. package/dist/types/server/operations/mcp-contracts.d.ts +20 -0
  124. package/dist/types/server/operations/mcp.d.ts +2 -0
  125. package/dist/types/server/operations/ops/batch.d.ts +1 -0
  126. package/dist/types/server/operations/ops/boards.d.ts +0 -1
  127. package/dist/types/server/operations/registry.d.ts +2 -2
  128. package/dist/types/server/operations/types.d.ts +12 -2
  129. package/dist/types/server/server.d.ts +2 -0
  130. package/dist/types/server/tour-control.d.ts +13 -3
  131. package/dist/types/server/webpage-node.d.ts +1 -0
  132. package/dist/types/shared/ax-surface-protocol.d.ts +0 -9
  133. package/dist/types/shared/tour.d.ts +8 -0
  134. package/docs/api-reference.md +2 -2
  135. package/docs/cli.md +1 -1
  136. package/docs/design/Chrome.dc.html +32 -6
  137. package/docs/design/Types1.dc.html +3 -2
  138. package/docs/design/Types2.dc.html +1 -1
  139. package/docs/design/Types3.dc.html +1 -1
  140. package/docs/design/Zoom.dc.html +37 -60
  141. package/docs/design/canvas.json +1 -1
  142. package/docs/design.md +169 -4
  143. package/docs/http-api.md +82 -1
  144. package/docs/mcp.md +101 -21
  145. package/docs/node-types.md +38 -3
  146. package/docs/plans/plan-009-tech-debt-backlog.md +2 -2
  147. package/docs/plans/plan-019-post-0.9-roadmap.md +402 -13
  148. package/docs/product-vision-2026-09.md +9 -0
  149. package/docs/sdk.md +5 -2
  150. package/package.json +2 -2
  151. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  152. package/plugins/pmx-canvas/plugin.json +1 -1
  153. package/plugins/pmx-canvas/runtime.json +1 -1
  154. package/plugins/pmx-canvas/tools.json +9418 -2016
  155. package/skills/pmx-canvas/SKILL.md +19 -21
  156. package/skills/pmx-canvas/references/full-reference.md +44 -6
  157. package/skills/pmx-canvas/references/installing-pmx-canvas.md +7 -7
  158. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +3 -5
  159. package/src/client/canvas/BarHint.tsx +6 -3
  160. package/src/client/canvas/CanvasNode.tsx +34 -47
  161. package/src/client/canvas/CanvasViewport.tsx +6 -7
  162. package/src/client/canvas/ContextMenu.tsx +63 -3
  163. package/src/client/canvas/EdgeLayer.tsx +365 -169
  164. package/src/client/canvas/ExpandedNodeOverlay.tsx +1 -2
  165. package/src/client/canvas/Presentation.tsx +53 -3
  166. package/src/client/canvas/ToolRail.tsx +2 -0
  167. package/src/client/canvas/TopBar.tsx +24 -9
  168. package/src/client/canvas/auto-fit.ts +9 -11
  169. package/src/client/canvas/canvas-area.ts +1 -1
  170. package/src/client/canvas/use-focus-trap.ts +6 -1
  171. package/src/client/mermaid-entry.ts +6 -20
  172. package/src/client/mermaid-render.ts +39 -0
  173. package/src/client/nodes/AxStepControls.tsx +45 -69
  174. package/src/client/nodes/BoardNode.tsx +65 -9
  175. package/src/client/nodes/ContextNode.tsx +88 -396
  176. package/src/client/nodes/ExtAppFrame.tsx +283 -1462
  177. package/src/client/nodes/FileNode.tsx +92 -81
  178. package/src/client/nodes/HtmlNode.tsx +8 -36
  179. package/src/client/nodes/LedgerNode.tsx +31 -17
  180. package/src/client/nodes/MarkdownNode.tsx +8 -17
  181. package/src/client/nodes/McpAppNode.tsx +48 -154
  182. package/src/client/nodes/MermaidNode.tsx +80 -69
  183. package/src/client/nodes/PromptNode.tsx +12 -14
  184. package/src/client/nodes/ResponseNode.tsx +8 -6
  185. package/src/client/nodes/StatusNode.tsx +22 -7
  186. package/src/client/nodes/StatusSummary.tsx +5 -3
  187. package/src/client/nodes/TraceNode.tsx +8 -8
  188. package/src/client/nodes/ViewerFrame.tsx +262 -0
  189. package/src/client/nodes/ViewerHost.tsx +41 -0
  190. package/src/client/nodes/WebpageNode.tsx +22 -24
  191. package/src/client/nodes/iframe-document-url.ts +20 -27
  192. package/src/client/nodes/primitive-copy.ts +97 -0
  193. package/src/client/nodes/use-iframe-content-height.ts +34 -10
  194. package/src/client/nodes/use-surface-frame.ts +22 -11
  195. package/src/client/nodes/viewer-runtime-state.ts +41 -35
  196. package/src/client/state/canvas-store.ts +18 -12
  197. package/src/client/state/intent-bridge.ts +1 -0
  198. package/src/client/state/presentation.ts +135 -15
  199. package/src/client/state/sse-bridge.ts +18 -19
  200. package/src/client/theme/global.css +389 -229
  201. package/src/client/theme/surface-theme.css +6 -6
  202. package/src/json-render/charts/tufte-components.tsx +44 -2
  203. package/src/json-render/renderer/index.css +62 -2
  204. package/src/json-render/renderer/index.tsx +31 -7
  205. package/src/json-render/server.ts +3 -2
  206. package/src/mcp/canvas-access.ts +6 -47
  207. package/src/mcp/resource-subscriptions.ts +181 -0
  208. package/src/mcp/server.ts +185 -219
  209. package/src/server/ax-wait.ts +21 -6
  210. package/src/server/board-export.ts +35 -24
  211. package/src/server/canvas-geometry.ts +1 -0
  212. package/src/server/canvas-operations.ts +6 -2
  213. package/src/server/canvas-state.ts +5 -0
  214. package/src/server/demo-state.json +106 -100
  215. package/src/server/html-primitives.ts +3 -9
  216. package/src/server/html-surface.ts +8 -0
  217. package/src/server/index.ts +30 -12
  218. package/src/server/operations/http.ts +69 -21
  219. package/src/server/operations/index.ts +8 -1
  220. package/src/server/operations/invoker.ts +53 -5
  221. package/src/server/operations/mcp-contracts.ts +354 -0
  222. package/src/server/operations/mcp.ts +153 -18
  223. package/src/server/operations/ops/ax-await.ts +18 -10
  224. package/src/server/operations/ops/ax-read.ts +2 -0
  225. package/src/server/operations/ops/ax-timeline.ts +2 -2
  226. package/src/server/operations/ops/batch.ts +27 -8
  227. package/src/server/operations/ops/boards.ts +1 -3
  228. package/src/server/operations/ops/nodes.ts +18 -3
  229. package/src/server/operations/ops/viewport.ts +11 -9
  230. package/src/server/operations/registry.ts +24 -4
  231. package/src/server/operations/types.ts +16 -2
  232. package/src/server/server.ts +17 -59
  233. package/src/server/tour-control.ts +66 -6
  234. package/src/server/webpage-node.ts +9 -4
  235. package/src/shared/ax-surface-protocol.ts +0 -11
  236. package/src/shared/content-height-reporter.ts +4 -3
  237. package/src/shared/tour.ts +8 -0
@@ -4,6 +4,7 @@ import { canvasArea } from './canvas-area';
4
4
  import { cancelViewportAnimation, hasInitialServerLayout, viewport } from '../state/canvas-store';
5
5
  import {
6
6
  endPresentation,
7
+ presentationCursor,
7
8
  presentationError,
8
9
  presenting,
9
10
  recordingCamera,
@@ -11,6 +12,7 @@ import {
11
12
  stepTour,
12
13
  } from '../state/presentation';
13
14
  import { usePanZoom } from './use-pan-zoom';
15
+ import { BarHint } from './BarHint';
14
16
 
15
17
  declare global {
16
18
  interface Window {
@@ -25,6 +27,9 @@ declare global {
25
27
 
26
28
  export function Presentation() {
27
29
  const active = presenting.value;
30
+ const cursor = presentationCursor.value;
31
+ const canBack = cursor !== null && cursor.index > 0;
32
+ const canNext = cursor !== null && cursor.index < cursor.total - 1;
28
33
  const ready = hasInitialServerLayout.value;
29
34
  const moveCamera = (camera: Camera) => {
30
35
  cancelViewportAnimation();
@@ -89,9 +94,54 @@ export function Presentation() {
89
94
  }}
90
95
  onContextMenu={(event) => event.preventDefault()}
91
96
  >
92
- <button type="button" class="presentation-exit" onClick={endPresentation}>
93
- Exit presentation
94
- </button>
97
+ <div class="presentation-controls" role="group" aria-label="Presentation controls">
98
+ <BarHint
99
+ id="presentation-back-hint"
100
+ label="Previous stop"
101
+ shortcut="← / ↑"
102
+ fitWithin=".presentation-navigation"
103
+ >
104
+ <button
105
+ type="button"
106
+ class="presentation-step"
107
+ aria-describedby="presentation-back-hint"
108
+ aria-disabled={!canBack}
109
+ onClick={() => {
110
+ if (canBack) void stepTour({ step: 'previous' });
111
+ }}
112
+ >
113
+ Back
114
+ </button>
115
+ </BarHint>
116
+ <BarHint
117
+ id="presentation-next-hint"
118
+ label="Next stop"
119
+ shortcut="→ / ↓ / Space"
120
+ fitWithin=".presentation-navigation"
121
+ >
122
+ <button
123
+ type="button"
124
+ class="presentation-step"
125
+ aria-describedby="presentation-next-hint"
126
+ aria-disabled={!canNext}
127
+ onClick={() => {
128
+ if (canNext) void stepTour({ step: 'next' });
129
+ }}
130
+ >
131
+ Next
132
+ </button>
133
+ </BarHint>
134
+ <BarHint id="presentation-exit-hint" label="Exit presentation" shortcut="Esc" align="end">
135
+ <button
136
+ type="button"
137
+ class="presentation-exit"
138
+ aria-describedby="presentation-exit-hint"
139
+ onClick={endPresentation}
140
+ >
141
+ Exit presentation
142
+ </button>
143
+ </BarHint>
144
+ </div>
95
145
  </div>
96
146
  )}
97
147
  {presentationError.value && (
@@ -97,6 +97,8 @@ function RailButton({
97
97
  }) {
98
98
  const [hint, setHint] = useState<{ left: number; top: number } | null>(null);
99
99
  const show = (e: Event) => {
100
+ // Closing the menu restores focus before it unmounts; that must not reopen the hint.
101
+ if (menuOpen) return;
100
102
  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
101
103
  setHint({ left: rect.right + 10, top: rect.top + rect.height / 2 });
102
104
  };
@@ -39,6 +39,14 @@ import {
39
39
  import { formatCountdown, gateRemainingMs } from '../../shared/approval-gates.js';
40
40
  import { agentSessionsEnabled } from '../state/workspace-settings';
41
41
 
42
+ const PRESENT_DESCRIPTION = (
43
+ <span style={{ whiteSpace: 'pre-line' }}>
44
+ {
45
+ 'A presentation follows either a saved tour or the board’s groups.\n\nSaved tour: ask your agent to define the stops and their order. Each stop can focus on a card, a group, a rectangular area, or a specific camera position.\n\nNo saved tour: every group becomes a stop, ordered top-to-bottom, then left-to-right. It doesn’t automatically visit every card.'
46
+ }
47
+ </span>
48
+ );
49
+
42
50
  /**
43
51
  * The attached session's chip (rail-chrome-v2 phase 3): phase-colored, with a
44
52
  * dot that pulses while the agent thinks or runs a tool. Renders ONLY while a
@@ -481,12 +489,16 @@ export function TopBar() {
481
489
  )}
482
490
  {activeBoardId.value && (
483
491
  <BarHint
492
+ id="present-hint"
484
493
  label="Present board"
485
- body="Arrow keys or Space step through stops; Esc exits. Without a saved tour, groups are read top-to-bottom, left-to-right."
494
+ body={PRESENT_DESCRIPTION}
495
+ align="end"
496
+ fitWithin="body"
486
497
  >
487
498
  <button
488
499
  type="button"
489
500
  class="present-button"
501
+ aria-describedby="present-hint"
490
502
  onClick={() => {
491
503
  void startPresentation();
492
504
  }}
@@ -592,14 +604,17 @@ function TopBarMore({ boardOpen, onFit }: { boardOpen: boolean; onFit: () => voi
592
604
  </button>
593
605
  )}
594
606
  {boardOpen && (
595
- <button
596
- type="button"
597
- role="menuitem"
598
- class="toolbar-menu-item"
599
- onClick={run(() => void startPresentation())}
600
- >
601
- Present board
602
- </button>
607
+ <BarHint id="present-menu-hint" label="Present board" body={PRESENT_DESCRIPTION}>
608
+ <button
609
+ type="button"
610
+ role="menuitem"
611
+ class="toolbar-menu-item"
612
+ aria-describedby="present-menu-hint"
613
+ onClick={run(() => void startPresentation())}
614
+ >
615
+ Present board
616
+ </button>
617
+ </BarHint>
603
618
  )}
604
619
  <button type="button" role="menuitem" class="toolbar-menu-item" onClick={run(() => zoomByFactor(1.25))}>
605
620
  Zoom in
@@ -7,12 +7,6 @@ export const AUTO_FIT_MAX_HEIGHT = 600;
7
7
  // Iframe surfaces (charts/dashboards/rich html) can legitimately need more room
8
8
  // than a text node, so they grow to a higher ceiling before scrolling.
9
9
  export const AUTO_FIT_MAX_HEIGHT_IFRAME = 1400;
10
- // `.node-body` adds 12px padding top+bottom around an iframe surface (global.css).
11
- // The bridge reports the iframe's OWN document scrollHeight, so the grow target
12
- // must add titlebar + this body padding or the node settles ~24px short and the
13
- // surface shows a residual inner scrollbar. (The DOM auto-fit above doesn't need
14
- // this: body.scrollHeight already includes the body's padding.)
15
- export const AUTO_FIT_BODY_PADDING = 24;
16
10
 
17
11
  /** Node types the DOM auto-fit can't measure: iframe-backed surfaces (html/
18
12
  * json-render/graph/mcp-app), where the body's scrollHeight equals the iframe
@@ -77,13 +71,17 @@ export function shouldContentFitIframeNode(node: CanvasNodeState): boolean {
77
71
  /**
78
72
  * Grow-only target height from a surface-reported content height. Returns null
79
73
  * when the node is exempt, the report is non-positive, or the node already fits
80
- * (so it never shrinks — monotonic growth can't oscillate). Adds the titlebar +
81
- * node-body padding so the content fully clears (no residual inner scrollbar),
82
- * capped at the iframe ceiling.
74
+ * (so it never shrinks). Callers measure the actual chrome in world pixels,
75
+ * including zoom-compensated headers and any captions. A fixed padding allowance
76
+ * makes a full-height viewer grow again on every content-height report.
83
77
  */
84
- export function computeContentGrowHeight(node: CanvasNodeState, contentHeight: number): number | null {
78
+ export function computeContentGrowHeight(
79
+ node: CanvasNodeState,
80
+ contentHeight: number,
81
+ chromeHeight: number,
82
+ ): number | null {
85
83
  if (!shouldContentFitIframeNode(node) || !Number.isFinite(contentHeight) || contentHeight <= 0) return null;
86
- const want = Math.min(contentHeight + AUTO_FIT_TITLEBAR_HEIGHT + AUTO_FIT_BODY_PADDING, AUTO_FIT_MAX_HEIGHT_IFRAME);
84
+ const want = Math.min(Math.ceil(contentHeight + chromeHeight), AUTO_FIT_MAX_HEIGHT_IFRAME);
87
85
  return want > node.size.height + 8 ? want : null;
88
86
  }
89
87
 
@@ -55,7 +55,7 @@ export interface CanvasFitInsets {
55
55
  * both (0.5.0 Amp report, finding A; architecture rule 9's trap).
56
56
  */
57
57
  export function canvasFitInsets(): CanvasFitInsets {
58
- let top = 16;
58
+ let top = 44;
59
59
  let bottom = 16;
60
60
  try {
61
61
  const area = canvasArea();
@@ -71,7 +71,12 @@ export function useFocusTrap(
71
71
  // from (focus mode unmounts the node while expanded) names the element
72
72
  // to return to, which may only exist after the next frame.
73
73
  if (restoreTo) {
74
- requestAnimationFrame(() => restoreTo()?.focus());
74
+ requestAnimationFrame(() => {
75
+ // A newly opened menu owns focus before this deferred return runs.
76
+ if (document.activeElement === document.body || root.contains(document.activeElement)) {
77
+ restoreTo()?.focus();
78
+ }
79
+ });
75
80
  } else if (previous && document.contains(previous) && previous !== document.body) {
76
81
  previous.focus();
77
82
  }
@@ -5,16 +5,10 @@
5
5
  * HTML-escaped diagram source in `<pre class="mermaid-source">`; this script
6
6
  * reads it back via textContent, renders it to SVG, and re-renders when the
7
7
  * parent canvas live-switches the theme (the theme bridge toggles the document's
8
- * `data-theme` attribute). Keeping mermaid out of the main SPA bundle keeps the
9
- * canvas boot lean — only mermaid-node surfaces pay for the library.
8
+ * `data-theme` attribute). Standalone views and exports share the canvas renderer.
10
9
  */
11
10
 
12
- import mermaid from 'mermaid';
13
- import { canvasThemeScheme } from '../shared/themes.js';
14
-
15
- function documentScheme(): 'dark' | 'light' {
16
- return canvasThemeScheme(document.documentElement.getAttribute('data-theme'));
17
- }
11
+ import { renderMermaid } from './mermaid-render.js';
18
12
 
19
13
  function showError(container: HTMLElement, message: string): void {
20
14
  container.innerHTML = '';
@@ -65,18 +59,14 @@ function fitDiagram(container: HTMLElement): void {
65
59
  }
66
60
 
67
61
  async function renderDiagram(source: string, container: HTMLElement): Promise<void> {
68
- const scheme = documentScheme();
69
- mermaid.initialize({
70
- startOnLoad: false,
71
- theme: scheme === 'dark' ? 'dark' : 'default',
72
- securityLevel: 'strict',
73
- });
74
- renderSeq += 1;
62
+ const sequence = ++renderSeq;
75
63
  try {
76
- const { svg } = await mermaid.render(`pmx-mermaid-${renderSeq}`, source);
64
+ const svg = await renderMermaid(source);
65
+ if (sequence !== renderSeq) return;
77
66
  container.innerHTML = svg;
78
67
  fitDiagram(container);
79
68
  } catch (error) {
69
+ if (sequence !== renderSeq) return;
80
70
  // Never leave a blank/black frame: show the parse/render error instead.
81
71
  showError(container, error instanceof Error ? error.message : String(error));
82
72
  }
@@ -98,11 +88,7 @@ function boot(): void {
98
88
  void renderDiagram(source, container);
99
89
  // Live theme switching: the parent's theme bridge toggles data-theme on the
100
90
  // root element; re-render so the diagram palette follows the canvas theme.
101
- let lastScheme = documentScheme();
102
91
  const observer = new MutationObserver(() => {
103
- const scheme = documentScheme();
104
- if (scheme === lastScheme) return;
105
- lastScheme = scheme;
106
92
  void renderDiagram(source, container);
107
93
  });
108
94
  observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
@@ -0,0 +1,39 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ // Mermaid configuration is global: serialize configuration with each render.
4
+ let rendering: Promise<unknown> = Promise.resolve();
5
+
6
+ export function renderMermaid(source: string): Promise<string> {
7
+ const result = rendering.then(async () => {
8
+ // Diagram-only dependency: the SPA splits this module; self-contained MCP
9
+ // documents defer its initialization without requiring external assets.
10
+ const { default: mermaid } = await import('mermaid');
11
+ const style = getComputedStyle(document.documentElement);
12
+ const token = (name: string) => style.getPropertyValue(name).trim();
13
+ mermaid.initialize({
14
+ startOnLoad: false,
15
+ securityLevel: 'strict',
16
+ theme: 'base',
17
+ fontFamily: token('--font-ui'),
18
+ themeVariables: {
19
+ primaryColor: token('--c-panel-soft'),
20
+ primaryTextColor: token('--c-text'),
21
+ primaryBorderColor: token('--c-line'),
22
+ lineColor: token('--c-muted'),
23
+ secondaryColor: token('--c-panel'),
24
+ tertiaryColor: token('--c-panel'),
25
+ edgeLabelBackground: token('--c-panel'),
26
+ fontSize: '12px',
27
+ },
28
+ });
29
+ const { svg } = await mermaid.render(`mermaid-${crypto.randomUUID()}`, source);
30
+ return DOMPurify.sanitize(svg, {
31
+ ADD_TAGS: ['foreignObject'],
32
+ ADD_ATTR: ['dominant-baseline'],
33
+ HTML_INTEGRATION_POINTS: { foreignobject: true },
34
+ });
35
+ });
36
+ // A malformed diagram must not prevent the next diagram from rendering.
37
+ rendering = result.catch(() => {});
38
+ return result;
39
+ }
@@ -18,37 +18,10 @@ import { axNodeActionButtonStyle, runNodeAxInteraction } from './ax-node-actions
18
18
  */
19
19
 
20
20
  /**
21
- * The rows sit in a dock stuck to the bottom of the scrollable node body: a step
22
- * node is short (360x200 as materialized), so a control row that simply followed
23
- * the content scrolled out of sight. Sticky keeps it reachable at any node height
24
- * while still being INSIDE the body, which is what auto-fit measures.
21
+ * The rows sit in `.ax-step-dock`, a footer that stays reachable at the bottom of
22
+ * the node body (global.css). On a card it shares one footer row with the card's
23
+ * own actions; in the expanded overlay it is a sticky bar under the document.
25
24
  */
26
- /**
27
- * The dock reads as a real footer toolbar rather than stray text at the bottom
28
- * edge. It bleeds to the container's edges via negative margins (`.node-body`
29
- * has 12px padding; the expanded overlay's content area has 16px, and the dock
30
- * is a sibling of that area, so it starts flush) and then re-applies its own
31
- * padding — otherwise the first label sits hard against the left edge and looks
32
- * clipped. `--c-panel-overlay` separates it from the content above it.
33
- */
34
- const dockStyle = {
35
- position: 'sticky',
36
- // Bleed OUT of the container's padding so the bar spans edge to edge, then
37
- // re-apply padding inside it. These are two different numbers: in a node body
38
- // the dock sits INSIDE 12px of padding and must pull back out of it, while in
39
- // the expanded overlay it is a sibling of the padded content area and is
40
- // already flush — bleeding there would push it off the panel.
41
- bottom: 'calc(-1 * var(--ax-dock-bleed, 12px))',
42
- marginTop: '12px',
43
- marginLeft: 'calc(-1 * var(--ax-dock-bleed, 12px))',
44
- marginRight: 'calc(-1 * var(--ax-dock-bleed, 12px))',
45
- marginBottom: 'calc(-1 * var(--ax-dock-bleed, 12px))',
46
- padding: '9px var(--ax-dock-pad, 12px) 10px',
47
- background: 'var(--c-panel-overlay)',
48
- borderTop: '1px solid var(--c-line)',
49
- backdropFilter: 'blur(6px)',
50
- } as const;
51
-
52
25
  const rowStyle = {
53
26
  display: 'flex',
54
27
  alignItems: 'center',
@@ -57,7 +30,7 @@ const rowStyle = {
57
30
  } as const;
58
31
 
59
32
  const labelStyle = {
60
- fontSize: '10px',
33
+ fontSize: 'var(--fs-meta)',
61
34
  color: 'var(--c-muted)',
62
35
  textTransform: 'uppercase',
63
36
  letterSpacing: '0.04em',
@@ -160,47 +133,50 @@ function FlowLoopControls({ node, flow }: { node: CanvasNodeState; flow: AxFlowS
160
133
  </button>
161
134
  </BarHint>
162
135
  )}
163
- <input
164
- class="ax-flow-steer-input"
165
- value={message}
166
- placeholder="Steer the agent…"
167
- aria-label="Steer the agent"
168
- onInput={(e) => setMessage((e.target as HTMLInputElement).value)}
169
- onClick={(e) => e.stopPropagation()}
170
- onKeyDown={(e) => {
171
- if (e.key !== 'Enter') return;
172
- e.stopPropagation();
173
- void handleSteer();
174
- }}
175
- style={{
176
- flex: '1 1 120px',
177
- minWidth: '90px',
178
- fontSize: '10px',
179
- padding: '3px 6px',
180
- borderRadius: '4px',
181
- border: '1px solid var(--c-line)',
182
- background: 'var(--c-panel-overlay)',
183
- color: 'var(--c-text)',
184
- }}
185
- />
186
- <BarHint
187
- label="Send a steering message to the agent working this flow"
188
- side="up"
189
- align="end"
190
- fitWithin=".node-content, .expanded-overlay-panel"
191
- >
192
- <button
193
- type="button"
194
- class="ax-node-action"
195
- style={axNodeActionButtonStyle}
196
- onClick={(e) => {
136
+ {/* The draft and its send button wrap as one unit, never a lone Steer. */}
137
+ <span style={{ display: 'flex', gap: '6px', flex: '1 1 140px', minWidth: 0 }}>
138
+ <input
139
+ class="ax-flow-steer-input"
140
+ value={message}
141
+ placeholder="Steer the agent…"
142
+ aria-label="Steer the agent"
143
+ onInput={(e) => setMessage((e.target as HTMLInputElement).value)}
144
+ onClick={(e) => e.stopPropagation()}
145
+ onKeyDown={(e) => {
146
+ if (e.key !== 'Enter') return;
197
147
  e.stopPropagation();
198
148
  void handleSteer();
199
149
  }}
150
+ style={{
151
+ flex: '1 1 0',
152
+ minWidth: 0,
153
+ fontSize: 'var(--fs-meta)',
154
+ padding: '3px 6px',
155
+ borderRadius: '4px',
156
+ border: '1px solid var(--c-line)',
157
+ background: 'var(--c-panel-overlay)',
158
+ color: 'var(--c-text)',
159
+ }}
160
+ />
161
+ <BarHint
162
+ label="Send a steering message to the agent working this flow"
163
+ side="up"
164
+ align="end"
165
+ fitWithin=".node-content, .expanded-overlay-panel"
200
166
  >
201
- Steer
202
- </button>
203
- </BarHint>
167
+ <button
168
+ type="button"
169
+ class="ax-node-action"
170
+ style={axNodeActionButtonStyle}
171
+ onClick={(e) => {
172
+ e.stopPropagation();
173
+ void handleSteer();
174
+ }}
175
+ >
176
+ Steer
177
+ </button>
178
+ </BarHint>
179
+ </span>
204
180
  </div>
205
181
  );
206
182
  }
@@ -212,7 +188,7 @@ export function AxStepControls({ node }: { node: CanvasNodeState }) {
212
188
  const status = mirroredStatus(node.data);
213
189
 
214
190
  return (
215
- <div class="ax-step-dock" style={dockStyle}>
191
+ <div class="ax-step-dock">
216
192
  <div class="ax-step-controls" style={rowStyle}>
217
193
  <span class="ax-step-position" style={labelStyle}>
218
194
  Step {step.index}/{step.total}
@@ -1,20 +1,43 @@
1
1
  import type { CanvasNodeState } from '../types';
2
- import { boardList, openBoard } from '../state/boards-store';
2
+ import { useState } from 'preact/hooks';
3
+ import { activeBoardId, boardList, openBoard } from '../state/boards-store';
4
+ import { updateNodeFromClient } from '../state/intent-bridge';
3
5
  import { IconExternalLink } from '../icons';
4
6
 
5
- /** Native text-only cross-board link. Target identity is the durable board id. */
7
+ /** Target identity is the durable board id; a missing target can be relinked. */
6
8
  export function BoardNode({ node }: { node: CanvasNodeState }) {
7
9
  const boardId = typeof node.data.boardId === 'string' ? node.data.boardId : '';
8
10
  const target = boardList.value.find((board) => board.id === boardId);
11
+ const [relinking, setRelinking] = useState(false);
12
+ const [error, setError] = useState('');
9
13
  return (
10
14
  <div class="board-node">
15
+ {target ? (
16
+ <div class="home-thumb" aria-hidden="true">
17
+ <div class="home-thumb-inner">
18
+ {(target.preview ?? []).map((rect, index) => (
19
+ <span
20
+ key={index}
21
+ class={`home-thumb-card${rect.pinned ? ' is-pinned' : ''}`}
22
+ style={{
23
+ left: `${rect.x * 100}%`,
24
+ top: `${rect.y * 100}%`,
25
+ width: `${rect.w * 100}%`,
26
+ height: `${rect.h * 100}%`,
27
+ }}
28
+ />
29
+ ))}
30
+ </div>
31
+ </div>
32
+ ) : (
33
+ <div class="board-node-missing">Board not found</div>
34
+ )}
11
35
  <div class="board-node-content">
12
- <div class="board-node-label">Linked board</div>
13
- <strong class="board-node-title">{target?.name ?? 'Missing board'}</strong>
36
+ <strong class="board-node-title">{target?.name ?? String(node.data.title || 'Linked board')}</strong>
14
37
  <div class="board-node-meta">
15
38
  {target
16
- ? `${target.nodeCount} nodes${target.pin ? ' · pinned, so it travels with you' : ''}`
17
- : boardId || 'No target id'}
39
+ ? `Board · ${target.nodeCount} nodes${target.pin ? ' · pinned, so it travels with you' : ''}`
40
+ : 'The linked board is no longer available.'}
18
41
  </div>
19
42
  {target?.summary && <p class="board-node-summary">{target.summary}</p>}
20
43
  {!!target?.pinnedTitles?.length && (
@@ -25,14 +48,47 @@ export function BoardNode({ node }: { node: CanvasNodeState }) {
25
48
  Linked from: {[...new Set(target.backlinks.map((link) => link.title))].join(', ')}
26
49
  </p>
27
50
  )}
51
+ {relinking && (
52
+ <select
53
+ aria-label="Link to board"
54
+ class="board-node-relink"
55
+ value=""
56
+ onChange={async (event) => {
57
+ const id = event.currentTarget.value;
58
+ if (!id) return;
59
+ setError('');
60
+ const result = await updateNodeFromClient(node.id, {
61
+ data: { boardId: id },
62
+ expectedBoardId: activeBoardId.value,
63
+ });
64
+ if (result.ok) setRelinking(false);
65
+ else setError('Could not relink this card. Try again.');
66
+ }}
67
+ >
68
+ <option value="" disabled>
69
+ Choose board…
70
+ </option>
71
+ {boardList.value
72
+ .filter((board) => board.id !== activeBoardId.value)
73
+ .map((board) => (
74
+ <option key={board.id} value={board.id}>
75
+ {board.name}
76
+ </option>
77
+ ))}
78
+ </select>
79
+ )}
80
+ {error && (
81
+ <div role="status" class="board-node-meta">
82
+ {error}
83
+ </div>
84
+ )}
28
85
  </div>
29
86
  <button
30
87
  class="board-node-open"
31
88
  type="button"
32
- disabled={!target}
33
- onClick={() => target && void openBoard(target.id)}
89
+ onClick={() => (target ? void openBoard(target.id) : setRelinking(!relinking))}
34
90
  >
35
- {target ? 'Open board' : 'Target unavailable'}
91
+ {target ? 'Open board' : 'Link to board…'}
36
92
  {target && <IconExternalLink size={14} />}
37
93
  </button>
38
94
  </div>