pmx-canvas 0.4.6 → 0.4.7
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 +62 -0
- package/Readme.md +56 -33
- package/dist/canvas/global.css +101 -3
- package/dist/canvas/index.js +87 -83
- package/dist/types/cli/shared.d.ts +13 -1
- package/dist/types/client/canvas/CanvasNode.d.ts +11 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
- package/dist/types/client/nodes/AxStepControls.d.ts +4 -0
- package/dist/types/client/nodes/FileNode.d.ts +14 -0
- package/dist/types/client/state/canvas-store.d.ts +10 -0
- package/dist/types/client/state/intent-bridge.d.ts +9 -0
- package/dist/types/server/ax-flow-loop.d.ts +5 -0
- package/dist/types/server/ax-interaction.d.ts +21 -2
- package/dist/types/server/canvas-operations.d.ts +5 -0
- package/dist/types/server/canvas-state.d.ts +8 -0
- package/dist/types/server/canvas-validation.d.ts +15 -21
- package/dist/types/server/file-content.d.ts +46 -0
- package/dist/types/server/html-primitives.d.ts +13 -1
- package/dist/types/server/operations/ops/ax-work.d.ts +1 -0
- package/dist/types/server/server.d.ts +1 -0
- package/dist/types/server/web-artifacts.d.ts +14 -0
- package/dist/types/shared/ax-flow.d.ts +51 -0
- package/dist/types/shared/node-sizes.d.ts +33 -0
- package/docs/cli.md +19 -2
- package/docs/environment.md +1 -0
- package/docs/http-api.md +17 -0
- package/docs/mcp.md +12 -0
- package/docs/node-types.md +45 -1
- package/docs/screenshot.png +0 -0
- package/package.json +2 -2
- package/skills/pmx-canvas/SKILL.md +68 -7
- package/skills/pmx-canvas/evals/evals.json +54 -3
- package/skills/pmx-canvas/references/full-reference.md +11 -6
- package/src/cli/commands/history.ts +1 -1
- package/src/cli/commands/nodes.ts +2 -2
- package/src/cli/commands/query.ts +1 -1
- package/src/cli/commands/view.ts +1 -1
- package/src/cli/index.ts +6 -0
- package/src/cli/shared.ts +34 -5
- package/src/client/App.tsx +28 -15
- package/src/client/canvas/CanvasNode.tsx +54 -5
- package/src/client/canvas/EdgeLayer.tsx +44 -24
- package/src/client/canvas/ExpandedNodeOverlay.tsx +11 -0
- package/src/client/canvas/auto-fit.ts +10 -1
- package/src/client/canvas/use-pan-zoom.ts +16 -1
- package/src/client/nodes/AxStepControls.tsx +226 -0
- package/src/client/nodes/FileNode.tsx +249 -51
- package/src/client/state/canvas-store.ts +19 -0
- package/src/client/state/intent-bridge.ts +32 -0
- package/src/client/state/sse-bridge.ts +4 -1
- package/src/client/theme/global.css +101 -3
- package/src/server/ax-flow-loop.ts +114 -0
- package/src/server/ax-interaction.ts +299 -4
- package/src/server/canvas-operations.ts +44 -17
- package/src/server/canvas-state.ts +20 -10
- package/src/server/canvas-validation.ts +50 -41
- package/src/server/demo-state.json +4605 -523
- package/src/server/demo.ts +24 -0
- package/src/server/file-content.ts +152 -0
- package/src/server/file-watcher.ts +7 -10
- package/src/server/html-primitives.ts +636 -0
- package/src/server/operations/ops/ax-work.ts +5 -0
- package/src/server/operations/ops/nodes.ts +15 -1
- package/src/server/operations/ops/viewport.ts +7 -1
- package/src/server/server.ts +106 -5
- package/src/server/web-artifacts.ts +109 -0
- package/src/shared/ax-flow.ts +102 -0
- package/src/shared/node-sizes.ts +52 -0
- package/dist/types/server/chart-template.d.ts +0 -29
- package/src/server/chart-template.ts +0 -448
|
@@ -1,448 +0,0 @@
|
|
|
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(join(extAppsPackageDir, 'dist', 'src', 'app-with-deps.js'), 'utf-8');
|
|
19
|
-
const appBindingMatch = extAppsRuntimeSource.match(/([A-Za-z_$][\w$]*) as App/);
|
|
20
|
-
const transportBindingMatch = extAppsRuntimeSource.match(/([A-Za-z_$][\w$]*) as PostMessageTransport/);
|
|
21
|
-
|
|
22
|
-
if (!appBindingMatch || !transportBindingMatch) {
|
|
23
|
-
throw new Error(
|
|
24
|
-
'Failed to locate App or PostMessageTransport export bindings in @modelcontextprotocol/ext-apps runtime',
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const extAppsBootstrapSource = `${extAppsRuntimeSource}
|
|
29
|
-
const App = ${appBindingMatch[1]};
|
|
30
|
-
const PostMessageTransport = ${transportBindingMatch[1]};`;
|
|
31
|
-
|
|
32
|
-
export interface ChartDataset {
|
|
33
|
-
label: string;
|
|
34
|
-
values: number[];
|
|
35
|
-
color?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ChartConfig {
|
|
39
|
-
title: string;
|
|
40
|
-
chartType: 'bar' | 'line' | 'pie' | 'scatter' | 'doughnut' | 'radar';
|
|
41
|
-
labels: string[];
|
|
42
|
-
datasets: ChartDataset[];
|
|
43
|
-
xAxisLabel?: string;
|
|
44
|
-
yAxisLabel?: string;
|
|
45
|
-
stacked?: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const PALETTE = ['#46b6ff', '#2fd07f', '#f4c542', '#ff6a7f', '#e896ff', '#ff9f40', '#a7b2c8'];
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Map our simplified config to a Chart.js configuration object.
|
|
52
|
-
*/
|
|
53
|
-
function buildChartJsConfig(config: ChartConfig): Record<string, unknown> {
|
|
54
|
-
const type = config.chartType === 'radar' ? 'radar' : config.chartType;
|
|
55
|
-
const isPolar = type === 'pie' || type === 'doughnut' || type === 'radar';
|
|
56
|
-
|
|
57
|
-
const datasets = config.datasets.map((ds, i) => {
|
|
58
|
-
const color = ds.color || PALETTE[i % PALETTE.length];
|
|
59
|
-
const base: Record<string, unknown> = {
|
|
60
|
-
label: ds.label,
|
|
61
|
-
data: ds.values,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
if (isPolar) {
|
|
65
|
-
// Pie/doughnut/radar: per-segment colors
|
|
66
|
-
base.backgroundColor = config.labels.map((_, j) => `${PALETTE[j % PALETTE.length]}cc`);
|
|
67
|
-
base.borderColor = config.labels.map((_, j) => PALETTE[j % PALETTE.length]);
|
|
68
|
-
base.borderWidth = 1;
|
|
69
|
-
} else {
|
|
70
|
-
base.backgroundColor = `${color}99`;
|
|
71
|
-
base.borderColor = color;
|
|
72
|
-
base.borderWidth = 2;
|
|
73
|
-
if (type === 'line') {
|
|
74
|
-
base.tension = 0.3;
|
|
75
|
-
base.fill = false;
|
|
76
|
-
base.pointRadius = 4;
|
|
77
|
-
base.pointHoverRadius = 6;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return base;
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const scales: Record<string, unknown> = {};
|
|
85
|
-
if (!isPolar) {
|
|
86
|
-
scales.x = {
|
|
87
|
-
grid: { color: 'rgba(255,255,255,0.06)' },
|
|
88
|
-
ticks: { color: '#7b8da8', font: { size: 11 } },
|
|
89
|
-
...(config.xAxisLabel && {
|
|
90
|
-
title: { display: true, text: config.xAxisLabel, color: '#a7b2c8', font: { size: 12 } },
|
|
91
|
-
}),
|
|
92
|
-
...(config.stacked && { stacked: true }),
|
|
93
|
-
};
|
|
94
|
-
scales.y = {
|
|
95
|
-
grid: { color: 'rgba(255,255,255,0.06)' },
|
|
96
|
-
ticks: { color: '#7b8da8', font: { size: 11 } },
|
|
97
|
-
...(config.yAxisLabel && {
|
|
98
|
-
title: { display: true, text: config.yAxisLabel, color: '#a7b2c8', font: { size: 12 } },
|
|
99
|
-
}),
|
|
100
|
-
...(config.stacked && { stacked: true }),
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
type,
|
|
106
|
-
data: { labels: config.labels, datasets },
|
|
107
|
-
options: {
|
|
108
|
-
responsive: true,
|
|
109
|
-
maintainAspectRatio: false,
|
|
110
|
-
animation: { duration: 400 },
|
|
111
|
-
plugins: {
|
|
112
|
-
legend: {
|
|
113
|
-
display: datasets.length > 1 || isPolar,
|
|
114
|
-
labels: { color: '#a7b2c8', font: { size: 11 }, padding: 12 },
|
|
115
|
-
},
|
|
116
|
-
tooltip: {
|
|
117
|
-
backgroundColor: 'rgba(26,29,35,0.95)',
|
|
118
|
-
titleColor: '#e0e4ec',
|
|
119
|
-
bodyColor: '#a7b2c8',
|
|
120
|
-
borderColor: 'rgba(255,255,255,0.1)',
|
|
121
|
-
borderWidth: 1,
|
|
122
|
-
padding: 10,
|
|
123
|
-
cornerRadius: 6,
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
...(!isPolar && { scales }),
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Generate a self-contained HTML document that renders a Chart.js chart
|
|
133
|
-
* and optionally connects to the host via the ext-app App SDK.
|
|
134
|
-
*/
|
|
135
|
-
export function generateChartHtml(config: ChartConfig): string {
|
|
136
|
-
const chartJsConfig = buildChartJsConfig(config);
|
|
137
|
-
const configJson = JSON.stringify(chartJsConfig);
|
|
138
|
-
const chartConfigJson = JSON.stringify(config);
|
|
139
|
-
const titleEscaped = escapeHtml(config.title);
|
|
140
|
-
|
|
141
|
-
// Chart type buttons — highlight the active one
|
|
142
|
-
const chartTypes: Array<{ key: string; label: string }> = [
|
|
143
|
-
{ key: 'bar', label: 'Bar' },
|
|
144
|
-
{ key: 'line', label: 'Line' },
|
|
145
|
-
{ key: 'pie', label: 'Pie' },
|
|
146
|
-
{ key: 'scatter', label: 'Scatter' },
|
|
147
|
-
{ key: 'doughnut', label: 'Donut' },
|
|
148
|
-
{ key: 'radar', label: 'Radar' },
|
|
149
|
-
];
|
|
150
|
-
const typeButtons = chartTypes
|
|
151
|
-
.map(
|
|
152
|
-
(t) =>
|
|
153
|
-
`<button class="type-btn${t.key === config.chartType ? ' active' : ''}" data-type="${t.key}">${t.label}</button>`,
|
|
154
|
-
)
|
|
155
|
-
.join('\n ');
|
|
156
|
-
|
|
157
|
-
return `<!DOCTYPE html>
|
|
158
|
-
<html lang="en">
|
|
159
|
-
<head>
|
|
160
|
-
<meta charset="utf-8">
|
|
161
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
162
|
-
<style>
|
|
163
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
164
|
-
html {
|
|
165
|
-
width: 100%;
|
|
166
|
-
height: 100%;
|
|
167
|
-
}
|
|
168
|
-
body {
|
|
169
|
-
background: #1a1d23;
|
|
170
|
-
color: #e0e4ec;
|
|
171
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
172
|
-
overflow: hidden;
|
|
173
|
-
width: 100%;
|
|
174
|
-
height: 100%;
|
|
175
|
-
}
|
|
176
|
-
#container {
|
|
177
|
-
width: 100%;
|
|
178
|
-
height: 100%;
|
|
179
|
-
display: flex;
|
|
180
|
-
flex-direction: column;
|
|
181
|
-
min-width: 0;
|
|
182
|
-
min-height: 0;
|
|
183
|
-
}
|
|
184
|
-
.toolbar {
|
|
185
|
-
display: flex;
|
|
186
|
-
align-items: center;
|
|
187
|
-
gap: 4px;
|
|
188
|
-
padding: 8px 12px;
|
|
189
|
-
background: rgba(255,255,255,0.03);
|
|
190
|
-
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
191
|
-
flex-shrink: 0;
|
|
192
|
-
}
|
|
193
|
-
.chart-title {
|
|
194
|
-
font-size: 13px;
|
|
195
|
-
font-weight: 600;
|
|
196
|
-
color: #e0e4ec;
|
|
197
|
-
margin-right: auto;
|
|
198
|
-
white-space: nowrap;
|
|
199
|
-
overflow: hidden;
|
|
200
|
-
text-overflow: ellipsis;
|
|
201
|
-
}
|
|
202
|
-
.type-btn {
|
|
203
|
-
padding: 3px 8px;
|
|
204
|
-
border: 1px solid rgba(255,255,255,0.1);
|
|
205
|
-
border-radius: 4px;
|
|
206
|
-
background: transparent;
|
|
207
|
-
color: #7b8da8;
|
|
208
|
-
font-size: 11px;
|
|
209
|
-
cursor: pointer;
|
|
210
|
-
transition: all 0.15s;
|
|
211
|
-
white-space: nowrap;
|
|
212
|
-
}
|
|
213
|
-
.type-btn:hover { background: rgba(255,255,255,0.05); color: #e0e4ec; }
|
|
214
|
-
.type-btn.active {
|
|
215
|
-
background: rgba(70,182,255,0.15);
|
|
216
|
-
border-color: rgba(70,182,255,0.3);
|
|
217
|
-
color: #46b6ff;
|
|
218
|
-
}
|
|
219
|
-
.chart-area {
|
|
220
|
-
flex: 1;
|
|
221
|
-
padding: 12px;
|
|
222
|
-
position: relative;
|
|
223
|
-
min-width: 0;
|
|
224
|
-
min-height: 0;
|
|
225
|
-
}
|
|
226
|
-
#chart {
|
|
227
|
-
display: block;
|
|
228
|
-
width: 100% !important;
|
|
229
|
-
height: 100% !important;
|
|
230
|
-
}
|
|
231
|
-
.empty-state {
|
|
232
|
-
display: flex;
|
|
233
|
-
align-items: center;
|
|
234
|
-
justify-content: center;
|
|
235
|
-
height: 100%;
|
|
236
|
-
color: #7b8da8;
|
|
237
|
-
font-size: 13px;
|
|
238
|
-
}
|
|
239
|
-
</style>
|
|
240
|
-
</head>
|
|
241
|
-
<body>
|
|
242
|
-
<div id="container">
|
|
243
|
-
<div class="toolbar">
|
|
244
|
-
<span class="chart-title">${titleEscaped}</span>
|
|
245
|
-
${typeButtons}
|
|
246
|
-
</div>
|
|
247
|
-
<div class="chart-area">
|
|
248
|
-
<canvas id="chart"></canvas>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
|
|
252
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
|
253
|
-
<script>
|
|
254
|
-
// Inline chart configuration — renders immediately without bridge
|
|
255
|
-
var CHART_CONFIG = ${configJson};
|
|
256
|
-
var CHART_META = ${chartConfigJson};
|
|
257
|
-
var chartInstance = null;
|
|
258
|
-
var chartResizeObserver = null;
|
|
259
|
-
var chartResizeRaf = null;
|
|
260
|
-
|
|
261
|
-
function getChartArea() {
|
|
262
|
-
return document.querySelector('.chart-area');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function getChartSize() {
|
|
266
|
-
var area = getChartArea();
|
|
267
|
-
if (!area) return null;
|
|
268
|
-
var rect = area.getBoundingClientRect();
|
|
269
|
-
if (!rect || rect.width < 24 || rect.height < 24) return null;
|
|
270
|
-
return rect;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function scheduleChartResize() {
|
|
274
|
-
if (chartResizeRaf) cancelAnimationFrame(chartResizeRaf);
|
|
275
|
-
chartResizeRaf = requestAnimationFrame(function() {
|
|
276
|
-
chartResizeRaf = null;
|
|
277
|
-
if (chartInstance) chartInstance.resize();
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function ensureChartResizeTracking() {
|
|
282
|
-
var area = getChartArea();
|
|
283
|
-
if (!area || chartResizeObserver || typeof ResizeObserver !== 'function') return;
|
|
284
|
-
chartResizeObserver = new ResizeObserver(function() {
|
|
285
|
-
if (chartInstance) {
|
|
286
|
-
scheduleChartResize();
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (getChartSize()) {
|
|
290
|
-
renderChart(CHART_CONFIG);
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
chartResizeObserver.observe(area);
|
|
294
|
-
window.addEventListener('resize', scheduleChartResize);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function renderChart(cfg) {
|
|
298
|
-
var size = getChartSize();
|
|
299
|
-
if (!size) return false;
|
|
300
|
-
if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
|
|
301
|
-
var canvas = document.getElementById('chart');
|
|
302
|
-
if (!canvas) return;
|
|
303
|
-
canvas.width = Math.max(1, Math.floor(size.width));
|
|
304
|
-
canvas.height = Math.max(1, Math.floor(size.height));
|
|
305
|
-
chartInstance = new Chart(canvas.getContext('2d'), JSON.parse(JSON.stringify(cfg)));
|
|
306
|
-
scheduleChartResize();
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function renderWhenReady(cfg, attempt) {
|
|
311
|
-
if (renderChart(cfg)) return;
|
|
312
|
-
if ((attempt || 0) >= 20) return;
|
|
313
|
-
requestAnimationFrame(function() {
|
|
314
|
-
renderWhenReady(cfg, (attempt || 0) + 1);
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function switchType(newType) {
|
|
319
|
-
// Rebuild config for the new chart type using stored metadata
|
|
320
|
-
var meta = JSON.parse(JSON.stringify(CHART_META));
|
|
321
|
-
meta.chartType = newType;
|
|
322
|
-
CHART_META = meta;
|
|
323
|
-
|
|
324
|
-
// Update button states
|
|
325
|
-
document.querySelectorAll('.type-btn').forEach(function(btn) {
|
|
326
|
-
btn.classList.toggle('active', btn.dataset.type === newType);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// Post message to request new config from parent (or rebuild locally)
|
|
330
|
-
var isPolar = (newType === 'pie' || newType === 'doughnut' || newType === 'radar');
|
|
331
|
-
var palette = ${JSON.stringify(PALETTE)};
|
|
332
|
-
|
|
333
|
-
var datasets = meta.datasets.map(function(ds, i) {
|
|
334
|
-
var color = ds.color || palette[i % palette.length];
|
|
335
|
-
var base = { label: ds.label, data: ds.values };
|
|
336
|
-
if (isPolar) {
|
|
337
|
-
base.backgroundColor = meta.labels.map(function(_, j) { return palette[j % palette.length] + 'cc'; });
|
|
338
|
-
base.borderColor = meta.labels.map(function(_, j) { return palette[j % palette.length]; });
|
|
339
|
-
base.borderWidth = 1;
|
|
340
|
-
} else {
|
|
341
|
-
base.backgroundColor = color + '99';
|
|
342
|
-
base.borderColor = color;
|
|
343
|
-
base.borderWidth = 2;
|
|
344
|
-
if (newType === 'line') { base.tension = 0.3; base.fill = false; base.pointRadius = 4; }
|
|
345
|
-
}
|
|
346
|
-
return base;
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
var scales = {};
|
|
350
|
-
if (!isPolar) {
|
|
351
|
-
scales.x = {
|
|
352
|
-
grid: { color: 'rgba(255,255,255,0.06)' },
|
|
353
|
-
ticks: { color: '#7b8da8', font: { size: 11 } },
|
|
354
|
-
stacked: !!meta.stacked
|
|
355
|
-
};
|
|
356
|
-
scales.y = {
|
|
357
|
-
grid: { color: 'rgba(255,255,255,0.06)' },
|
|
358
|
-
ticks: { color: '#7b8da8', font: { size: 11 } },
|
|
359
|
-
stacked: !!meta.stacked
|
|
360
|
-
};
|
|
361
|
-
if (meta.xAxisLabel) scales.x.title = { display: true, text: meta.xAxisLabel, color: '#a7b2c8' };
|
|
362
|
-
if (meta.yAxisLabel) scales.y.title = { display: true, text: meta.yAxisLabel, color: '#a7b2c8' };
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
var newCfg = {
|
|
366
|
-
type: newType,
|
|
367
|
-
data: { labels: meta.labels, datasets: datasets },
|
|
368
|
-
options: {
|
|
369
|
-
responsive: true,
|
|
370
|
-
maintainAspectRatio: false,
|
|
371
|
-
animation: { duration: 300 },
|
|
372
|
-
plugins: {
|
|
373
|
-
legend: {
|
|
374
|
-
display: datasets.length > 1 || isPolar,
|
|
375
|
-
labels: { color: '#a7b2c8', font: { size: 11 }, padding: 12 }
|
|
376
|
-
},
|
|
377
|
-
tooltip: {
|
|
378
|
-
backgroundColor: 'rgba(26,29,35,0.95)',
|
|
379
|
-
titleColor: '#e0e4ec',
|
|
380
|
-
bodyColor: '#a7b2c8',
|
|
381
|
-
borderColor: 'rgba(255,255,255,0.1)',
|
|
382
|
-
borderWidth: 1, padding: 10, cornerRadius: 6
|
|
383
|
-
}
|
|
384
|
-
},
|
|
385
|
-
scales: isPolar ? undefined : scales
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
CHART_CONFIG = newCfg;
|
|
389
|
-
renderWhenReady(newCfg, 0);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
window.__PMX_CHART_BRIDGE__ = {
|
|
393
|
-
updateChartMeta: function(nextMeta) {
|
|
394
|
-
if (!nextMeta || typeof nextMeta !== 'object') return;
|
|
395
|
-
CHART_META = nextMeta;
|
|
396
|
-
switchType(CHART_META.chartType || 'bar');
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
// Toolbar click handler
|
|
401
|
-
document.querySelector('.toolbar').addEventListener('click', function(e) {
|
|
402
|
-
var btn = e.target.closest('.type-btn');
|
|
403
|
-
if (btn && btn.dataset.type) switchType(btn.dataset.type);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// Initial render
|
|
407
|
-
window.addEventListener('load', function() {
|
|
408
|
-
ensureChartResizeTracking();
|
|
409
|
-
if (CHART_META.datasets.length === 0 || CHART_META.labels.length === 0) {
|
|
410
|
-
document.querySelector('.chart-area').innerHTML =
|
|
411
|
-
'<div class="empty-state">No data to display</div>';
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
renderWhenReady(CHART_CONFIG, 0);
|
|
415
|
-
});
|
|
416
|
-
</script>
|
|
417
|
-
<script type="module">
|
|
418
|
-
${extAppsBootstrapSource}
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
if (!App) {
|
|
422
|
-
throw new Error('AppBridge SDK unavailable');
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const bridge = window.__PMX_CHART_BRIDGE__;
|
|
426
|
-
const app = new App({ name: 'PMX Chart', version: '1.0.0' }, {});
|
|
427
|
-
app.ontoolinput = function(params) {
|
|
428
|
-
bridge?.updateChartMeta?.(params?.arguments);
|
|
429
|
-
};
|
|
430
|
-
await app.connect(new PostMessageTransport(window.parent, window.parent));
|
|
431
|
-
} catch (error) {
|
|
432
|
-
// Bridge connection optional — chart already rendered from inline data
|
|
433
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
434
|
-
console.debug('[pmx-chart] AppBridge not available:', message);
|
|
435
|
-
}
|
|
436
|
-
</script>
|
|
437
|
-
</body>
|
|
438
|
-
</html>`;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function escapeHtml(str: string): string {
|
|
442
|
-
return str
|
|
443
|
-
.replace(/&/g, '&')
|
|
444
|
-
.replace(/</g, '<')
|
|
445
|
-
.replace(/>/g, '>')
|
|
446
|
-
.replace(/"/g, '"')
|
|
447
|
-
.replace(/'/g, ''');
|
|
448
|
-
}
|