pmx-canvas 0.10.0 → 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 (201) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +246 -108
  3. package/CHANGELOG.md +32 -0
  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/server/canvas-state.d.ts +2 -0
  114. package/dist/types/server/index.d.ts +10 -5
  115. package/dist/types/server/operations/ops/boards.d.ts +0 -1
  116. package/dist/types/server/tour-control.d.ts +13 -3
  117. package/dist/types/shared/ax-surface-protocol.d.ts +0 -9
  118. package/dist/types/shared/tour.d.ts +8 -0
  119. package/docs/design/Chrome.dc.html +32 -6
  120. package/docs/design/Types1.dc.html +3 -2
  121. package/docs/design/Types2.dc.html +1 -1
  122. package/docs/design/Types3.dc.html +1 -1
  123. package/docs/design/Zoom.dc.html +37 -60
  124. package/docs/design/canvas.json +1 -1
  125. package/docs/design.md +169 -4
  126. package/docs/http-api.md +51 -1
  127. package/docs/mcp.md +8 -0
  128. package/docs/node-types.md +38 -3
  129. package/docs/plans/plan-009-tech-debt-backlog.md +2 -2
  130. package/docs/plans/plan-019-post-0.9-roadmap.md +283 -11
  131. package/docs/product-vision-2026-09.md +9 -0
  132. package/package.json +2 -2
  133. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  134. package/plugins/pmx-canvas/plugin.json +1 -1
  135. package/plugins/pmx-canvas/runtime.json +1 -1
  136. package/plugins/pmx-canvas/tools.json +246 -108
  137. package/skills/pmx-canvas/SKILL.md +11 -18
  138. package/skills/pmx-canvas/references/full-reference.md +9 -4
  139. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  140. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +3 -5
  141. package/src/client/canvas/BarHint.tsx +6 -3
  142. package/src/client/canvas/CanvasNode.tsx +34 -47
  143. package/src/client/canvas/CanvasViewport.tsx +6 -7
  144. package/src/client/canvas/ContextMenu.tsx +63 -3
  145. package/src/client/canvas/EdgeLayer.tsx +365 -169
  146. package/src/client/canvas/ExpandedNodeOverlay.tsx +1 -2
  147. package/src/client/canvas/Presentation.tsx +53 -3
  148. package/src/client/canvas/TopBar.tsx +24 -9
  149. package/src/client/canvas/auto-fit.ts +9 -11
  150. package/src/client/canvas/canvas-area.ts +1 -1
  151. package/src/client/canvas/use-focus-trap.ts +6 -1
  152. package/src/client/mermaid-entry.ts +6 -20
  153. package/src/client/mermaid-render.ts +39 -0
  154. package/src/client/nodes/AxStepControls.tsx +45 -69
  155. package/src/client/nodes/BoardNode.tsx +65 -9
  156. package/src/client/nodes/ContextNode.tsx +88 -396
  157. package/src/client/nodes/ExtAppFrame.tsx +283 -1462
  158. package/src/client/nodes/FileNode.tsx +92 -81
  159. package/src/client/nodes/HtmlNode.tsx +8 -36
  160. package/src/client/nodes/LedgerNode.tsx +31 -17
  161. package/src/client/nodes/MarkdownNode.tsx +8 -17
  162. package/src/client/nodes/McpAppNode.tsx +48 -154
  163. package/src/client/nodes/MermaidNode.tsx +80 -69
  164. package/src/client/nodes/PromptNode.tsx +12 -14
  165. package/src/client/nodes/ResponseNode.tsx +8 -6
  166. package/src/client/nodes/StatusNode.tsx +22 -7
  167. package/src/client/nodes/StatusSummary.tsx +5 -3
  168. package/src/client/nodes/TraceNode.tsx +8 -8
  169. package/src/client/nodes/ViewerFrame.tsx +262 -0
  170. package/src/client/nodes/ViewerHost.tsx +41 -0
  171. package/src/client/nodes/WebpageNode.tsx +22 -24
  172. package/src/client/nodes/iframe-document-url.ts +20 -27
  173. package/src/client/nodes/primitive-copy.ts +97 -0
  174. package/src/client/nodes/use-iframe-content-height.ts +34 -10
  175. package/src/client/nodes/use-surface-frame.ts +22 -11
  176. package/src/client/nodes/viewer-runtime-state.ts +41 -35
  177. package/src/client/state/canvas-store.ts +18 -12
  178. package/src/client/state/intent-bridge.ts +1 -0
  179. package/src/client/state/presentation.ts +135 -15
  180. package/src/client/state/sse-bridge.ts +18 -19
  181. package/src/client/theme/global.css +389 -229
  182. package/src/client/theme/surface-theme.css +6 -6
  183. package/src/json-render/charts/tufte-components.tsx +44 -2
  184. package/src/json-render/renderer/index.css +62 -2
  185. package/src/json-render/renderer/index.tsx +31 -7
  186. package/src/json-render/server.ts +3 -2
  187. package/src/server/board-export.ts +35 -24
  188. package/src/server/canvas-geometry.ts +1 -0
  189. package/src/server/canvas-state.ts +5 -0
  190. package/src/server/demo-state.json +106 -100
  191. package/src/server/html-primitives.ts +3 -9
  192. package/src/server/html-surface.ts +8 -0
  193. package/src/server/index.ts +19 -9
  194. package/src/server/operations/mcp-contracts.ts +17 -2
  195. package/src/server/operations/ops/boards.ts +1 -3
  196. package/src/server/operations/ops/viewport.ts +11 -9
  197. package/src/server/server.ts +7 -59
  198. package/src/server/tour-control.ts +66 -6
  199. package/src/shared/ax-surface-protocol.ts +0 -11
  200. package/src/shared/content-height-reporter.ts +4 -3
  201. package/src/shared/tour.ts +8 -0
