pmx-canvas 0.1.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 (226) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/Readme.md +865 -0
  4. package/dist/canvas/global.css +3173 -0
  5. package/dist/canvas/index.js +183 -0
  6. package/dist/json-render/index.css +2 -0
  7. package/dist/json-render/index.js +389 -0
  8. package/dist/types/cli/agent.d.ts +13 -0
  9. package/dist/types/cli/index.d.ts +2 -0
  10. package/dist/types/cli/watch.d.ts +5 -0
  11. package/dist/types/client/App.d.ts +1 -0
  12. package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
  13. package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
  14. package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
  15. package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
  16. package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
  18. package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
  19. package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
  20. package/dist/types/client/canvas/DockedNode.d.ts +4 -0
  21. package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
  23. package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
  24. package/dist/types/client/canvas/Minimap.d.ts +23 -0
  25. package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
  26. package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
  27. package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
  28. package/dist/types/client/canvas/snap-guides.d.ts +23 -0
  29. package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
  30. package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
  31. package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
  32. package/dist/types/client/ext-app/bridge.d.ts +161 -0
  33. package/dist/types/client/icons.d.ts +70 -0
  34. package/dist/types/client/index.d.ts +1 -0
  35. package/dist/types/client/nodes/ContextNode.d.ts +34 -0
  36. package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
  37. package/dist/types/client/nodes/FileNode.d.ts +5 -0
  38. package/dist/types/client/nodes/GroupNode.d.ts +6 -0
  39. package/dist/types/client/nodes/ImageNode.d.ts +10 -0
  40. package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
  41. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
  42. package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
  43. package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
  44. package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
  45. package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
  46. package/dist/types/client/nodes/PromptNode.d.ts +5 -0
  47. package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
  48. package/dist/types/client/nodes/StatusNode.d.ts +4 -0
  49. package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
  50. package/dist/types/client/nodes/TraceNode.d.ts +4 -0
  51. package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
  52. package/dist/types/client/nodes/image-warnings.d.ts +6 -0
  53. package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
  54. package/dist/types/client/nodes/md-format.d.ts +25 -0
  55. package/dist/types/client/state/attention-bridge.d.ts +3 -0
  56. package/dist/types/client/state/attention-store.d.ts +25 -0
  57. package/dist/types/client/state/canvas-store.d.ts +74 -0
  58. package/dist/types/client/state/intent-bridge.d.ts +158 -0
  59. package/dist/types/client/state/sse-bridge.d.ts +5 -0
  60. package/dist/types/client/theme/tokens.d.ts +27 -0
  61. package/dist/types/client/types.d.ts +40 -0
  62. package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
  63. package/dist/types/client/utils/placement.d.ts +1 -0
  64. package/dist/types/client/utils/platform.d.ts +2 -0
  65. package/dist/types/json-render/catalog.d.ts +815 -0
  66. package/dist/types/json-render/charts/components.d.ts +54 -0
  67. package/dist/types/json-render/charts/definitions.d.ts +103 -0
  68. package/dist/types/json-render/charts/extra-components.d.ts +58 -0
  69. package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
  70. package/dist/types/json-render/renderer/index.d.ts +16 -0
  71. package/dist/types/json-render/schema.d.ts +46 -0
  72. package/dist/types/json-render/server.d.ts +55 -0
  73. package/dist/types/mcp/server.d.ts +22 -0
  74. package/dist/types/server/agent-context.d.ts +21 -0
  75. package/dist/types/server/artifact-paths.d.ts +3 -0
  76. package/dist/types/server/canvas-operations.d.ts +154 -0
  77. package/dist/types/server/canvas-provenance.d.ts +13 -0
  78. package/dist/types/server/canvas-schema.d.ts +49 -0
  79. package/dist/types/server/canvas-serialization.d.ts +25 -0
  80. package/dist/types/server/canvas-state.d.ts +174 -0
  81. package/dist/types/server/canvas-validation.d.ts +33 -0
  82. package/dist/types/server/chart-template.d.ts +29 -0
  83. package/dist/types/server/code-graph.d.ts +67 -0
  84. package/dist/types/server/context-cards.d.ts +24 -0
  85. package/dist/types/server/diagram-presets.d.ts +28 -0
  86. package/dist/types/server/ext-app-call-registry.d.ts +16 -0
  87. package/dist/types/server/ext-app-tool-result.d.ts +1 -0
  88. package/dist/types/server/file-watcher.d.ts +16 -0
  89. package/dist/types/server/index.d.ts +243 -0
  90. package/dist/types/server/mcp-app-candidate.d.ts +25 -0
  91. package/dist/types/server/mcp-app-host.d.ts +65 -0
  92. package/dist/types/server/mcp-app-runtime.d.ts +47 -0
  93. package/dist/types/server/mutation-history.d.ts +105 -0
  94. package/dist/types/server/placement.d.ts +37 -0
  95. package/dist/types/server/server.d.ts +103 -0
  96. package/dist/types/server/spatial-analysis.d.ts +87 -0
  97. package/dist/types/server/trace-manager.d.ts +48 -0
  98. package/dist/types/server/web-artifacts.d.ts +50 -0
  99. package/dist/types/server/webpage-node.d.ts +25 -0
  100. package/dist/types/shared/auto-arrange.d.ts +29 -0
  101. package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
  102. package/dist/types/shared/placement.d.ts +26 -0
  103. package/dist/types/shared/semantic-attention.d.ts +97 -0
  104. package/package.json +109 -0
  105. package/skills/data-analysis/SKILL.md +324 -0
  106. package/skills/doc-coauthoring/SKILL.md +375 -0
  107. package/skills/frontend-design/SKILL.md +45 -0
  108. package/skills/json-render-codegen/SKILL.md +112 -0
  109. package/skills/json-render-core/SKILL.md +265 -0
  110. package/skills/json-render-ink/SKILL.md +273 -0
  111. package/skills/json-render-mcp/SKILL.md +132 -0
  112. package/skills/json-render-react/SKILL.md +264 -0
  113. package/skills/json-render-shadcn/SKILL.md +159 -0
  114. package/skills/playwright-cli/SKILL.md +67 -0
  115. package/skills/pmx-canvas/SKILL.md +668 -0
  116. package/skills/pmx-canvas/evals/evals.json +186 -0
  117. package/skills/pmx-canvas-testing/SKILL.md +78 -0
  118. package/skills/published-consumer-e2e/SKILL.md +43 -0
  119. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
  120. package/skills/web-artifacts-builder/SKILL.md +80 -0
  121. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
  122. package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
  123. package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  124. package/skills/web-design-guidelines/SKILL.md +39 -0
  125. package/src/cli/agent.ts +2144 -0
  126. package/src/cli/index.ts +622 -0
  127. package/src/cli/watch.ts +88 -0
  128. package/src/client/App.tsx +507 -0
  129. package/src/client/canvas/AttentionHistory.tsx +81 -0
  130. package/src/client/canvas/AttentionToast.tsx +19 -0
  131. package/src/client/canvas/CanvasNode.tsx +363 -0
  132. package/src/client/canvas/CanvasViewport.tsx +590 -0
  133. package/src/client/canvas/CommandPalette.tsx +302 -0
  134. package/src/client/canvas/ContextMenu.tsx +601 -0
  135. package/src/client/canvas/ContextPinBar.tsx +25 -0
  136. package/src/client/canvas/ContextPinHud.tsx +22 -0
  137. package/src/client/canvas/DockedNode.tsx +66 -0
  138. package/src/client/canvas/EdgeLayer.tsx +280 -0
  139. package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
  140. package/src/client/canvas/FocusFieldLayer.tsx +107 -0
  141. package/src/client/canvas/Minimap.tsx +301 -0
  142. package/src/client/canvas/SelectionBar.tsx +69 -0
  143. package/src/client/canvas/ShortcutOverlay.tsx +69 -0
  144. package/src/client/canvas/SnapshotPanel.tsx +236 -0
  145. package/src/client/canvas/snap-guides.ts +170 -0
  146. package/src/client/canvas/use-node-drag.ts +51 -0
  147. package/src/client/canvas/use-node-resize.ts +59 -0
  148. package/src/client/canvas/use-pan-zoom.ts +191 -0
  149. package/src/client/ext-app/bridge.ts +542 -0
  150. package/src/client/icons.tsx +424 -0
  151. package/src/client/index.tsx +7 -0
  152. package/src/client/nodes/ContextNode.tsx +412 -0
  153. package/src/client/nodes/ExtAppFrame.tsx +509 -0
  154. package/src/client/nodes/FileNode.tsx +256 -0
  155. package/src/client/nodes/GroupNode.tsx +39 -0
  156. package/src/client/nodes/ImageNode.tsx +160 -0
  157. package/src/client/nodes/InlineFormatBar.tsx +169 -0
  158. package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
  159. package/src/client/nodes/LedgerNode.tsx +37 -0
  160. package/src/client/nodes/MarkdownNode.tsx +359 -0
  161. package/src/client/nodes/McpAppNode.tsx +85 -0
  162. package/src/client/nodes/MdFormatBar.tsx +109 -0
  163. package/src/client/nodes/PromptNode.tsx +597 -0
  164. package/src/client/nodes/ResponseNode.tsx +153 -0
  165. package/src/client/nodes/StatusNode.tsx +84 -0
  166. package/src/client/nodes/StatusSummary.tsx +38 -0
  167. package/src/client/nodes/TraceNode.tsx +120 -0
  168. package/src/client/nodes/WebpageNode.tsx +288 -0
  169. package/src/client/nodes/image-warnings.ts +95 -0
  170. package/src/client/nodes/inline-editor-commands.ts +37 -0
  171. package/src/client/nodes/md-format.ts +206 -0
  172. package/src/client/state/attention-bridge.ts +328 -0
  173. package/src/client/state/attention-store.ts +73 -0
  174. package/src/client/state/canvas-store.ts +631 -0
  175. package/src/client/state/intent-bridge.ts +315 -0
  176. package/src/client/state/sse-bridge.ts +965 -0
  177. package/src/client/theme/global.css +3173 -0
  178. package/src/client/theme/tokens.ts +72 -0
  179. package/src/client/types-shims.d.ts +5 -0
  180. package/src/client/types.ts +81 -0
  181. package/src/client/utils/ext-app-tool-result.ts +4 -0
  182. package/src/client/utils/placement.ts +4 -0
  183. package/src/client/utils/platform.ts +2 -0
  184. package/src/json-render/catalog.ts +256 -0
  185. package/src/json-render/charts/components.tsx +198 -0
  186. package/src/json-render/charts/definitions.ts +81 -0
  187. package/src/json-render/charts/extra-components.tsx +267 -0
  188. package/src/json-render/charts/extra-definitions.ts +145 -0
  189. package/src/json-render/renderer/index.css +174 -0
  190. package/src/json-render/renderer/index.tsx +86 -0
  191. package/src/json-render/schema.ts +62 -0
  192. package/src/json-render/server.ts +597 -0
  193. package/src/mcp/server.ts +1377 -0
  194. package/src/server/agent-context.ts +242 -0
  195. package/src/server/artifact-paths.ts +17 -0
  196. package/src/server/canvas-operations.ts +1279 -0
  197. package/src/server/canvas-provenance.ts +243 -0
  198. package/src/server/canvas-schema.ts +432 -0
  199. package/src/server/canvas-serialization.ts +95 -0
  200. package/src/server/canvas-state.ts +1134 -0
  201. package/src/server/canvas-validation.ts +114 -0
  202. package/src/server/chart-template.ts +449 -0
  203. package/src/server/code-graph.ts +370 -0
  204. package/src/server/context-cards.ts +31 -0
  205. package/src/server/diagram-presets.ts +71 -0
  206. package/src/server/ext-app-call-registry.ts +77 -0
  207. package/src/server/ext-app-tool-result.ts +4 -0
  208. package/src/server/file-watcher.ts +121 -0
  209. package/src/server/index.ts +647 -0
  210. package/src/server/mcp-app-candidate.ts +174 -0
  211. package/src/server/mcp-app-host.ts +814 -0
  212. package/src/server/mcp-app-runtime.ts +459 -0
  213. package/src/server/mutation-history.ts +350 -0
  214. package/src/server/placement.ts +125 -0
  215. package/src/server/server.ts +3846 -0
  216. package/src/server/spatial-analysis.ts +356 -0
  217. package/src/server/trace-manager.ts +333 -0
  218. package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
  219. package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
  220. package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
  221. package/src/server/web-artifacts.ts +442 -0
  222. package/src/server/webpage-node.ts +328 -0
  223. package/src/shared/auto-arrange.ts +439 -0
  224. package/src/shared/ext-app-tool-result.ts +76 -0
  225. package/src/shared/placement.ts +81 -0
  226. package/src/shared/semantic-attention.ts +598 -0
