pmx-canvas 0.1.19 → 0.1.21
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 +159 -0
- package/Readme.md +19 -6
- package/dist/canvas/global.css +123 -2
- package/dist/canvas/index.js +103 -68
- package/dist/json-render/index.js +109 -109
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/icons.d.ts +2 -0
- package/dist/types/client/nodes/HtmlNode.d.ts +12 -1
- package/dist/types/client/state/canvas-store.d.ts +2 -0
- package/dist/types/client/types.d.ts +3 -2
- package/dist/types/json-render/charts/components.d.ts +5 -1
- package/dist/types/json-render/renderer/index.d.ts +1 -0
- package/dist/types/json-render/server.d.ts +1 -0
- package/dist/types/mcp/canvas-access.d.ts +3 -0
- package/dist/types/server/canvas-operations.d.ts +4 -0
- package/dist/types/server/canvas-schema.d.ts +19 -3
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +6 -2
- package/dist/types/server/html-node-summary.d.ts +2 -0
- package/dist/types/server/html-primitives.d.ts +42 -0
- package/dist/types/server/index.d.ts +26 -0
- package/docs/cli.md +4 -1
- package/docs/http-api.md +11 -1
- package/docs/mcp.md +10 -4
- package/docs/node-types.md +54 -4
- package/docs/screenshot.png +0 -0
- package/docs/sdk.md +12 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +17 -3
- package/skills/pmx-canvas/references/html-primitives.md +132 -0
- package/src/cli/agent.ts +159 -5
- package/src/cli/index.ts +1 -1
- package/src/client/App.tsx +21 -2
- package/src/client/canvas/AnnotationLayer.tsx +33 -12
- package/src/client/canvas/CanvasViewport.tsx +88 -7
- package/src/client/canvas/CommandPalette.tsx +2 -2
- package/src/client/canvas/ContextMenu.tsx +2 -2
- package/src/client/canvas/ExpandedNodeOverlay.tsx +112 -3
- package/src/client/canvas/auto-fit.ts +5 -1
- package/src/client/icons.tsx +13 -0
- package/src/client/nodes/HtmlNode.tsx +125 -13
- package/src/client/nodes/McpAppNode.tsx +12 -4
- package/src/client/state/canvas-store.ts +15 -5
- package/src/client/state/sse-bridge.ts +5 -4
- package/src/client/theme/global.css +123 -2
- package/src/client/types.ts +2 -1
- package/src/json-render/charts/components.tsx +41 -7
- package/src/json-render/charts/extra-components.tsx +13 -12
- package/src/json-render/renderer/index.tsx +1 -0
- package/src/json-render/server.ts +3 -1
- package/src/mcp/canvas-access.ts +54 -1
- package/src/mcp/server.ts +98 -28
- package/src/server/agent-context.ts +39 -0
- package/src/server/canvas-operations.ts +99 -38
- package/src/server/canvas-provenance.ts +8 -6
- package/src/server/canvas-schema.ts +94 -3
- package/src/server/canvas-serialization.ts +16 -4
- package/src/server/canvas-state.ts +9 -4
- package/src/server/demo-state.json +1143 -0
- package/src/server/demo.ts +25 -777
- package/src/server/html-node-summary.ts +141 -0
- package/src/server/html-primitives.ts +1300 -0
- package/src/server/index.ts +63 -3
- package/src/server/server.ts +154 -17
- package/src/server/spatial-analysis.ts +5 -3
|
@@ -173,10 +173,18 @@ export function addNode(node: CanvasNodeState): void {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
export function updateNode(id: string, patch: Partial<CanvasNodeState>): void {
|
|
176
|
+
updateNodeWithOptions(id, patch);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function updateNodeWithOptions(
|
|
180
|
+
id: string,
|
|
181
|
+
patch: Partial<CanvasNodeState>,
|
|
182
|
+
options: { skipGroupChildTranslation?: boolean } = {},
|
|
183
|
+
): void {
|
|
176
184
|
const existing = nodes.value.get(id);
|
|
177
185
|
if (!existing) return;
|
|
178
186
|
const next = new Map(nodes.value);
|
|
179
|
-
if (existing.type === 'group' && patch.position) {
|
|
187
|
+
if (existing.type === 'group' && patch.position && options.skipGroupChildTranslation !== true) {
|
|
180
188
|
const deltaX = patch.position.x - existing.position.x;
|
|
181
189
|
const deltaY = patch.position.y - existing.position.y;
|
|
182
190
|
if (deltaX !== 0 || deltaY !== 0) {
|
|
@@ -272,9 +280,11 @@ export function removeAnnotation(id: string): void {
|
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
export async function createAnnotationFromClient(input: {
|
|
283
|
+
type?: CanvasAnnotation['type'];
|
|
275
284
|
points: CanvasAnnotation['points'];
|
|
276
285
|
color: string;
|
|
277
286
|
width: number;
|
|
287
|
+
text?: string;
|
|
278
288
|
label?: string;
|
|
279
289
|
}): Promise<{ ok: boolean }> {
|
|
280
290
|
try {
|
|
@@ -748,10 +758,10 @@ export function autoArrange(): void {
|
|
|
748
758
|
updateNode(id, { position });
|
|
749
759
|
}
|
|
750
760
|
for (const [groupId, bounds] of result.groupBounds.entries()) {
|
|
751
|
-
|
|
761
|
+
updateNodeWithOptions(groupId, {
|
|
752
762
|
position: { x: bounds.x, y: bounds.y },
|
|
753
763
|
size: { width: bounds.width, height: bounds.height },
|
|
754
|
-
});
|
|
764
|
+
}, { skipGroupChildTranslation: true });
|
|
755
765
|
}
|
|
756
766
|
});
|
|
757
767
|
persistLayout();
|
|
@@ -766,10 +776,10 @@ export function forceDirectedArrange(): void {
|
|
|
766
776
|
updateNode(id, { position });
|
|
767
777
|
}
|
|
768
778
|
for (const [groupId, bounds] of result.groupBounds.entries()) {
|
|
769
|
-
|
|
779
|
+
updateNodeWithOptions(groupId, {
|
|
770
780
|
position: { x: bounds.x, y: bounds.y },
|
|
771
781
|
size: { width: bounds.width, height: bounds.height },
|
|
772
|
-
});
|
|
782
|
+
}, { skipGroupChildTranslation: true });
|
|
773
783
|
}
|
|
774
784
|
});
|
|
775
785
|
persistLayout();
|
|
@@ -291,9 +291,9 @@ function ensureLedgerNode(summary: Record<string, unknown>): void {
|
|
|
291
291
|
function applyCanvasTheme(theme: string): void {
|
|
292
292
|
const valid = theme === 'dark' || theme === 'light' || theme === 'high-contrast';
|
|
293
293
|
if (!valid || canvasTheme.value === theme) return;
|
|
294
|
-
canvasTheme.value = theme;
|
|
295
294
|
document.documentElement.setAttribute('data-theme', theme);
|
|
296
295
|
invalidateTokenCache();
|
|
296
|
+
canvasTheme.value = theme;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
function isCanvasNodeType(value: unknown): value is CanvasNodeState['type'] {
|
|
@@ -388,21 +388,22 @@ function parseCanvasEdge(raw: Record<string, unknown>): CanvasEdge | null {
|
|
|
388
388
|
|
|
389
389
|
function parseCanvasAnnotation(raw: Record<string, unknown>): CanvasAnnotation | null {
|
|
390
390
|
if (typeof raw.id !== 'string' || !raw.id) return null;
|
|
391
|
-
if (raw.type !== 'freehand') return null;
|
|
391
|
+
if (raw.type !== 'freehand' && raw.type !== 'text') return null;
|
|
392
392
|
if (!Array.isArray(raw.points)) return null;
|
|
393
393
|
const points = raw.points
|
|
394
394
|
.map((point) => parseCanvasPosition(point))
|
|
395
395
|
.filter((point): point is { x: number; y: number } => point !== null);
|
|
396
396
|
const bounds = parseCanvasRect(raw.bounds);
|
|
397
|
-
if (points.length < 2 || !bounds) return null;
|
|
397
|
+
if (points.length < (raw.type === 'text' ? 1 : 2) || !bounds) return null;
|
|
398
398
|
|
|
399
399
|
return {
|
|
400
400
|
id: raw.id,
|
|
401
|
-
type:
|
|
401
|
+
type: raw.type,
|
|
402
402
|
points,
|
|
403
403
|
bounds,
|
|
404
404
|
color: typeof raw.color === 'string' ? raw.color : '#f97316',
|
|
405
405
|
width: typeof raw.width === 'number' ? raw.width : 4,
|
|
406
|
+
...(typeof raw.text === 'string' ? { text: raw.text } : {}),
|
|
406
407
|
...(typeof raw.label === 'string' ? { label: raw.label } : {}),
|
|
407
408
|
createdAt: typeof raw.createdAt === 'string' ? raw.createdAt : '',
|
|
408
409
|
};
|
|
@@ -392,6 +392,19 @@ body,
|
|
|
392
392
|
margin: 0.4em 0;
|
|
393
393
|
color: var(--c-muted);
|
|
394
394
|
}
|
|
395
|
+
|
|
396
|
+
.canvas-node .node-body ul,
|
|
397
|
+
.canvas-node .node-body ol {
|
|
398
|
+
margin: 0.4em 0;
|
|
399
|
+
padding-left: 0.25em;
|
|
400
|
+
list-style-position: inside;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.canvas-node .node-body li {
|
|
404
|
+
margin: 0.2em 0;
|
|
405
|
+
padding-left: 0.15em;
|
|
406
|
+
}
|
|
407
|
+
|
|
395
408
|
.canvas-node .node-body a {
|
|
396
409
|
color: var(--c-accent);
|
|
397
410
|
text-decoration: none;
|
|
@@ -1481,14 +1494,14 @@ body,
|
|
|
1481
1494
|
height: 1px;
|
|
1482
1495
|
overflow: visible;
|
|
1483
1496
|
pointer-events: none;
|
|
1484
|
-
z-index:
|
|
1497
|
+
z-index: 9000;
|
|
1485
1498
|
}
|
|
1486
1499
|
|
|
1487
1500
|
.annotation-capture-layer {
|
|
1488
1501
|
position: absolute;
|
|
1489
1502
|
inset: 0;
|
|
1490
1503
|
z-index: 9996;
|
|
1491
|
-
pointer-events:
|
|
1504
|
+
pointer-events: auto;
|
|
1492
1505
|
background: color-mix(in srgb, var(--c-accent) 5%, transparent);
|
|
1493
1506
|
}
|
|
1494
1507
|
|
|
@@ -1496,6 +1509,26 @@ body,
|
|
|
1496
1509
|
background: color-mix(in srgb, var(--c-danger) 6%, transparent);
|
|
1497
1510
|
}
|
|
1498
1511
|
|
|
1512
|
+
.annotation-capture-layer.text {
|
|
1513
|
+
background: color-mix(in srgb, var(--c-annotation) 4%, transparent);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
.annotation-text-input {
|
|
1517
|
+
position: absolute;
|
|
1518
|
+
z-index: 9997;
|
|
1519
|
+
min-width: 120px;
|
|
1520
|
+
max-width: min(560px, calc(100% - 24px));
|
|
1521
|
+
padding: 2px 4px;
|
|
1522
|
+
border: 1px solid var(--c-annotation);
|
|
1523
|
+
border-radius: 4px;
|
|
1524
|
+
background: color-mix(in srgb, var(--c-bg) 72%, transparent);
|
|
1525
|
+
color: var(--c-annotation);
|
|
1526
|
+
font-family: var(--font);
|
|
1527
|
+
font-weight: 700;
|
|
1528
|
+
line-height: 1.15;
|
|
1529
|
+
outline: none;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1499
1532
|
/* ── Drop Zone (file drag-and-drop) ─────────────────────────── */
|
|
1500
1533
|
.drop-zone-overlay {
|
|
1501
1534
|
position: absolute;
|
|
@@ -2405,6 +2438,18 @@ body,
|
|
|
2405
2438
|
border-color: var(--c-muted);
|
|
2406
2439
|
}
|
|
2407
2440
|
|
|
2441
|
+
.expanded-action-btn.expanded-action-primary {
|
|
2442
|
+
background: var(--c-accent-12);
|
|
2443
|
+
border-color: var(--c-accent-30);
|
|
2444
|
+
color: var(--c-accent);
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
.expanded-action-btn.expanded-action-primary:hover {
|
|
2448
|
+
background: var(--c-accent-25);
|
|
2449
|
+
border-color: var(--c-accent);
|
|
2450
|
+
color: var(--c-text);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2408
2453
|
.expanded-action-btn.expanded-action-active {
|
|
2409
2454
|
background: var(--c-warn-12);
|
|
2410
2455
|
border-color: var(--c-warn-30);
|
|
@@ -2422,6 +2467,82 @@ body,
|
|
|
2422
2467
|
padding: 0 4px;
|
|
2423
2468
|
}
|
|
2424
2469
|
|
|
2470
|
+
.html-presentation-overlay {
|
|
2471
|
+
position: fixed;
|
|
2472
|
+
inset: 0;
|
|
2473
|
+
z-index: 10050;
|
|
2474
|
+
display: flex;
|
|
2475
|
+
flex-direction: column;
|
|
2476
|
+
gap: 14px;
|
|
2477
|
+
padding: clamp(12px, 2vw, 28px);
|
|
2478
|
+
background:
|
|
2479
|
+
radial-gradient(circle at top left, var(--c-accent-25), transparent 36rem),
|
|
2480
|
+
rgba(3, 7, 18, 0.96);
|
|
2481
|
+
color: var(--c-text);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
.html-presentation-toolbar {
|
|
2485
|
+
display: flex;
|
|
2486
|
+
align-items: center;
|
|
2487
|
+
justify-content: space-between;
|
|
2488
|
+
gap: 16px;
|
|
2489
|
+
flex-shrink: 0;
|
|
2490
|
+
padding: 10px 12px;
|
|
2491
|
+
border: 1px solid var(--c-line);
|
|
2492
|
+
border-radius: 16px;
|
|
2493
|
+
background: var(--c-panel-glass);
|
|
2494
|
+
box-shadow: 0 18px 50px var(--c-shadow-heavy);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
.html-presentation-kicker {
|
|
2498
|
+
color: var(--c-accent);
|
|
2499
|
+
font-size: 10px;
|
|
2500
|
+
font-weight: 800;
|
|
2501
|
+
letter-spacing: 0.14em;
|
|
2502
|
+
text-transform: uppercase;
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
.html-presentation-title {
|
|
2506
|
+
max-width: min(72vw, 900px);
|
|
2507
|
+
overflow: hidden;
|
|
2508
|
+
color: var(--c-text);
|
|
2509
|
+
font-size: 14px;
|
|
2510
|
+
font-weight: 700;
|
|
2511
|
+
text-overflow: ellipsis;
|
|
2512
|
+
white-space: nowrap;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
.html-presentation-exit {
|
|
2516
|
+
flex-shrink: 0;
|
|
2517
|
+
padding: 8px 12px;
|
|
2518
|
+
border: 1px solid var(--c-line);
|
|
2519
|
+
border-radius: 999px;
|
|
2520
|
+
background: var(--c-panel-soft);
|
|
2521
|
+
color: var(--c-text-soft);
|
|
2522
|
+
cursor: pointer;
|
|
2523
|
+
font: 600 12px/1 var(--font);
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
.html-presentation-exit:hover {
|
|
2527
|
+
border-color: var(--c-accent);
|
|
2528
|
+
color: var(--c-text);
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
.html-presentation-stage {
|
|
2532
|
+
flex: 1;
|
|
2533
|
+
min-height: 0;
|
|
2534
|
+
display: flex;
|
|
2535
|
+
border-radius: 22px;
|
|
2536
|
+
background: var(--c-bg);
|
|
2537
|
+
box-shadow: 0 24px 90px rgba(0, 0, 0, 0.55);
|
|
2538
|
+
overflow: hidden;
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
.html-node-frame-presentation {
|
|
2542
|
+
flex: 1;
|
|
2543
|
+
min-height: 0;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2425
2546
|
/* ── Context pin button on node title bar ────────────────────── */
|
|
2426
2547
|
.node-controls .ctx-pin-btn {
|
|
2427
2548
|
color: var(--c-muted);
|
package/src/client/types.ts
CHANGED
|
@@ -48,11 +48,12 @@ export interface CanvasAnnotationPoint {
|
|
|
48
48
|
|
|
49
49
|
export interface CanvasAnnotation {
|
|
50
50
|
id: string;
|
|
51
|
-
type: 'freehand';
|
|
51
|
+
type: 'freehand' | 'text';
|
|
52
52
|
points: CanvasAnnotationPoint[];
|
|
53
53
|
bounds: { x: number; y: number; width: number; height: number };
|
|
54
54
|
color: string;
|
|
55
55
|
width: number;
|
|
56
|
+
text?: string;
|
|
56
57
|
label?: string;
|
|
57
58
|
createdAt: string;
|
|
58
59
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* a responsive chart inside a styled container.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type
|
|
11
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
12
12
|
import type { BaseComponentProps } from '@json-render/react';
|
|
13
13
|
import {
|
|
14
14
|
BarChart as RechartsBarChart,
|
|
@@ -106,6 +106,40 @@ export const polarChartMargin = { top: 18, right: 40, bottom: 30, left: 40 };
|
|
|
106
106
|
export const axisTickMargin = 8;
|
|
107
107
|
export const legendMargin = { top: 10 };
|
|
108
108
|
|
|
109
|
+
export function useChartFrameHeight(explicitHeight: number | null | undefined, fallbackHeight = 300) {
|
|
110
|
+
const frameRef = useRef<HTMLDivElement>(null);
|
|
111
|
+
const [autoHeight, setAutoHeight] = useState(fallbackHeight);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const frame = frameRef.current;
|
|
115
|
+
if (!frame) return;
|
|
116
|
+
|
|
117
|
+
const updateHeight = () => {
|
|
118
|
+
const rect = frame.getBoundingClientRect();
|
|
119
|
+
const doc = document.documentElement;
|
|
120
|
+
const currentHeight = frame.getBoundingClientRect().height;
|
|
121
|
+
const overflow = Math.max(0, doc.scrollHeight - doc.clientHeight);
|
|
122
|
+
const available = overflow > 0 ? currentHeight - overflow : window.innerHeight - rect.top - 24;
|
|
123
|
+
setAutoHeight(Math.max(220, Math.round(available)));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
updateHeight();
|
|
127
|
+
const observer = new ResizeObserver(updateHeight);
|
|
128
|
+
observer.observe(document.documentElement);
|
|
129
|
+
observer.observe(frame);
|
|
130
|
+
window.addEventListener('resize', updateHeight);
|
|
131
|
+
return () => {
|
|
132
|
+
observer.disconnect();
|
|
133
|
+
window.removeEventListener('resize', updateHeight);
|
|
134
|
+
};
|
|
135
|
+
}, [explicitHeight]);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
frameRef,
|
|
139
|
+
height: typeof explicitHeight === 'number' ? Math.min(explicitHeight, autoHeight) : autoHeight,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
109
143
|
/** Shared wrapper for cartesian charts (Line + Bar). */
|
|
110
144
|
export function CartesianChart({
|
|
111
145
|
props,
|
|
@@ -117,12 +151,12 @@ export function CartesianChart({
|
|
|
117
151
|
className?: string;
|
|
118
152
|
}) {
|
|
119
153
|
const chartData = processChartData(props.data ?? [], props.xKey, props.yKey, props.aggregate);
|
|
120
|
-
const
|
|
154
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
121
155
|
|
|
122
156
|
return (
|
|
123
|
-
<div className={`pmx-chart${className ? ` ${className}` : ''}`}>
|
|
157
|
+
<div ref={frameRef} className={`pmx-chart${className ? ` ${className}` : ''}`}>
|
|
124
158
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
125
|
-
<ResponsiveContainer width="100%" height={
|
|
159
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
126
160
|
{children(chartData)}
|
|
127
161
|
</ResponsiveContainer>
|
|
128
162
|
</div>
|
|
@@ -172,12 +206,12 @@ function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>) {
|
|
|
172
206
|
|
|
173
207
|
function ChartPieChart({ props }: BaseComponentProps<PieChartProps>) {
|
|
174
208
|
const data = props.data ?? [];
|
|
175
|
-
const
|
|
209
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
176
210
|
|
|
177
211
|
return (
|
|
178
|
-
<div className="pmx-chart pmx-chart--pie">
|
|
212
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--pie">
|
|
179
213
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
180
|
-
<ResponsiveContainer width="100%" height={
|
|
214
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
181
215
|
<RechartsPieChart margin={polarChartMargin}>
|
|
182
216
|
<Tooltip contentStyle={tooltipStyle} />
|
|
183
217
|
{props.showLegend !== false && <Legend wrapperStyle={legendMargin} />}
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
legendMargin,
|
|
41
41
|
polarChartMargin,
|
|
42
42
|
tooltipStyle,
|
|
43
|
+
useChartFrameHeight,
|
|
43
44
|
type CartesianChartProps,
|
|
44
45
|
} from './components';
|
|
45
46
|
|
|
@@ -89,12 +90,12 @@ interface ScatterChartProps {
|
|
|
89
90
|
function ChartScatterChart({ props }: BaseComponentProps<ScatterChartProps>) {
|
|
90
91
|
const fill = props.color ?? CHART_COLORS[0];
|
|
91
92
|
const data = props.data ?? [];
|
|
92
|
-
const
|
|
93
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
93
94
|
|
|
94
95
|
return (
|
|
95
|
-
<div className="pmx-chart pmx-chart--scatter">
|
|
96
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--scatter">
|
|
96
97
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
97
|
-
<ResponsiveContainer width="100%" height={
|
|
98
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
98
99
|
<RechartsScatterChart margin={chartMargin}>
|
|
99
100
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
|
|
100
101
|
<XAxis type="number" dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} name={props.xKey} />
|
|
@@ -120,12 +121,12 @@ interface RadarChartProps {
|
|
|
120
121
|
function ChartRadarChart({ props }: BaseComponentProps<RadarChartProps>) {
|
|
121
122
|
const data = props.data ?? [];
|
|
122
123
|
const metrics = (props.metrics ?? []).filter((m) => typeof m === 'string' && m.length > 0);
|
|
123
|
-
const
|
|
124
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 320);
|
|
124
125
|
|
|
125
126
|
return (
|
|
126
|
-
<div className="pmx-chart pmx-chart--radar">
|
|
127
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--radar">
|
|
127
128
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
128
|
-
<ResponsiveContainer width="100%" height={
|
|
129
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
129
130
|
<RechartsRadarChart data={data} outerRadius="66%" margin={polarChartMargin}>
|
|
130
131
|
<PolarGrid stroke="var(--border, #e5e5e5)" />
|
|
131
132
|
<PolarAngleAxis dataKey={props.axisKey} tick={axisStyle} />
|
|
@@ -166,12 +167,12 @@ function ChartStackedBarChart({ props }: BaseComponentProps<StackedBarChartProps
|
|
|
166
167
|
const chartData = props.aggregate
|
|
167
168
|
? mergeAggregated(props.data ?? [], props.xKey, series, props.aggregate)
|
|
168
169
|
: props.data ?? [];
|
|
169
|
-
const
|
|
170
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
170
171
|
|
|
171
172
|
return (
|
|
172
|
-
<div className="pmx-chart pmx-chart--stacked-bar">
|
|
173
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--stacked-bar">
|
|
173
174
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
174
|
-
<ResponsiveContainer width="100%" height={
|
|
175
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
175
176
|
<RechartsBarChart data={chartData} margin={chartMargin}>
|
|
176
177
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
|
|
177
178
|
<XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
|
|
@@ -238,12 +239,12 @@ function ChartComposedChart({ props }: BaseComponentProps<ComposedChartProps>) {
|
|
|
238
239
|
const data = props.data ?? [];
|
|
239
240
|
const barFill = props.barColor ?? CHART_COLORS[0];
|
|
240
241
|
const lineStroke = props.lineColor ?? CHART_COLORS[3];
|
|
241
|
-
const
|
|
242
|
+
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
242
243
|
|
|
243
244
|
return (
|
|
244
|
-
<div className="pmx-chart pmx-chart--composed">
|
|
245
|
+
<div ref={frameRef} className="pmx-chart pmx-chart--composed">
|
|
245
246
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
246
|
-
<ResponsiveContainer width="100%" height={
|
|
247
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
247
248
|
<RechartsComposedChart data={data} margin={chartMargin}>
|
|
248
249
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
|
|
249
250
|
<XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
|
|
@@ -511,7 +511,7 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
|
|
|
511
511
|
|
|
512
512
|
const chartProps: Record<string, unknown> = {
|
|
513
513
|
data: input.data,
|
|
514
|
-
height: input.height
|
|
514
|
+
...(typeof input.height === 'number' ? { height: input.height } : {}),
|
|
515
515
|
};
|
|
516
516
|
|
|
517
517
|
switch (chartType) {
|
|
@@ -648,6 +648,7 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
648
648
|
title: string;
|
|
649
649
|
spec: JsonRenderSpec;
|
|
650
650
|
theme?: 'dark' | 'light' | 'high-contrast';
|
|
651
|
+
display?: 'expanded';
|
|
651
652
|
}): Promise<string> {
|
|
652
653
|
try {
|
|
653
654
|
await ensureJsonRenderBundle();
|
|
@@ -661,6 +662,7 @@ export async function buildJsonRenderViewerHtml(options: {
|
|
|
661
662
|
const boot = [
|
|
662
663
|
`window.__PMX_CANVAS_JSON_RENDER_SPEC__ = ${JSON.stringify(options.spec)};`,
|
|
663
664
|
...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
|
|
665
|
+
...(options.display ? [`window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ = ${JSON.stringify(options.display)};`] : []),
|
|
664
666
|
jsBundle,
|
|
665
667
|
].join('\n');
|
|
666
668
|
return buildAppHtml({
|
package/src/mcp/canvas-access.ts
CHANGED
|
@@ -19,6 +19,8 @@ type AddDiagramInput = Parameters<PmxCanvas['addDiagram']>[0];
|
|
|
19
19
|
type AddJsonRenderNodeInput = Parameters<PmxCanvas['addJsonRenderNode']>[0];
|
|
20
20
|
type AddJsonRenderNodeResult = ReturnType<PmxCanvas['addJsonRenderNode']>;
|
|
21
21
|
type AddHtmlNodeInput = Parameters<PmxCanvas['addHtmlNode']>[0];
|
|
22
|
+
type AddHtmlPrimitiveInput = Parameters<PmxCanvas['addHtmlPrimitive']>[0];
|
|
23
|
+
type AddHtmlPrimitiveResult = ReturnType<PmxCanvas['addHtmlPrimitive']>;
|
|
22
24
|
type AddGraphNodeInput = Parameters<PmxCanvas['addGraphNode']>[0];
|
|
23
25
|
type AddGraphNodeResult = ReturnType<PmxCanvas['addGraphNode']>;
|
|
24
26
|
type UpdateNodePatch = Parameters<PmxCanvas['updateNode']>[1];
|
|
@@ -102,6 +104,7 @@ export interface CanvasAccess {
|
|
|
102
104
|
addDiagram(input: AddDiagramInput): Promise<OpenMcpAppResult>;
|
|
103
105
|
addJsonRenderNode(input: AddJsonRenderNodeInput): Promise<AddJsonRenderNodeResult>;
|
|
104
106
|
addHtmlNode(input: AddHtmlNodeInput): Promise<string>;
|
|
107
|
+
addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult>;
|
|
105
108
|
addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult>;
|
|
106
109
|
buildWebArtifact(input: WebArtifactInput): Promise<WebArtifactResult>;
|
|
107
110
|
updateNode(id: string, patch: UpdateNodePatch): Promise<void>;
|
|
@@ -188,6 +191,10 @@ class LocalCanvasAccess implements CanvasAccess {
|
|
|
188
191
|
return this.canvas.addHtmlNode(input);
|
|
189
192
|
}
|
|
190
193
|
|
|
194
|
+
async addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult> {
|
|
195
|
+
return this.canvas.addHtmlPrimitive(input);
|
|
196
|
+
}
|
|
197
|
+
|
|
191
198
|
async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {
|
|
192
199
|
return this.canvas.addGraphNode(input);
|
|
193
200
|
}
|
|
@@ -435,7 +442,53 @@ class RemoteCanvasAccess implements CanvasAccess {
|
|
|
435
442
|
}
|
|
436
443
|
|
|
437
444
|
async addHtmlNode(input: AddHtmlNodeInput): Promise<string> {
|
|
438
|
-
|
|
445
|
+
const {
|
|
446
|
+
summary,
|
|
447
|
+
agentSummary,
|
|
448
|
+
description,
|
|
449
|
+
presentation,
|
|
450
|
+
slideTitles,
|
|
451
|
+
embeddedNodeIds,
|
|
452
|
+
embeddedUrls,
|
|
453
|
+
...rest
|
|
454
|
+
} = input as AddHtmlNodeInput & {
|
|
455
|
+
summary?: string;
|
|
456
|
+
agentSummary?: string;
|
|
457
|
+
description?: string;
|
|
458
|
+
presentation?: boolean;
|
|
459
|
+
slideTitles?: string[];
|
|
460
|
+
embeddedNodeIds?: string[];
|
|
461
|
+
embeddedUrls?: string[];
|
|
462
|
+
};
|
|
463
|
+
return await this.requestNodeId('POST', '/api/canvas/node', {
|
|
464
|
+
type: 'html',
|
|
465
|
+
...rest,
|
|
466
|
+
data: {
|
|
467
|
+
...(typeof summary === 'string' ? { summary } : {}),
|
|
468
|
+
...(typeof agentSummary === 'string' ? { agentSummary } : {}),
|
|
469
|
+
...(typeof description === 'string' ? { description } : {}),
|
|
470
|
+
...(presentation === true ? { presentation: true } : {}),
|
|
471
|
+
...(Array.isArray(slideTitles) ? { slideTitles } : {}),
|
|
472
|
+
...(Array.isArray(embeddedNodeIds) ? { embeddedNodeIds } : {}),
|
|
473
|
+
...(Array.isArray(embeddedUrls) ? { embeddedUrls } : {}),
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async addHtmlPrimitive(input: AddHtmlPrimitiveInput): Promise<AddHtmlPrimitiveResult> {
|
|
479
|
+
const response = await this.requestJson<{
|
|
480
|
+
id?: string;
|
|
481
|
+
node?: { id?: string };
|
|
482
|
+
primitive?: { kind?: string; title?: string; htmlBytes?: number };
|
|
483
|
+
}>('POST', '/api/canvas/node', { type: 'html', ...input, primitive: input.kind });
|
|
484
|
+
const id = typeof response.id === 'string' ? response.id : response.node?.id;
|
|
485
|
+
if (!id) throw new Error('html primitive response did not include a node id.');
|
|
486
|
+
return {
|
|
487
|
+
id,
|
|
488
|
+
kind: input.kind,
|
|
489
|
+
title: response.primitive?.title ?? input.title ?? input.kind,
|
|
490
|
+
htmlBytes: response.primitive?.htmlBytes ?? 0,
|
|
491
|
+
};
|
|
439
492
|
}
|
|
440
493
|
|
|
441
494
|
async addGraphNode(input: AddGraphNodeInput): Promise<AddGraphNodeResult> {
|