@@ -21,12 +21,12 @@
21
21
  /* ── Core palette (dark · ink + cyan) ────────────────────── */
22
22
  --c-bg: #081524;
23
23
  --c-panel: #0f1d31;
24
- --c-panel-soft: #0a1729;
24
+ --c-panel-soft: #13243b;
25
25
  --c-line: #1b2c44;
26
26
  --c-text: #e6eef7;
27
- --c-text-soft: #c7d3ea;
27
+ --c-text-soft: #b9c6d6;
28
28
  --c-muted: #8ea3bd;
29
- --c-dim: #5c6b80;
29
+ --c-dim: #6b7d94;
30
30
  --c-accent: #4BBCFF;
31
31
  --c-ok: #2fd07f;
32
32
  --c-warn: #f4c542;
@@ -46,13 +46,13 @@
46
46
  /* Common aliases authored HTML might use. */
47
47
  --color-bg: #081524;
48
48
  --color-panel: #0f1d31;
49
- --color-surface: #0a1729;
49
+ --color-surface: #13243b;
50
50
  --color-border: #1b2c44;
51
51
  --color-text: #e6eef7;
52
52
  --color-text-primary: #e6eef7;
53
- --color-text-secondary: #c7d3ea;
53
+ --color-text-secondary: #b9c6d6;
54
54
  --color-text-muted: #8ea3bd;
55
- --color-text-dim: #5c6b80;
55
+ --color-text-dim: #6b7d94;
56
56
  --color-accent: #4BBCFF;
57
57
  --color-success: #2fd07f;
58
58
  --color-warning: #f4c542;
@@ -336,6 +336,18 @@ interface SlopegraphProps {
336
336
  height?: number | null;
337
337
  }
338
338
 
339
+ /** Separate close labels without moving the data points they describe. */
340
+ function slopeLabelPositions(values: number[], top: number, bottom: number): number[] {
341
+ const order = values.map((y, index) => ({ y, index })).sort((a, b) => a.y - b.y);
342
+ const gap = Math.min(14, (bottom - top) / Math.max(1, order.length - 1));
343
+ for (let i = 1; i < order.length; i++) order[i].y = Math.max(order[i].y, order[i - 1].y + gap);
344
+ if (order.length) order[order.length - 1].y = Math.min(order[order.length - 1].y, bottom);
345
+ for (let i = order.length - 2; i >= 0; i--) order[i].y = Math.min(order[i].y, order[i + 1].y - gap);
346
+ const result = [...values];
347
+ for (const { y, index } of order) result[index] = y;
348
+ return result;
349
+ }
350
+
339
351
  function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
340
352
  const stroke = props.color ?? ACCENT;