@@ -0,0 +1,114 @@
1
+ import type { CanvasLayout, CanvasNodeState } from './canvas-state.js';
2
+ import { getCanvasNodeTitle } from './canvas-serialization.js';
3
+
4
+ export interface CanvasValidationPair {
5
+ aId: string;
6
+ aTitle: string | null;
7
+ bId: string;
8
+ bTitle: string | null;
9
+ }
10
+
11
+ export interface CanvasContainmentIssue {
12
+ groupId: string;
13
+ groupTitle: string | null;
14
+ childId: string;
15
+ childTitle: string | null;
16
+ }
17
+
18
+ export interface CanvasValidationResult {
19
+ ok: boolean;
20
+ collisions: CanvasValidationPair[];
21
+ containments: CanvasContainmentIssue[];
22
+ containmentViolations: CanvasContainmentIssue[];
23
+ missingEdgeEndpoints: Array<{ edgeId: string; from: string; to: string }>;
24
+ summary: {
25
+ nodes: number;
26
+ edges: number;
27
+ collisions: number;
28
+ containments: number;
29
+ containmentViolations: number;
30
+ missingEdgeEndpoints: number;
31
+ };
32
+ }
33
+
34
+ function overlaps(a: CanvasNodeState, b: CanvasNodeState): boolean {
35
+ return (
36
+ a.position.x < b.position.x + b.size.width &&
37
+ a.position.x + a.size.width > b.position.x &&
38
+ a.position.y < b.position.y + b.size.height &&
39
+ a.position.y + a.size.height > b.position.y
40
+ );
41
+ }
42
+
43
+ function fullyContains(group: CanvasNodeState, child: CanvasNodeState): boolean {
44
+ return (
45
+ child.position.x >= group.position.x &&
46
+ child.position.y >= group.position.y &&
47
+ child.position.x + child.size.width <= group.position.x + group.size.width &&
48
+ child.position.y + child.size.height <= group.position.y + group.size.height
49
+ );
50
+ }
51
+
52
+ function pair(a: CanvasNodeState, b: CanvasNodeState): CanvasValidationPair {
53
+ return {
54
+ aId: a.id,
55
+ aTitle: getCanvasNodeTitle(a),
56
+ bId: b.id,
57
+ bTitle: getCanvasNodeTitle(b),
58
+ };
59
+ }
60
+
61
+ function containment(group: CanvasNodeState, child: CanvasNodeState): CanvasContainmentIssue {
62
+ return {
63
+ groupId: group.id,
64
+ groupTitle: getCanvasNodeTitle(group),
65
+ childId: child.id,
66
+ childTitle: getCanvasNodeTitle(child),
67
+ };
68
+ }
69
+
70
+ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResult {
71
+ const collisions: CanvasValidationPair[] = [];
72
+ const containments: CanvasContainmentIssue[] = [];
73
+ const containmentViolations: CanvasContainmentIssue[] = [];
74
+
75
+ for (let i = 0; i < layout.nodes.length; i++) {
76
+ const a = layout.nodes[i]!;
77
+ for (let j = i + 1; j < layout.nodes.length; j++) {
78
+ const b = layout.nodes[j]!;
79
+ if (!overlaps(a, b)) continue;
80
+
81
+ if (a.type === 'group' && b.data.parentGroup === a.id) {
82
+ (fullyContains(a, b) ? containments : containmentViolations).push(containment(a, b));
83
+ continue;
84
+ }
85
+ if (b.type === 'group' && a.data.parentGroup === b.id) {
86
+ (fullyContains(b, a) ? containments : containmentViolations).push(containment(b, a));
87
+ continue;
88
+ }
89
+
90
+ collisions.push(pair(a, b));
91
+ }
92
+ }
93
+
94
+ const nodeIds = new Set(layout.nodes.map((node) => node.id));
95
+ const missingEdgeEndpoints = layout.edges
96
+ .filter((edge) => !nodeIds.has(edge.from) || !nodeIds.has(edge.to))
97
+ .map((edge) => ({ edgeId: edge.id, from: edge.from, to: edge.to }));
98
+
99
+ return {
100
+ ok: collisions.length === 0 && containmentViolations.length === 0 && missingEdgeEndpoints.length === 0,
101
+ collisions,
102
+ containments,
103
+ containmentViolations,
104
+ missingEdgeEndpoints,
105
+ summary: {
106
+ nodes: layout.nodes.length,
107
+ edges: layout.edges.length,
108
+ collisions: collisions.length,
109
+ containments: containments.length,
110
+ containmentViolations: containmentViolations.length,
111
+ missingEdgeEndpoints: missingEdgeEndpoints.length,
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,449 @@
1
+ /**
2
+ * Chart HTML template generator — produces self-contained ext-app HTML
3
+ * documents that render interactive Chart.js charts inside the canvas
4
+ * ExtAppFrame iframe.
5
+ *
6
+ * The generated HTML:
7
+ * 1. Renders immediately from inline data (no bridge needed)
8
+ * 2. Connects to host AppBridge via the embedded ext-app App SDK runtime
9
+ * 3. Accepts updated data via toolInput for re-rendering
10
+ */
11
+
12
+ import { readFileSync } from 'node:fs';
13
+ import { dirname, join } from 'node:path';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const extAppsPackageDir = dirname(require.resolve('@modelcontextprotocol/ext-apps/package.json'));
18
+ const extAppsRuntimeSource = readFileSync(
19
+ join(extAppsPackageDir, 'dist', 'src', 'app-with-deps.js'),
20
+ 'utf-8',
21
+ );
22
+ const appBindingMatch = extAppsRuntimeSource.match(/([A-Za-z_$][\w$]*) as App/);
23
+ const transportBindingMatch = extAppsRuntimeSource.match(/([A-Za-z_$][\w$]*) as PostMessageTransport/);
24
+
25
+ if (!appBindingMatch || !transportBindingMatch) {
26
+ throw new Error('Failed to locate App or PostMessageTransport export bindings in @modelcontextprotocol/ext-apps runtime');
27
+ }
28
+
29
+ const extAppsBootstrapSource = `${extAppsRuntimeSource}
30
+ const App = ${appBindingMatch[1]};
31
+ const PostMessageTransport = ${transportBindingMatch[1]};`;
32
+
33
+ export interface ChartDataset {
34
+ label: string;
35
+ values: number[];
36
+ color?: string;
37
+ }
38
+
39
+ export interface ChartConfig {
40
+ title: string;
41
+ chartType: 'bar' | 'line' | 'pie' | 'scatter' | 'doughnut' | 'radar';
42
+ labels: string[];
43
+ datasets: ChartDataset[];
44
+ xAxisLabel?: string;
45
+ yAxisLabel?: string;
46
+ stacked?: boolean;
47
+ }
48
+
49
+ const PALETTE = ['#46b6ff', '#2fd07f', '#f4c542', '#ff6a7f', '#e896ff', '#ff9f40', '#a7b2c8'];
50
+
51
+ /**
52
+ * Map our simplified config to a Chart.js configuration object.
53
+ */
54
+ function buildChartJsConfig(config: ChartConfig): Record<string, unknown> {
55
+ const type = config.chartType === 'radar' ? 'radar' : config.chartType;
56
+ const isPolar = type === 'pie' || type === 'doughnut' || type === 'radar';
57
+
58
+ const datasets = config.datasets.map((ds, i) => {
59
+ const color = ds.color || PALETTE[i % PALETTE.length];
60
+ const base: Record<string, unknown> = {
61
+ label: ds.label,
62
+ data: ds.values,
63
+ };
64
+
65
+ if (isPolar) {
66
+ // Pie/doughnut/radar: per-segment colors
67
+ base.backgroundColor = config.labels.map((_, j) => `${PALETTE[j % PALETTE.length]}cc`);
68
+ base.borderColor = config.labels.map((_, j) => PALETTE[j % PALETTE.length]);
69
+ base.borderWidth = 1;
70
+ } else {
71
+ base.backgroundColor = `${color}99`;
72
+ base.borderColor = color;
73
+ base.borderWidth = 2;
74
+ if (type === 'line') {
75
+ base.tension = 0.3;
76
+ base.fill = false;
77
+ base.pointRadius = 4;
78
+ base.pointHoverRadius = 6;
79
+ }
80
+ }
81
+
82
+ return base;
83
+ });
84
+
85
+ const scales: Record<string, unknown> = {};
86
+ if (!isPolar) {
87
+ scales.x = {
88
+ grid: { color: 'rgba(255,255,255,0.06)' },
89
+ ticks: { color: '#7b8da8', font: { size: 11 } },
90
+ ...(config.xAxisLabel && {
91
+ title: { display: true, text: config.xAxisLabel, color: '#a7b2c8', font: { size: 12 } },
92
+ }),
93
+ ...(config.stacked && { stacked: true }),
94
+ };
95
+ scales.y = {
96
+ grid: { color: 'rgba(255,255,255,0.06)' },
97
+ ticks: { color: '#7b8da8', font: { size: 11 } },
98
+ ...(config.yAxisLabel && {
99
+ title: { display: true, text: config.yAxisLabel, color: '#a7b2c8', font: { size: 12 } },
100
+ }),
101
+ ...(config.stacked && { stacked: true }),
102
+ };
103
+ }
104
+
105
+ return {
106
+ type,
107
+ data: { labels: config.labels, datasets },
108
+ options: {
109
+ responsive: true,
110
+ maintainAspectRatio: false,
111
+ animation: { duration: 400 },
112
+ plugins: {
113
+ legend: {
114
+ display: datasets.length > 1 || isPolar,
115
+ labels: { color: '#a7b2c8', font: { size: 11 }, padding: 12 },
116
+ },
117
+ tooltip: {
118
+ backgroundColor: 'rgba(26,29,35,0.95)',
119
+ titleColor: '#e0e4ec',
120
+ bodyColor: '#a7b2c8',
121
+ borderColor: 'rgba(255,255,255,0.1)',
122
+ borderWidth: 1,
123
+ padding: 10,
124
+ cornerRadius: 6,
125
+ },
126
+ },
127
+ ...(!isPolar && { scales }),
128
+ },
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Generate a self-contained HTML document that renders a Chart.js chart
134
+ * and optionally connects to the host via the ext-app App SDK.
135
+ */
136
+ export function generateChartHtml(config: ChartConfig): string {
137
+ const chartJsConfig = buildChartJsConfig(config);
138
+ const configJson = JSON.stringify(chartJsConfig);
139
+ const chartConfigJson = JSON.stringify(config);
140
+ const titleEscaped = escapeHtml(config.title);
141
+
142
+ // Chart type buttons — highlight the active one
143
+ const chartTypes: Array<{ key: string; label: string }> = [
144
+ { key: 'bar', label: 'Bar' },
145
+ { key: 'line', label: 'Line' },
146
+ { key: 'pie', label: 'Pie' },
147
+ { key: 'scatter', label: 'Scatter' },
148
+ { key: 'doughnut', label: 'Donut' },
149
+ { key: 'radar', label: 'Radar' },
150
+ ];
151
+ const typeButtons = chartTypes
152
+ .map(
153
+ (t) =>
154
+ `<button class="type-btn${t.key === config.chartType ? ' active' : ''}" data-type="${t.key}">${t.label}</button>`,
155
+ )
156
+ .join('\n ');
157
+
158
+ return `<!DOCTYPE html>
159
+ <html lang="en">
160
+ <head>
161
+ <meta charset="utf-8">
162
+ <meta name="viewport" content="width=device-width, initial-scale=1">
163
+ <style>
164
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
165
+ html {
166
+ width: 100%;
167
+ height: 100%;
168
+ }
169
+ body {
170
+ background: #1a1d23;
171
+ color: #e0e4ec;
172
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
173
+ overflow: hidden;
174
+ width: 100%;
175
+ height: 100%;
176
+ }
177
+ #container {
178
+ width: 100%;
179
+ height: 100%;
180
+ display: flex;
181
+ flex-direction: column;
182
+ min-width: 0;
183
+ min-height: 0;
184
+ }
185
+ .toolbar {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 4px;
189
+ padding: 8px 12px;
190
+ background: rgba(255,255,255,0.03);
191
+ border-bottom: 1px solid rgba(255,255,255,0.06);
192
+ flex-shrink: 0;
193
+ }
194
+ .chart-title {
195
+ font-size: 13px;
196
+ font-weight: 600;
197
+ color: #e0e4ec;
198
+ margin-right: auto;
199
+ white-space: nowrap;
200
+ overflow: hidden;
201
+ text-overflow: ellipsis;
202
+ }
203
+ .type-btn {
204
+ padding: 3px 8px;
205
+ border: 1px solid rgba(255,255,255,0.1);
206
+ border-radius: 4px;
207
+ background: transparent;
208
+ color: #7b8da8;
209
+ font-size: 11px;
210
+ cursor: pointer;
211
+ transition: all 0.15s;
212
+ white-space: nowrap;
213
+ }
214
+ .type-btn:hover { background: rgba(255,255,255,0.05); color: #e0e4ec; }
215
+ .type-btn.active {
216
+ background: rgba(70,182,255,0.15);
217
+ border-color: rgba(70,182,255,0.3);
218
+ color: #46b6ff;
219
+ }
220
+ .chart-area {
221
+ flex: 1;
222
+ padding: 12px;
223
+ position: relative;
224
+ min-width: 0;
225
+ min-height: 0;
226
+ }
227
+ #chart {
228
+ display: block;
229
+ width: 100% !important;
230
+ height: 100% !important;
231
+ }
232
+ .empty-state {
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: center;
236
+ height: 100%;
237
+ color: #7b8da8;
238
+ font-size: 13px;
239
+ }
240
+ </style>
241
+ </head>
242
+ <body>
243
+ <div id="container">
244
+ <div class="toolbar">
245
+ <span class="chart-title">${titleEscaped}</span>
246
+ ${typeButtons}
247
+ </div>
248
+ <div class="chart-area">
249
+ <canvas id="chart"></canvas>
250
+ </div>
251
+ </div>
252
+
253
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
254
+ <script>
255
+ // Inline chart configuration — renders immediately without bridge
256
+ var CHART_CONFIG = ${configJson};
257
+ var CHART_META = ${chartConfigJson};
258
+ var chartInstance = null;
259
+ var chartResizeObserver = null;
260
+ var chartResizeRaf = null;
261
+
262
+ function getChartArea() {
263
+ return document.querySelector('.chart-area');
264
+ }
265
+
266
+ function getChartSize() {
267
+ var area = getChartArea();
268
+ if (!area) return null;
269
+ var rect = area.getBoundingClientRect();
270
+ if (!rect || rect.width < 24 || rect.height < 24) return null;
271
+ return rect;
272
+ }
273
+
274
+ function scheduleChartResize() {
275
+ if (chartResizeRaf) cancelAnimationFrame(chartResizeRaf);
276
+ chartResizeRaf = requestAnimationFrame(function() {
277
+ chartResizeRaf = null;
278
+ if (chartInstance) chartInstance.resize();
279
+ });
280
+ }
281
+
282
+ function ensureChartResizeTracking() {
283
+ var area = getChartArea();
284
+ if (!area || chartResizeObserver || typeof ResizeObserver !== 'function') return;
285
+ chartResizeObserver = new ResizeObserver(function() {
286
+ if (chartInstance) {
287
+ scheduleChartResize();
288
+ return;
289
+ }
290
+ if (getChartSize()) {
291
+ renderChart(CHART_CONFIG);
292
+ }
293
+ });
294
+ chartResizeObserver.observe(area);
295
+ window.addEventListener('resize', scheduleChartResize);
296
+ }
297
+
298
+ function renderChart(cfg) {
299
+ var size = getChartSize();
300
+ if (!size) return false;
301
+ if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
302
+ var canvas = document.getElementById('chart');
303
+ if (!canvas) return;
304
+ canvas.width = Math.max(1, Math.floor(size.width));
305
+ canvas.height = Math.max(1, Math.floor(size.height));
306
+ chartInstance = new Chart(canvas.getContext('2d'), JSON.parse(JSON.stringify(cfg)));
307
+ scheduleChartResize();
308
+ return true;
309
+ }
310
+
311
+ function renderWhenReady(cfg, attempt) {
312
+ if (renderChart(cfg)) return;
313
+ if ((attempt || 0) >= 20) return;
314
+ requestAnimationFrame(function() {
315
+ renderWhenReady(cfg, (attempt || 0) + 1);
316
+ });
317
+ }
318
+
319
+ function switchType(newType) {
320
+ // Rebuild config for the new chart type using stored metadata
321
+ var meta = JSON.parse(JSON.stringify(CHART_META));
322
+ meta.chartType = newType;
323
+ CHART_META = meta;
324
+
325
+ // Update button states
326
+ document.querySelectorAll('.type-btn').forEach(function(btn) {
327
+ btn.classList.toggle('active', btn.dataset.type === newType);
328
+ });
329
+
330
+ // Post message to request new config from parent (or rebuild locally)
331
+ var isPolar = (newType === 'pie' || newType === 'doughnut' || newType === 'radar');
332
+ var palette = ${JSON.stringify(PALETTE)};
333
+
334
+ var datasets = meta.datasets.map(function(ds, i) {
335
+ var color = ds.color || palette[i % palette.length];
336
+ var base = { label: ds.label, data: ds.values };
337
+ if (isPolar) {
338
+ base.backgroundColor = meta.labels.map(function(_, j) { return palette[j % palette.length] + 'cc'; });
339
+ base.borderColor = meta.labels.map(function(_, j) { return palette[j % palette.length]; });
340
+ base.borderWidth = 1;
341
+ } else {
342
+ base.backgroundColor = color + '99';
343
+ base.borderColor = color;
344
+ base.borderWidth = 2;
345
+ if (newType === 'line') { base.tension = 0.3; base.fill = false; base.pointRadius = 4; }
346
+ }
347
+ return base;
348
+ });
349
+
350
+ var scales = {};
351
+ if (!isPolar) {
352
+ scales.x = {
353
+ grid: { color: 'rgba(255,255,255,0.06)' },
354
+ ticks: { color: '#7b8da8', font: { size: 11 } },
355
+ stacked: !!meta.stacked
356
+ };
357
+ scales.y = {
358
+ grid: { color: 'rgba(255,255,255,0.06)' },
359
+ ticks: { color: '#7b8da8', font: { size: 11 } },
360
+ stacked: !!meta.stacked
361
+ };
362
+ if (meta.xAxisLabel) scales.x.title = { display: true, text: meta.xAxisLabel, color: '#a7b2c8' };
363
+ if (meta.yAxisLabel) scales.y.title = { display: true, text: meta.yAxisLabel, color: '#a7b2c8' };
364
+ }
365
+
366
+ var newCfg = {
367
+ type: newType,
368
+ data: { labels: meta.labels, datasets: datasets },
369
+ options: {
370
+ responsive: true,
371
+ maintainAspectRatio: false,
372
+ animation: { duration: 300 },
373
+ plugins: {
374
+ legend: {
375
+ display: datasets.length > 1 || isPolar,
376
+ labels: { color: '#a7b2c8', font: { size: 11 }, padding: 12 }
377
+ },
378
+ tooltip: {
379
+ backgroundColor: 'rgba(26,29,35,0.95)',
380
+ titleColor: '#e0e4ec',
381
+ bodyColor: '#a7b2c8',
382
+ borderColor: 'rgba(255,255,255,0.1)',
383
+ borderWidth: 1, padding: 10, cornerRadius: 6
384
+ }
385
+ },
386
+ scales: isPolar ? undefined : scales
387
+ }
388
+ };
389
+ CHART_CONFIG = newCfg;
390
+ renderWhenReady(newCfg, 0);
391
+ }
392
+
393
+ window.__PMX_CHART_BRIDGE__ = {
394
+ updateChartMeta: function(nextMeta) {
395
+ if (!nextMeta || typeof nextMeta !== 'object') return;
396
+ CHART_META = nextMeta;
397
+ switchType(CHART_META.chartType || 'bar');
398
+ }
399
+ };
400
+
401
+ // Toolbar click handler
402
+ document.querySelector('.toolbar').addEventListener('click', function(e) {
403
+ var btn = e.target.closest('.type-btn');
404
+ if (btn && btn.dataset.type) switchType(btn.dataset.type);
405
+ });
406
+
407
+ // Initial render
408
+ window.addEventListener('load', function() {
409
+ ensureChartResizeTracking();
410
+ if (CHART_META.datasets.length === 0 || CHART_META.labels.length === 0) {
411
+ document.querySelector('.chart-area').innerHTML =
412
+ '<div class="empty-state">No data to display</div>';
413
+ return;
414
+ }
415
+ renderWhenReady(CHART_CONFIG, 0);
416
+ });
417
+ </script>
418
+ <script type="module">
419
+ ${extAppsBootstrapSource}
420
+
421
+ try {
422
+ if (!App) {
423
+ throw new Error('AppBridge SDK unavailable');
424
+ }
425
+
426
+ const bridge = window.__PMX_CHART_BRIDGE__;
427
+ const app = new App({ name: 'PMX Chart', version: '1.0.0' }, {});
428
+ app.ontoolinput = function(params) {
429
+ bridge?.updateChartMeta?.(params?.arguments);
430
+ };
431
+ await app.connect(new PostMessageTransport(window.parent, window.parent));
432
+ } catch (error) {
433
+ // Bridge connection optional — chart already rendered from inline data
434
+ const message = error instanceof Error ? error.message : String(error);
435
+ console.debug('[pmx-chart] AppBridge not available:', message);
436
+ }
437
+ </script>
438
+ </body>
439
+ </html>`;
440
+ }
441
+
442
+ function escapeHtml(str: string): string {
443
+ return str
444
+ .replace(/&/g, '&amp;')
445
+ .replace(/</g, '&lt;')
446
+ .replace(/>/g, '&gt;')
447
+ .replace(/"/g, '&quot;')
448
+ .replace(/'/g, '&#x27;');
449
+ }