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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/Readme.md +865 -0
- package/dist/canvas/global.css +3173 -0
- package/dist/canvas/index.js +183 -0
- package/dist/json-render/index.css +2 -0
- package/dist/json-render/index.js +389 -0
- package/dist/types/cli/agent.d.ts +13 -0
- package/dist/types/cli/index.d.ts +2 -0
- package/dist/types/cli/watch.d.ts +5 -0
- package/dist/types/client/App.d.ts +1 -0
- package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
- package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
- package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
- package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
- package/dist/types/client/canvas/DockedNode.d.ts +4 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
- package/dist/types/client/canvas/Minimap.d.ts +23 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
- package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
- package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
- package/dist/types/client/canvas/snap-guides.d.ts +23 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
- package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
- package/dist/types/client/ext-app/bridge.d.ts +161 -0
- package/dist/types/client/icons.d.ts +70 -0
- package/dist/types/client/index.d.ts +1 -0
- package/dist/types/client/nodes/ContextNode.d.ts +34 -0
- package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
- package/dist/types/client/nodes/FileNode.d.ts +5 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -0
- package/dist/types/client/nodes/ImageNode.d.ts +10 -0
- package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
- package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
- package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
- package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
- package/dist/types/client/nodes/PromptNode.d.ts +5 -0
- package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
- package/dist/types/client/nodes/StatusNode.d.ts +4 -0
- package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
- package/dist/types/client/nodes/TraceNode.d.ts +4 -0
- package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
- package/dist/types/client/nodes/image-warnings.d.ts +6 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
- package/dist/types/client/nodes/md-format.d.ts +25 -0
- package/dist/types/client/state/attention-bridge.d.ts +3 -0
- package/dist/types/client/state/attention-store.d.ts +25 -0
- package/dist/types/client/state/canvas-store.d.ts +74 -0
- package/dist/types/client/state/intent-bridge.d.ts +158 -0
- package/dist/types/client/state/sse-bridge.d.ts +5 -0
- package/dist/types/client/theme/tokens.d.ts +27 -0
- package/dist/types/client/types.d.ts +40 -0
- package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
- package/dist/types/client/utils/placement.d.ts +1 -0
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/json-render/catalog.d.ts +815 -0
- package/dist/types/json-render/charts/components.d.ts +54 -0
- package/dist/types/json-render/charts/definitions.d.ts +103 -0
- package/dist/types/json-render/charts/extra-components.d.ts +58 -0
- package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
- package/dist/types/json-render/renderer/index.d.ts +16 -0
- package/dist/types/json-render/schema.d.ts +46 -0
- package/dist/types/json-render/server.d.ts +55 -0
- package/dist/types/mcp/server.d.ts +22 -0
- package/dist/types/server/agent-context.d.ts +21 -0
- package/dist/types/server/artifact-paths.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +154 -0
- package/dist/types/server/canvas-provenance.d.ts +13 -0
- package/dist/types/server/canvas-schema.d.ts +49 -0
- package/dist/types/server/canvas-serialization.d.ts +25 -0
- package/dist/types/server/canvas-state.d.ts +174 -0
- package/dist/types/server/canvas-validation.d.ts +33 -0
- package/dist/types/server/chart-template.d.ts +29 -0
- package/dist/types/server/code-graph.d.ts +67 -0
- package/dist/types/server/context-cards.d.ts +24 -0
- package/dist/types/server/diagram-presets.d.ts +28 -0
- package/dist/types/server/ext-app-call-registry.d.ts +16 -0
- package/dist/types/server/ext-app-tool-result.d.ts +1 -0
- package/dist/types/server/file-watcher.d.ts +16 -0
- package/dist/types/server/index.d.ts +243 -0
- package/dist/types/server/mcp-app-candidate.d.ts +25 -0
- package/dist/types/server/mcp-app-host.d.ts +65 -0
- package/dist/types/server/mcp-app-runtime.d.ts +47 -0
- package/dist/types/server/mutation-history.d.ts +105 -0
- package/dist/types/server/placement.d.ts +37 -0
- package/dist/types/server/server.d.ts +103 -0
- package/dist/types/server/spatial-analysis.d.ts +87 -0
- package/dist/types/server/trace-manager.d.ts +48 -0
- package/dist/types/server/web-artifacts.d.ts +50 -0
- package/dist/types/server/webpage-node.d.ts +25 -0
- package/dist/types/shared/auto-arrange.d.ts +29 -0
- package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
- package/dist/types/shared/placement.d.ts +26 -0
- package/dist/types/shared/semantic-attention.d.ts +97 -0
- package/package.json +109 -0
- package/skills/data-analysis/SKILL.md +324 -0
- package/skills/doc-coauthoring/SKILL.md +375 -0
- package/skills/frontend-design/SKILL.md +45 -0
- package/skills/json-render-codegen/SKILL.md +112 -0
- package/skills/json-render-core/SKILL.md +265 -0
- package/skills/json-render-ink/SKILL.md +273 -0
- package/skills/json-render-mcp/SKILL.md +132 -0
- package/skills/json-render-react/SKILL.md +264 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/playwright-cli/SKILL.md +67 -0
- package/skills/pmx-canvas/SKILL.md +668 -0
- package/skills/pmx-canvas/evals/evals.json +186 -0
- package/skills/pmx-canvas-testing/SKILL.md +78 -0
- package/skills/published-consumer-e2e/SKILL.md +43 -0
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
- package/skills/web-artifacts-builder/SKILL.md +80 -0
- package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
- package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/web-design-guidelines/SKILL.md +39 -0
- package/src/cli/agent.ts +2144 -0
- package/src/cli/index.ts +622 -0
- package/src/cli/watch.ts +88 -0
- package/src/client/App.tsx +507 -0
- package/src/client/canvas/AttentionHistory.tsx +81 -0
- package/src/client/canvas/AttentionToast.tsx +19 -0
- package/src/client/canvas/CanvasNode.tsx +363 -0
- package/src/client/canvas/CanvasViewport.tsx +590 -0
- package/src/client/canvas/CommandPalette.tsx +302 -0
- package/src/client/canvas/ContextMenu.tsx +601 -0
- package/src/client/canvas/ContextPinBar.tsx +25 -0
- package/src/client/canvas/ContextPinHud.tsx +22 -0
- package/src/client/canvas/DockedNode.tsx +66 -0
- package/src/client/canvas/EdgeLayer.tsx +280 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
- package/src/client/canvas/FocusFieldLayer.tsx +107 -0
- package/src/client/canvas/Minimap.tsx +301 -0
- package/src/client/canvas/SelectionBar.tsx +69 -0
- package/src/client/canvas/ShortcutOverlay.tsx +69 -0
- package/src/client/canvas/SnapshotPanel.tsx +236 -0
- package/src/client/canvas/snap-guides.ts +170 -0
- package/src/client/canvas/use-node-drag.ts +51 -0
- package/src/client/canvas/use-node-resize.ts +59 -0
- package/src/client/canvas/use-pan-zoom.ts +191 -0
- package/src/client/ext-app/bridge.ts +542 -0
- package/src/client/icons.tsx +424 -0
- package/src/client/index.tsx +7 -0
- package/src/client/nodes/ContextNode.tsx +412 -0
- package/src/client/nodes/ExtAppFrame.tsx +509 -0
- package/src/client/nodes/FileNode.tsx +256 -0
- package/src/client/nodes/GroupNode.tsx +39 -0
- package/src/client/nodes/ImageNode.tsx +160 -0
- package/src/client/nodes/InlineFormatBar.tsx +169 -0
- package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
- package/src/client/nodes/LedgerNode.tsx +37 -0
- package/src/client/nodes/MarkdownNode.tsx +359 -0
- package/src/client/nodes/McpAppNode.tsx +85 -0
- package/src/client/nodes/MdFormatBar.tsx +109 -0
- package/src/client/nodes/PromptNode.tsx +597 -0
- package/src/client/nodes/ResponseNode.tsx +153 -0
- package/src/client/nodes/StatusNode.tsx +84 -0
- package/src/client/nodes/StatusSummary.tsx +38 -0
- package/src/client/nodes/TraceNode.tsx +120 -0
- package/src/client/nodes/WebpageNode.tsx +288 -0
- package/src/client/nodes/image-warnings.ts +95 -0
- package/src/client/nodes/inline-editor-commands.ts +37 -0
- package/src/client/nodes/md-format.ts +206 -0
- package/src/client/state/attention-bridge.ts +328 -0
- package/src/client/state/attention-store.ts +73 -0
- package/src/client/state/canvas-store.ts +631 -0
- package/src/client/state/intent-bridge.ts +315 -0
- package/src/client/state/sse-bridge.ts +965 -0
- package/src/client/theme/global.css +3173 -0
- package/src/client/theme/tokens.ts +72 -0
- package/src/client/types-shims.d.ts +5 -0
- package/src/client/types.ts +81 -0
- package/src/client/utils/ext-app-tool-result.ts +4 -0
- package/src/client/utils/placement.ts +4 -0
- package/src/client/utils/platform.ts +2 -0
- package/src/json-render/catalog.ts +256 -0
- package/src/json-render/charts/components.tsx +198 -0
- package/src/json-render/charts/definitions.ts +81 -0
- package/src/json-render/charts/extra-components.tsx +267 -0
- package/src/json-render/charts/extra-definitions.ts +145 -0
- package/src/json-render/renderer/index.css +174 -0
- package/src/json-render/renderer/index.tsx +86 -0
- package/src/json-render/schema.ts +62 -0
- package/src/json-render/server.ts +597 -0
- package/src/mcp/server.ts +1377 -0
- package/src/server/agent-context.ts +242 -0
- package/src/server/artifact-paths.ts +17 -0
- package/src/server/canvas-operations.ts +1279 -0
- package/src/server/canvas-provenance.ts +243 -0
- package/src/server/canvas-schema.ts +432 -0
- package/src/server/canvas-serialization.ts +95 -0
- package/src/server/canvas-state.ts +1134 -0
- package/src/server/canvas-validation.ts +114 -0
- package/src/server/chart-template.ts +449 -0
- package/src/server/code-graph.ts +370 -0
- package/src/server/context-cards.ts +31 -0
- package/src/server/diagram-presets.ts +71 -0
- package/src/server/ext-app-call-registry.ts +77 -0
- package/src/server/ext-app-tool-result.ts +4 -0
- package/src/server/file-watcher.ts +121 -0
- package/src/server/index.ts +647 -0
- package/src/server/mcp-app-candidate.ts +174 -0
- package/src/server/mcp-app-host.ts +814 -0
- package/src/server/mcp-app-runtime.ts +459 -0
- package/src/server/mutation-history.ts +350 -0
- package/src/server/placement.ts +125 -0
- package/src/server/server.ts +3846 -0
- package/src/server/spatial-analysis.ts +356 -0
- package/src/server/trace-manager.ts +333 -0
- package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
- package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
- package/src/server/web-artifacts.ts +442 -0
- package/src/server/webpage-node.ts +328 -0
- package/src/shared/auto-arrange.ts +439 -0
- package/src/shared/ext-app-tool-result.ts +76 -0
- package/src/shared/placement.ts +81 -0
- 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, '&')
|
|
445
|
+
.replace(/</g, '<')
|
|
446
|
+
.replace(/>/g, '>')
|
|
447
|
+
.replace(/"/g, '"')
|
|
448
|
+
.replace(/'/g, ''');
|
|
449
|
+
}
|