341
353
  const rows = (props.data ?? []).map((row) => ({
@@ -359,6 +371,16 @@ function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
359
371
  // fills without pushing a scrollbar onto the iframe document.
360
372
  const plotH = chartPlotHeight(height, Boolean(props.title));
361
373
  const scaleY = (v: number) => topPad + (1 - (v - min) / (max - min)) * (plotH - topPad - botPad);
374
+ const beforeLabels = slopeLabelPositions(
375
+ rows.map((row) => scaleY(row.before)),
376
+ topPad,
377
+ plotH - botPad,
378
+ );
379
+ const afterLabels = slopeLabelPositions(
380
+ rows.map((row) => scaleY(row.after)),
381
+ topPad,
382
+ plotH - botPad,
383
+ );
362
384
 
363
385
  return (
364
386
  <div ref={frameRef} className="pmx-chart pmx-chart--slopegraph" style={{ height }}>
@@ -390,10 +412,30 @@ function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
390
412
  <line x1={leftX} y1={y1} x2={rightX} y2={y2} stroke={lineColor} strokeWidth={1.5} />
391
413
  <circle cx={leftX} cy={y1} r={2.5} fill={lineColor} />
392
414
  <circle cx={rightX} cy={y2} r={2.5} fill={lineColor} />
393
- <text x={leftX - 8} y={y1} textAnchor="end" dominantBaseline="central" fontSize={11} fill={INK}>
415
+ {beforeLabels[i] !== y1 && (
416
+ <line x1={leftX} y1={y1} x2={leftX - 5} y2={beforeLabels[i]} stroke={lineColor} />
417
+ )}
418
+ {afterLabels[i] !== y2 && (
419
+ <line x1={rightX} y1={y2} x2={rightX + 5} y2={afterLabels[i]} stroke={lineColor} />
420
+ )}
421
+ <text
422
+ x={leftX - 8}
423
+ y={beforeLabels[i]}
424
+ textAnchor="end"
425
+ dominantBaseline="central"
426
+ fontSize={11}
427
+ fill={INK}
428
+ >
394
429
  {`${row.label} ${row.before}`}
395
430
  </text>
396
- <text x={rightX + 8} y={y2} textAnchor="start" dominantBaseline="central" fontSize={11} fill={INK}>
431
+ <text
432
+ x={rightX + 8}
433
+ y={afterLabels[i]}
434
+ textAnchor="start"
435
+ dominantBaseline="central"
436
+ fontSize={11}
437
+ fill={INK}
438
+ >
397
439
  {`${row.after} ${row.label}`}
398
440
  </text>
399
441
  </g>
@@ -3,7 +3,7 @@
3
3
 
4
4
  @source "../../../../node_modules/@json-render/shadcn/dist/index.js";
5
5
 
6
- @custom-variant dark (&:is([data-theme="dark"] *));
6
+ @custom-variant dark (&:is(.dark *));
7
7
 
8
8
  @theme {
9
9
  --font-sans: "IBM Plex Sans", system-ui, sans-serif;
@@ -99,6 +99,66 @@
99
99
  --chart-6: #ffffff;
100
100
  }
101
101
 
102
+ /* Named themes share the canvas surface, including nested cards and charts.
103
+ Explicit dark/light remain available for authored design experiments. */
104
+ :root[data-canvas-theme] {
105
+ --radius: 6px;
106
+ --text-xs: 11px;
107
+ --text-sm: 12px;
108
+ --text-base: 13px;
109
+ --text-lg: 15px;
110
+ --text-xl: 20px;
111
+ --background: var(--c-panel);
112
+ --foreground: var(--c-text);
113
+ --card: var(--c-panel-soft);
114
+ --card-foreground: var(--c-text);
115
+ --popover: var(--c-panel);
116
+ --popover-foreground: var(--c-text);
117
+ --primary: var(--c-accent);
118
+ --primary-foreground: var(--c-on-accent, var(--c-on-pin));
119
+ --secondary: var(--c-panel-soft);
120
+ --secondary-foreground: var(--c-text);
121
+ --muted: var(--c-panel-soft);
122
+ --muted-foreground: var(--c-muted);
123
+ --accent: var(--c-panel-soft);
124
+ --accent-foreground: var(--c-text);
125
+ --destructive: var(--c-danger);
126
+ --border: var(--c-line);
127
+ --input: var(--c-line);
128
+ --ring: var(--c-accent);
129
+ --chart-1: var(--c-accent);
130
+ --chart-2: var(--c-rel-supports);
131
+ --chart-3: var(--c-rel-contradicts);
132
+ --chart-4: var(--c-rel-cites);
133
+ --chart-5: var(--c-rel-informs);
134
+ --chart-6: var(--c-muted);
135
+ font-size: 16px;
136
+ }
137
+
138
+ /* surface-theme's unlayered body rule otherwise wins over Tailwind's base layer. */
139
+ body {
140
+ background: var(--background);
141
+ color: var(--foreground);
142
+ padding: 0;
143
+ font-family: var(--font-sans);
144
+ font-size: 13px;
145
+ }
146
+
147
+ .pmx-viewer-root { width: 100%; min-width: 0; }
148
+ [data-embedded] .pmx-viewer-root:not(:has(> [data-slot="card"])) { padding: 12px 14px; }
149
+ [data-embedded] .pmx-viewer-root > [data-slot="card"] {
150
+ width: 100%;
151
+ max-width: none;
152
+ border: 0;
153
+ border-radius: 0;
154
+ box-shadow: none;
155
+ background: transparent;
156
+ padding: 12px 14px;
157
+ }
158
+ [data-embedded][data-duplicate-title] .pmx-viewer-root > [data-slot="card"] > [data-slot="card-header"] > [data-slot="card-title"] { display: none; }
159
+ [data-embedded][data-duplicate-title] .pmx-viewer-root > [data-slot="card"] > [data-slot="card-header"]:not(:has([data-slot="card-description"])) { display: none; }
160
+ [data-embedded] .pmx-viewer-root > [data-slot="card"] > [data-slot="card-content"] { padding: 0; }
161
+
102
162
  @theme inline {
103
163
  --radius-sm: calc(var(--radius) - 4px);
104
164
  --radius-md: calc(var(--radius) - 2px);
@@ -159,7 +219,7 @@ button {
159
219
  border: 1px solid transparent;
160
220
  border-radius: 9999px;
161
221
  padding: 0.125rem 0.5rem;
162
- font-size: 0.75rem;
222
+ font-size: max(11px, 0.75rem);
163
223
  font-weight: 500;
164
224
  line-height: 1.25rem;
165
225
  }
@@ -23,6 +23,7 @@ import { extraChartComponents } from '../charts/extra-components';
23
23
  import { tufteChartComponents } from '../charts/tufte-components';
24
24
  import { pmxCanvasDirectives } from '../directives';
25
25
  import { JsonRenderDevtools } from '@json-render/devtools-react';
26
+ import { canvasThemeScheme, isCanvasTheme } from '../../shared/themes.js';
26
27
 
27
28
  type BadgeVariant =
28
29
  | 'default'
@@ -167,10 +168,12 @@ function syncPreferredTheme(): void {
167
168
  }
168
169
 
169
170
  function applyTheme(theme: unknown): void {
170
- if (theme !== 'dark' && theme !== 'light' && theme !== 'high-contrast') return;
171
+ if (theme !== 'dark' && theme !== 'light' && !isCanvasTheme(theme)) return;
171
172
  document.documentElement.setAttribute('data-theme', theme);
172
- document.documentElement.classList.toggle('dark', theme === 'dark' || theme === 'high-contrast');
173
- document.documentElement.style.colorScheme = theme === 'light' ? 'light' : 'dark';
173
+ document.documentElement.toggleAttribute('data-canvas-theme', isCanvasTheme(theme));
174
+ const scheme = theme === 'light' ? 'light' : canvasThemeScheme(theme);
175
+ document.documentElement.classList.toggle('dark', scheme === 'dark');
176
+ document.documentElement.style.colorScheme = scheme;
174
177
  }
175
178
 
176
179
  function App({ store }: { store: StateStore }) {
@@ -235,11 +238,15 @@ function App({ store }: { store: StateStore }) {
235
238
  padding: 0,
236
239
  boxSizing: 'border-box' as const,
237
240
  }
238
- : { minHeight: '100vh', padding: 16, boxSizing: 'border-box' as const };
241
+ : { minHeight: '100vh', padding: 0, boxSizing: 'border-box' as const };
239
242
  return (
240
- <div style={containerStyle}>
243
+ <div
244
+ style={containerStyle}
245
+ data-embedded={!isSite ? '' : undefined}
246
+ data-duplicate-title={spec.elements[spec.root]?.props?.title === document.title ? '' : undefined}
247
+ >
241
248
  <JSONUIProvider registry={registry} store={store} directives={pmxCanvasDirectives} handlers={buildAxHandlers()}>
242
- <div style={isSite ? { flex: 1, minHeight: 0 } : undefined}>
249
+ <div className="pmx-viewer-root" style={isSite ? { flex: 1, minHeight: 0 } : undefined}>
243
250
  <Renderer spec={spec} registry={registry} loading={false} />
244
251
  </div>
245
252
  {window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ ? <JsonRenderDevtools position="right" /> : null}
@@ -282,8 +289,25 @@ if (root) {
282
289
  // Standalone viewers have no state token and render immediately.
283
290
  window.addEventListener('message', (event: MessageEvent) => {
284
291
  if (event.source !== window.parent) return;
285
- const m = event.data as { source?: string; type?: string; token?: string; state?: unknown } | null;
292
+ const m = event.data as {
293
+ source?: string;
294
+ type?: string;
295
+ token?: string;
296
+ state?: unknown;
297
+ theme?: unknown;
298
+ title?: unknown;
299
+ } | null;
286
300
  if (!m) return;
301
+ if (m.source === HTML_SURFACE_PUSH_SOURCE && m.type === 'theme-update' && stateToken && m.token === stateToken) {
302
+ applyTheme(m.theme);
303
+ if (typeof m.title === 'string') {
304
+ document.title = m.title;
305
+ const spec = window.__PMX_CANVAS_JSON_RENDER_SPEC__;
306
+ document
307
+ .querySelector('[data-embedded]')
308
+ ?.toggleAttribute('data-duplicate-title', spec?.elements[spec.root]?.props?.title === m.title);
309
+ }
310
+ }
287
311
  if (m.source === JSON_VIEWER_STATE_SOURCE && m.type === 'restore' && stateToken && m.token === stateToken) {
288
312
  if (m.state === null || (typeof m.state === 'object' && !Array.isArray(m.state)))
289
313
  mount(m.state as Record<string, unknown> | null);
@@ -6,6 +6,7 @@ import { applySpecPatch, parseSpecStreamLine, type Spec, type SpecStreamLine } f
6
6
  import { allComponentDefinitions, catalog, validateShadcnElementProps, type JsonRenderIssue } from './catalog.js';
7
7
  import { findUnknownDirectiveKey, isDynamicPropValue } from './directives.js';
8
8
  import { contentHeightReporterTag } from '../shared/content-height-reporter.js';
9
+ import type { CanvasThemeName } from '../shared/themes.js';
9
10
 
10
11
  export interface JsonRenderSpec {
11
12
  root: string;
@@ -1030,7 +1031,7 @@ export async function readJsonRenderBundle(): Promise<{ js: string; css: string
1030
1031
  export async function buildJsonRenderViewerHtml(options: {
1031
1032
  title: string;
1032
1033
  spec: JsonRenderSpec;
1033
- theme?: 'dark' | 'light' | 'high-contrast';
1034
+ theme?: CanvasThemeName | 'dark' | 'light';
1034
1035
  display?: 'expanded' | 'site';
1035
1036
  devtools?: boolean;
1036
1037
  nodeId?: string;
@@ -1076,7 +1077,7 @@ export async function buildJsonRenderViewerHtml(options: {
1076
1077
  const heightReporter = options.frameToken ? contentHeightReporterTag(options.frameToken) : '';
1077
1078
  return buildAppHtml({
1078
1079
  title: options.title,
1079
- css: cssBundle,
1080
+ css: `${readFileSync(join(import.meta.dir, '../client/theme/surface-theme.css'), 'utf-8')}\n${cssBundle}`,
1080
1081
  js: escapeInlineScriptSource(boot),
1081
1082
  head: `<meta name="color-scheme" content="light dark" />${heightReporter}`,
1082
1083
  });
@@ -12,7 +12,7 @@
12
12
  import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
13
13
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
14
14
  import { Marked, Renderer, type RendererThis, type Tokens } from 'marked';
15
- import { canvasThemeScheme } from '../shared/themes.js';
15
+ import { canvasThemeScheme, normalizeCanvasThemeName, type CanvasThemeName } from '../shared/themes.js';
16
16
  import { delimiterForPath, parseDelimitedText } from '../shared/delimited-text.js';
17
17
  import { buildJsonRenderViewerHtml, escapeInlineScriptSource, readJsonRenderBundle } from '../json-render/server.js';
18
18
  import type { JsonRenderSpec } from '../json-render/server.js';
@@ -58,6 +58,7 @@ interface ExportCard {
58
58
  w: number;
59
59
  h: number;
60
60
  group?: true;
61
+ heading?: true;
61
62
  html?: string;
62
63
  frame?: string;
63
64
  }
@@ -66,7 +67,7 @@ interface Collected {
66
67
  manifest: ExportManifest;
67
68
  cards: ExportCard[];
68
69
  edges: CanvasEdge[];
69
- scheme: 'dark' | 'light';
70
+ theme: CanvasThemeName;
70
71
  needsJsonRender: boolean;
71
72
  needsMermaid: boolean;
72
73
  }
@@ -207,9 +208,7 @@ async function collect(
207
208
  const read = canvasState.readBoard(boardId);
208
209
  if (!read) return null;
209
210
  const { board, state } = read;
210
- const theme = canvasThemeScheme(state.theme ?? canvasState.theme);
211
- // Exports collapse to a scheme; surface documents take the theme that draws it.
212
- const surfaceTheme = theme === 'light' ? 'daylight' : 'harbor';
211
+ const theme = normalizeCanvasThemeName(state.theme ?? canvasState.theme);
213
212
  const remoteImages = new Set<string>();
214
213
  const links = new Set<string>();
215
214
  const frameNetworkDestinations = new Set<string>();
@@ -278,6 +277,7 @@ async function collect(
278
277
  card.group = true;
279
278
  break;
280
279
  case 'markdown':
280
+ if (data.display === 'heading') card.heading = true;
281
281
  card.html = `<div class="md">${await markdown.parse(text(data.content))}</div>`;
282
282
  break;
283
283
  case 'status':
@@ -385,8 +385,7 @@ async function collect(
385
385
  const html = text(data.html) || text(data.content);
386
386
  collectFrameNetworkDestinations(html, frameNetworkDestinations);
387
387
  manifest.frames += 1;
388
- if (withFrames)
389
- card.frame = buildHtmlSurfaceDocument(html, { theme: surfaceTheme, title, inlineThemeCss: themeCss });
388
+ if (withFrames) card.frame = buildHtmlSurfaceDocument(html, { theme, title, inlineThemeCss: themeCss });
390
389
  break;
391
390
  }
392
391
  case 'mermaid':
@@ -398,7 +397,7 @@ async function collect(
398
397
  '<script src="/canvas/mermaid-entry.js"></script>',
399
398
  `<script src="${MERMAID_SRC}"></script>`,
400
399
  ),
401
- { theme: surfaceTheme, title, inlineThemeCss: themeCss },
400
+ { theme, title, inlineThemeCss: themeCss },
402
401
  );
403
402
  }
404
403
  break;
@@ -415,7 +414,8 @@ async function collect(
415
414
  title,
416
415
  spec: data.spec as JsonRenderSpec,
417
416
  theme,
418
- display: 'site',
417
+ // The saved card owns the viewport, just as on the live canvas.
418
+ // Omitting display selects embedded layout and its duplicate-title policy.
419
419
  });
420
420
  }
421
421
  break;
@@ -444,7 +444,7 @@ async function collect(
444
444
  manifest.links = [...links];
445
445
  manifest.frameNetworkDestinations = [...frameNetworkDestinations];
446
446
  manifest.embeddedCodeCanAccessNetwork = manifest.frames > 0;
447
- return { manifest, cards, edges: state.edges, scheme: theme, needsJsonRender, needsMermaid };
447
+ return { manifest, cards, edges: state.edges, theme, needsJsonRender, needsMermaid };
448
448
  }
449
449
 
450
450
  /** What an export of `boardId` would put in the file, without building it. */
@@ -467,7 +467,7 @@ export async function buildBoardExport(
467
467
  ): Promise<{ html: string; manifest: ExportManifest } | null> {
468
468
  const collected = await collect(boardId, includeFiles, true, includeDerivedText);
469
469
  if (!collected) return null;
470
- const { manifest, cards, edges: snapshotEdges, scheme, needsJsonRender, needsMermaid } = collected;
470
+ const { manifest, cards, edges: snapshotEdges, theme, needsJsonRender, needsMermaid } = collected;
471
471
 
472
472
  // Store each viewer bundle once; frames reference it by placeholder.
473
473
  const assets: Record<string, string> = {};
@@ -496,7 +496,7 @@ export async function buildBoardExport(
496
496
  const exportedAt = new Date().toISOString();
497
497
  const html = exportPage({
498
498
  title: manifest.boardName,
499
- scheme,
499
+ theme,
500
500
  exportedAt,
501
501
  board: { cards, edges },
502
502
  assets,
@@ -536,7 +536,7 @@ export function exportsFolder(): string | null {
536
536
 
537
537
  function exportPage(input: {
538
538
  title: string;
539
- scheme: 'dark' | 'light';
539
+ theme: CanvasThemeName;
540
540
  exportedAt: string;
541
541
  board: {
542
542
  cards: ExportCard[];
@@ -545,14 +545,22 @@ function exportPage(input: {
545
545
  assets: Record<string, string>;
546
546
  }): string {
547
547
  const date = new Date(input.exportedAt).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' });
548
+ // Use the workbench's shipped faces and shared palette without offline requests.
549
+ const fonts = (readAsset('global.css').match(/@font-face\s*\{[^}]*\}/g) ?? [])
550
+ .join('\n')
551
+ .replace(
552
+ /url\("fonts\/([\w.-]+\.woff2)"\)/g,
553
+ (_, file: string) =>
554
+ `url("data:font/woff2;base64,${readFileSync(join(canvasBundleDir(), 'fonts', file)).toString('base64')}")`,
555
+ );
548
556
  return `<!doctype html>
549
- <html lang="en" data-scheme="${input.scheme}">
557
+ <html lang="en" data-scheme="${canvasThemeScheme(input.theme)}" data-theme="${input.theme}">
550
558
  <head>
551
559
  <meta charset="utf-8">
552
560
  <meta name="viewport" content="width=device-width, initial-scale=1">
553
561
  <meta name="generator" content="PMX Canvas">
554
562
  <title>${escapeHtml(input.title)}</title>
555
- <style>${EXPORT_CSS}</style>
563
+ <style>${fonts}\n${readAsset('surface-theme.css')}\n${EXPORT_CSS}</style>
556
564
  </head>
557
565
  <body>
558
566
  <header class="bar">
@@ -574,10 +582,9 @@ function exportPage(input: {
574
582
  }
575
583
 
576
584
  const EXPORT_CSS = `
577
- :root{--bg:#0b1016;--panel:#121a23;--line:#263241;--text:#e8edf2;--soft:#9aa7b4;--accent:#4bbcff;--add:#2ea66f;--del:#e25d6e}
578
- [data-scheme=light]{--bg:#f4f6f8;--panel:#ffffff;--line:#d5dbe1;--text:#17202a;--soft:#5b6875;--accent:#0a6fc2;--add:#1c7a4f;--del:#b3263a}
585
+ :root{--bg:var(--c-bg);--panel:var(--c-panel);--line:var(--c-line);--text:var(--c-text);--soft:var(--c-muted);--accent:var(--c-accent);--add:var(--c-ok);--del:var(--c-danger)}
579
586
  *{box-sizing:border-box}
580
- html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font:15px/1.65 system-ui,-apple-system,Segoe UI,sans-serif}
587
+ html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font:13px/1.6 var(--font-ui)}
581
588
  .bar{position:fixed;top:0;left:0;right:0;height:48px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 16px;background:var(--panel);border-bottom:1px solid var(--line);z-index:5}
582
589
  .bar .name{display:flex;flex-direction:column;min-width:0}
583
590
  .bar .name strong,.bar .name span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@@ -592,12 +599,16 @@ button:focus-visible,a:focus-visible,[tabindex]:focus-visible{outline:2px solid
592
599
  #edges{position:absolute;left:0;top:0;overflow:visible;pointer-events:none}
593
600
  #edges line{stroke:var(--soft);stroke-width:1.5}
594
601
  #edges text{fill:var(--soft);font-size:12px;text-anchor:middle}
595
- .card{position:absolute;display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden;box-shadow:0 6px 24px rgba(0,0,0,.18)}
602
+ .card{position:absolute;display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:var(--r-node);overflow:hidden;box-shadow:var(--e-1)}
596
603
  .card.group{background:color-mix(in srgb,var(--accent) 5%,transparent);border-style:dashed;box-shadow:none}
597
- .card-title{position:relative;flex-shrink:0;width:100%;text-align:left;padding:12px 42px 12px 18px;font-weight:600;font-size:13px;border:0;border-bottom:1px solid var(--line);border-radius:0;cursor:zoom-in;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background:color-mix(in srgb,var(--soft) 5%,var(--panel))}
604
+ .card-title{position:relative;flex-shrink:0;width:100%;text-align:left;padding:7px 38px 7px 12px;font-weight:600;font-size:12px;line-height:22px;border:0;border-bottom:1px solid var(--line);border-radius:0;cursor:zoom-in;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background:transparent}
598
605
  button.card-title:after{content:'↗';position:absolute;right:16px;color:var(--soft);font-size:16px}
599
606
  .card.group .card-title{border-bottom:none;cursor:default;color:var(--soft)}
600
- .card-body{flex:1;min-height:0;min-width:0;overflow:auto;padding:20px;overflow-wrap:anywhere;overscroll-behavior:contain;cursor:auto}
607
+ .card-body{flex:1;min-height:0;min-width:0;overflow:auto;padding:10px 14px;font-size:var(--fs-ui);color:var(--c-text-soft);overflow-wrap:anywhere;overscroll-behavior:contain;cursor:auto}
608
+ .card.heading{background:transparent;border-color:transparent;box-shadow:none}
609
+ .card.heading .card-title{padding:8px 38px 0 16px;border:0;font-size:var(--fs-heading);line-height:28px;letter-spacing:-.01em}
610
+ .card.heading .card-body{padding:0 16px 8px;color:var(--soft)}
611
+ .card.heading:not(:hover):not(:focus-within) .card-title:after{visibility:hidden}
601
612
  .card-body>:first-child,.md>:first-child{margin-top:0}.card-body>:last-child,.md>:last-child{margin-bottom:0}
602
613
  .card iframe,#overlay-body iframe{flex:1;min-height:0;width:100%;height:100%;border:0;background:var(--bg)}
603
614
  .md h1,.md h2,.md h3,.md h4,.md h5,.md h6{margin:1.4em 0 .6em;line-height:1.3;font-weight:650;letter-spacing:-.02em}
@@ -607,7 +618,7 @@ button.card-title:after{content:'↗';position:absolute;right:16px;color:var(--s
607
618
  .md input[type=checkbox]{accent-color:var(--accent);margin-right:.5em}
608
619
  .md blockquote{margin:1em 0;padding:10px 16px;border-left:3px solid var(--accent);background:color-mix(in srgb,var(--accent) 7%,var(--panel));color:var(--soft)}
609
620
  .md blockquote>:last-child{margin-bottom:0}.md hr{border:0;border-top:1px solid var(--line);margin:24px 0}
610
- .md code,pre{font:13px/1.65 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2}
621
+ .md code,pre{font:13px/1.65 var(--font-code);tab-size:2}
611
622
  .md :not(pre)>code{padding:2px 5px;border:1px solid var(--line);border-radius:4px;background:color-mix(in srgb,var(--soft) 8%,var(--panel))}
612
623
  pre{margin:0 0 16px;padding:14px 16px;border:1px solid var(--line);border-radius:8px;background:color-mix(in srgb,var(--soft) 5%,var(--panel));white-space:pre;overflow:auto;overflow-wrap:normal;overscroll-behavior:contain}
613
624
  .prose{white-space:pre-wrap}.table-scroll{width:100%;max-width:100%;overflow:auto;margin:16px 0;border:1px solid var(--line);border-radius:8px;overscroll-behavior:contain}
@@ -629,7 +640,7 @@ details{margin-top:16px}summary{cursor:pointer;color:var(--soft);margin-bottom:1
629
640
  #overlay .sheet{width:min(1100px,100%);height:min(90vh,100%);display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
630
641
  #overlay .sheet>header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-bottom:1px solid var(--line)}
631
642
  #overlay-body{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}
632
- #overlay-body .card-body{padding:28px 32px}#overlay-body .md{max-width:960px;margin:0 auto}
643
+ #overlay-body .card-body{padding:28px 32px;font-size:15px}#overlay-body .md{max-width:960px;margin:0 auto}
633
644
  @media(max-width:600px){#overlay{padding:8px}#overlay .sheet{height:100%}#overlay-body .card-body{padding:18px}.bar{padding:0 10px}.bar .name span{display:none}}
634
645
  `;
635
646
 
@@ -660,7 +671,7 @@ const body = (card) => {
660
671
  const ordered = [...board.cards].sort((a, b) => (b.group ? 1 : 0) - (a.group ? 1 : 0));
661
672
  for (const card of ordered) {
662
673
  const el = document.createElement('section');
663
- el.className = 'card' + (card.group ? ' group' : '');
674
+ el.className = 'card' + (card.group ? ' group' : '') + (card.heading ? ' heading' : '');
664
675
  el.dataset.nodeId = card.id;
665
676
  el.dataset.nodeType = card.kind;
666
677
  Object.assign(el.style, { left: card.x + 'px', top: card.y + 'px', width: card.w + 'px', height: card.h + 'px' });
@@ -58,6 +58,7 @@ export function validCanvasNodeUpdates(input: unknown): CanvasNodeUpdate[] {
58
58
  return (
59
59
  typeof update.id === 'string' &&
60
60
  (update.collapsed === undefined || typeof update.collapsed === 'boolean') &&
61
+ (update.zIndex === undefined || finite(update.zIndex) !== undefined) &&
61
62
  validateCanvasNodePatch(update) === null
62
63
  );
63
64
  });
@@ -364,6 +364,7 @@ export interface CanvasNodeUpdate {
364
364
  position?: { x: number; y: number };
365
365
  size?: { width: number; height: number };
366
366
  collapsed?: boolean;
367
+ zIndex?: number;
367
368
  }
368
369
 
369
370
  export type CanvasChangeType = 'pins' | 'nodes' | 'ax' | 'ax-timeline';
@@ -3220,6 +3221,9 @@ class CanvasStateManager {
3220
3221
  if (update.collapsed !== undefined && update.collapsed !== existing.collapsed) {
3221
3222
  nextPatch.collapsed = update.collapsed;
3222
3223
  }
3224
+ if (update.zIndex !== undefined && update.zIndex !== existing.zIndex) {
3225
+ nextPatch.zIndex = update.zIndex;
3226
+ }
3223
3227
  if (Object.keys(nextPatch).length === 0) {
3224
3228
  skipped++;
3225
3229
  continue;
@@ -3977,4 +3981,5 @@ class CanvasStateManager {
3977
3981
  // Module-level singleton — safe because Bun is single-threaded and this
3978
3982
  // module is imported once per process. Agent tools and the HTTP server share
3979
3983
  // the same instance; no locking needed.
3984
+ export const boardCatalogEpoch = randomUUID();
3980
3985
  export const canvasState = new CanvasStateManager();