pmx-canvas 0.3.0 → 0.3.1
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/dist/canvas/index.js +2 -2
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/docs/http-api.md +28 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
- package/docs/tech-debt-assessment-2026-07.md +2 -2
- package/package.json +5 -2
- package/skills/pmx-canvas/SKILL.md +3 -1
- package/skills/pmx-canvas/references/full-reference.md +10 -3
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +35 -21
- package/src/mcp/server.ts +132 -70
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +182 -39
- package/src/server/ax-state.ts +142 -47
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +454 -73
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +150 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +19 -6
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +3 -12
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +320 -217
- package/src/server/intent-registry.ts +2 -5
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +1 -3
- package/src/server/operations/http.ts +2 -3
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +22 -30
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +98 -73
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +298 -109
- package/src/server/operations/ops/query.ts +46 -28
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +108 -1027
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -72,10 +72,8 @@ function applyLayoutOverrides(node: CanvasNodeState): CanvasNodeState {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// ── Default positions by type ─────────────────────────────────
|
|
75
|
-
const DEFAULT_POSITIONS: Record<
|
|
76
|
-
|
|
77
|
-
{ x: number; y: number; w: number; h: number }
|
|
78
|
-
> & Record<'prompt' | 'response', { x: number; y: number; w: number; h: number }> = {
|
|
75
|
+
const DEFAULT_POSITIONS: Record<CanvasNodeState['type'], { x: number; y: number; w: number; h: number }> &
|
|
76
|
+
Record<'prompt' | 'response', { x: number; y: number; w: number; h: number }> = {
|
|
79
77
|
status: { x: 40, y: 80, w: 300, h: 120 },
|
|
80
78
|
markdown: { x: 380, y: 80, w: 720, h: 600 },
|
|
81
79
|
context: { x: 1130, y: 80, w: 320, h: 400 },
|
|
@@ -114,11 +112,7 @@ function makeNode(
|
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
function getMarkdownPlacement(): { x: number; y: number } {
|
|
117
|
-
return findOpenCanvasPosition(
|
|
118
|
-
[...nodes.value.values()],
|
|
119
|
-
DEFAULT_POSITIONS.markdown.w,
|
|
120
|
-
DEFAULT_POSITIONS.markdown.h,
|
|
121
|
-
);
|
|
115
|
+
return findOpenCanvasPosition([...nodes.value.values()], DEFAULT_POSITIONS.markdown.w, DEFAULT_POSITIONS.markdown.h);
|
|
122
116
|
}
|
|
123
117
|
|
|
124
118
|
// ── Node ensure helpers ───────────────────────────────────────
|
|
@@ -243,11 +237,7 @@ function findExtAppNodeId(toolCallId: string): string | null {
|
|
|
243
237
|
const legacyDirectId = `ext-app-${toolCallId}`;
|
|
244
238
|
if (legacyDirectId !== directId && nodes.value.has(legacyDirectId)) return legacyDirectId;
|
|
245
239
|
for (const [nodeId, node] of nodes.value.entries()) {
|
|
246
|
-
if (
|
|
247
|
-
node.type === 'mcp-app' &&
|
|
248
|
-
node.data.mode === 'ext-app' &&
|
|
249
|
-
node.data.toolCallId === toolCallId
|
|
250
|
-
) {
|
|
240
|
+
if (node.type === 'mcp-app' && node.data.mode === 'ext-app' && node.data.toolCallId === toolCallId) {
|
|
251
241
|
return nodeId;
|
|
252
242
|
}
|
|
253
243
|
}
|
|
@@ -301,28 +291,27 @@ function applyCanvasTheme(theme: string): void {
|
|
|
301
291
|
}
|
|
302
292
|
|
|
303
293
|
function isCanvasNodeType(value: unknown): value is CanvasNodeState['type'] {
|
|
304
|
-
return
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
294
|
+
return (
|
|
295
|
+
value === 'markdown' ||
|
|
296
|
+
value === 'mcp-app' ||
|
|
297
|
+
value === 'webpage' ||
|
|
298
|
+
value === 'json-render' ||
|
|
299
|
+
value === 'graph' ||
|
|
300
|
+
value === 'prompt' ||
|
|
301
|
+
value === 'response' ||
|
|
302
|
+
value === 'status' ||
|
|
303
|
+
value === 'context' ||
|
|
304
|
+
value === 'ledger' ||
|
|
305
|
+
value === 'trace' ||
|
|
306
|
+
value === 'file' ||
|
|
307
|
+
value === 'image' ||
|
|
308
|
+
value === 'html' ||
|
|
309
|
+
value === 'group'
|
|
310
|
+
);
|
|
319
311
|
}
|
|
320
312
|
|
|
321
313
|
function isCanvasEdgeType(value: unknown): value is CanvasEdge['type'] {
|
|
322
|
-
return value === 'relation'
|
|
323
|
-
|| value === 'depends-on'
|
|
324
|
-
|| value === 'flow'
|
|
325
|
-
|| value === 'references';
|
|
314
|
+
return value === 'relation' || value === 'depends-on' || value === 'flow' || value === 'references';
|
|
326
315
|
}
|
|
327
316
|
|
|
328
317
|
function parseCanvasPosition(value: unknown): { x: number; y: number } | null {
|
|
@@ -353,10 +342,8 @@ function parseCanvasNode(raw: Record<string, unknown>): CanvasNodeState | null {
|
|
|
353
342
|
const size = parseCanvasSize(raw.size);
|
|
354
343
|
if (!position || !size) return null;
|
|
355
344
|
|
|
356
|
-
const dockPosition =
|
|
357
|
-
|
|
358
|
-
const data =
|
|
359
|
-
raw.data && typeof raw.data === 'object' ? Object.fromEntries(Object.entries(raw.data)) : {};
|
|
345
|
+
const dockPosition = raw.dockPosition === 'left' || raw.dockPosition === 'right' ? raw.dockPosition : null;
|
|
346
|
+
const data = raw.data && typeof raw.data === 'object' ? Object.fromEntries(Object.entries(raw.data)) : {};
|
|
360
347
|
|
|
361
348
|
return {
|
|
362
349
|
id: raw.id,
|
|
@@ -383,9 +370,7 @@ function parseCanvasEdge(raw: Record<string, unknown>): CanvasEdge | null {
|
|
|
383
370
|
to: raw.to,
|
|
384
371
|
type: raw.type,
|
|
385
372
|
...(typeof raw.label === 'string' ? { label: raw.label } : {}),
|
|
386
|
-
...(raw.style === 'solid' || raw.style === 'dashed' || raw.style === 'dotted'
|
|
387
|
-
? { style: raw.style }
|
|
388
|
-
: {}),
|
|
373
|
+
...(raw.style === 'solid' || raw.style === 'dashed' || raw.style === 'dotted' ? { style: raw.style } : {}),
|
|
389
374
|
...(raw.animated === true ? { animated: true } : {}),
|
|
390
375
|
};
|
|
391
376
|
}
|
|
@@ -429,8 +414,7 @@ function handleWorkbenchOpen(data: Record<string, unknown>): void {
|
|
|
429
414
|
// H6: Guard — path must be a string for node ID stability
|
|
430
415
|
if (typeof data.path !== 'string' || !data.path) return;
|
|
431
416
|
const path = data.path;
|
|
432
|
-
const title =
|
|
433
|
-
(typeof data.title === 'string' ? data.title : '') || path.split('/').pop() || 'Untitled';
|
|
417
|
+
const title = (typeof data.title === 'string' ? data.title : '') || path.split('/').pop() || 'Untitled';
|
|
434
418
|
|
|
435
419
|
ensureMarkdownNode(path, title);
|
|
436
420
|
if (data.ledgerSummary) {
|
|
@@ -515,9 +499,7 @@ function handleAuxClose(data: Record<string, unknown>): void {
|
|
|
515
499
|
} else {
|
|
516
500
|
const existing = nodes.value.get(id);
|
|
517
501
|
if (!existing) return;
|
|
518
|
-
const auxTabs = ((existing.data.auxTabs as Array<Record<string, unknown>>) ?? []).filter(
|
|
519
|
-
(t) => t.id !== data.id,
|
|
520
|
-
);
|
|
502
|
+
const auxTabs = ((existing.data.auxTabs as Array<Record<string, unknown>>) ?? []).filter((t) => t.id !== data.id);
|
|
521
503
|
updateNodeData(id, { auxTabs });
|
|
522
504
|
}
|
|
523
505
|
}
|
|
@@ -587,8 +569,7 @@ function handleExtAppOpen(data: Record<string, unknown>): void {
|
|
|
587
569
|
|
|
588
570
|
function handleExtAppUpdate(data: Record<string, unknown>): void {
|
|
589
571
|
if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
|
|
590
|
-
const id =
|
|
591
|
-
findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
|
|
572
|
+
const id = findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
|
|
592
573
|
if (!id) return;
|
|
593
574
|
if (nodes.value.has(id)) {
|
|
594
575
|
updateNodeData(id, { html: data.html });
|
|
@@ -597,8 +578,7 @@ function handleExtAppUpdate(data: Record<string, unknown>): void {
|
|
|
597
578
|
|
|
598
579
|
function handleExtAppResult(data: Record<string, unknown>): void {
|
|
599
580
|
if (typeof data.toolCallId !== 'string' || !data.toolCallId) return;
|
|
600
|
-
const id =
|
|
601
|
-
findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
|
|
581
|
+
const id = findExtAppEventNodeId(data) ?? findOnlyPendingExtAppNodeId(data.serverName, data.toolName);
|
|
602
582
|
if (!id) return;
|
|
603
583
|
if (nodes.value.has(id)) {
|
|
604
584
|
if (data.success === false) {
|
|
@@ -611,8 +591,7 @@ function handleExtAppResult(data: Record<string, unknown>): void {
|
|
|
611
591
|
success: typeof data.success === 'boolean' ? data.success : undefined,
|
|
612
592
|
error: typeof data.error === 'string' ? data.error : undefined,
|
|
613
593
|
content: typeof data.content === 'string' ? data.content : undefined,
|
|
614
|
-
detailedContent:
|
|
615
|
-
typeof data.detailedContent === 'string' ? data.detailedContent : undefined,
|
|
594
|
+
detailedContent: typeof data.detailedContent === 'string' ? data.detailedContent : undefined,
|
|
616
595
|
}),
|
|
617
596
|
});
|
|
618
597
|
}
|
|
@@ -843,14 +822,14 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
|
|
|
843
822
|
const shouldApplyViewport = !hasInitialServerLayout.value;
|
|
844
823
|
hasInitialServerLayout.value = true;
|
|
845
824
|
|
|
846
|
-
const serverNodes = layout.nodes
|
|
847
|
-
.map(parseCanvasNode)
|
|
848
|
-
.filter((node): node is CanvasNodeState => node !== null);
|
|
825
|
+
const serverNodes = layout.nodes.map(parseCanvasNode).filter((node): node is CanvasNodeState => node !== null);
|
|
849
826
|
const serverEdges = Array.isArray(layout.edges)
|
|
850
827
|
? layout.edges.map(parseCanvasEdge).filter((edge): edge is CanvasEdge => edge !== null)
|
|
851
828
|
: Array.from(edges.value.values());
|
|
852
829
|
const serverAnnotations = Array.isArray(layout.annotations)
|
|
853
|
-
? layout.annotations
|
|
830
|
+
? layout.annotations
|
|
831
|
+
.map(parseCanvasAnnotation)
|
|
832
|
+
.filter((annotation): annotation is CanvasAnnotation => annotation !== null)
|
|
854
833
|
: undefined;
|
|
855
834
|
const nextViewport = layout.viewport
|
|
856
835
|
? {
|
|
@@ -861,12 +840,15 @@ function handleCanvasLayoutUpdate(data: Record<string, unknown>): void {
|
|
|
861
840
|
: undefined;
|
|
862
841
|
|
|
863
842
|
cancelViewportAnimation();
|
|
864
|
-
applyServerCanvasLayout(
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
843
|
+
applyServerCanvasLayout(
|
|
844
|
+
{
|
|
845
|
+
...(nextViewport ? { viewport: nextViewport } : {}),
|
|
846
|
+
nodes: serverNodes,
|
|
847
|
+
edges: serverEdges,
|
|
848
|
+
...(serverAnnotations ? { annotations: serverAnnotations } : {}),
|
|
849
|
+
},
|
|
850
|
+
{ applyViewport: shouldApplyViewport },
|
|
851
|
+
);
|
|
870
852
|
|
|
871
853
|
syncAttentionFromSse({ event: 'canvas-layout-update', data });
|
|
872
854
|
}
|
|
@@ -923,9 +905,7 @@ function handleThemeChanged(data: Record<string, unknown>): void {
|
|
|
923
905
|
}
|
|
924
906
|
|
|
925
907
|
function handleContextPinsChanged(data: Record<string, unknown>): void {
|
|
926
|
-
const nodeIds = Array.isArray(data.nodeIds)
|
|
927
|
-
? data.nodeIds.filter((id): id is string => typeof id === 'string')
|
|
928
|
-
: [];
|
|
908
|
+
const nodeIds = Array.isArray(data.nodeIds) ? data.nodeIds.filter((id): id is string => typeof id === 'string') : [];
|
|
929
909
|
replaceContextPinsFromServer(nodeIds);
|
|
930
910
|
syncAttentionFromSse({ event: 'context-pins-changed', data });
|
|
931
911
|
}
|
|
@@ -939,7 +919,9 @@ function handleAxStateChanged(): void {
|
|
|
939
919
|
if (axRefreshTimer) clearTimeout(axRefreshTimer);
|
|
940
920
|
axRefreshTimer = setTimeout(() => {
|
|
941
921
|
axRefreshTimer = null;
|
|
942
|
-
void fetchAxSurfaceState().then((state) => {
|
|
922
|
+
void fetchAxSurfaceState().then((state) => {
|
|
923
|
+
axSurfaceState.value = state;
|
|
924
|
+
});
|
|
943
925
|
}, 150);
|
|
944
926
|
}
|
|
945
927
|
|
|
@@ -951,11 +933,12 @@ function handleAxIntent(data: Record<string, unknown>): void {
|
|
|
951
933
|
// be pruned if its `clear` frame were dropped. The server always sets it, so this
|
|
952
934
|
// only rejects a malformed frame — keeping the backstop's guarantee real.
|
|
953
935
|
if (
|
|
954
|
-
!intent
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
)
|
|
936
|
+
!intent ||
|
|
937
|
+
typeof intent.id !== 'string' ||
|
|
938
|
+
typeof intent.kind !== 'string' ||
|
|
939
|
+
typeof intent.expiresAt !== 'number'
|
|
940
|
+
)
|
|
941
|
+
return;
|
|
959
942
|
upsertIntent(intent);
|
|
960
943
|
}
|
|
961
944
|
|
|
@@ -23,7 +23,9 @@ export const allComponentDefinitions = {
|
|
|
23
23
|
Badge: {
|
|
24
24
|
...badgeDefinition,
|
|
25
25
|
props: badgeDefinition.props.extend({
|
|
26
|
-
variant: z
|
|
26
|
+
variant: z
|
|
27
|
+
.enum(['default', 'secondary', 'destructive', 'outline', 'success', 'info', 'warning', 'error', 'danger'])
|
|
28
|
+
.nullable(),
|
|
27
29
|
}),
|
|
28
30
|
},
|
|
29
31
|
Button: {
|
|
@@ -40,9 +42,12 @@ export const allComponentDefinitions = {
|
|
|
40
42
|
...tufteChartComponentDefinitions,
|
|
41
43
|
};
|
|
42
44
|
|
|
43
|
-
export const catalog = defineCatalog(
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
export const catalog = defineCatalog(
|
|
46
|
+
schema as never,
|
|
47
|
+
{
|
|
48
|
+
components: allComponentDefinitions,
|
|
49
|
+
} as never,
|
|
50
|
+
);
|
|
46
51
|
|
|
47
52
|
export interface JsonRenderIssue {
|
|
48
53
|
path?: PropertyKey[];
|
|
@@ -134,12 +139,7 @@ function unwrapSchema(value: unknown): {
|
|
|
134
139
|
|
|
135
140
|
const record = asRecord(current);
|
|
136
141
|
const def = asRecord(record?._def) ?? asRecord(record?.def);
|
|
137
|
-
const inner =
|
|
138
|
-
def?.innerType ??
|
|
139
|
-
def?.schema ??
|
|
140
|
-
def?.type ??
|
|
141
|
-
def?.out ??
|
|
142
|
-
def?.in;
|
|
142
|
+
const inner = def?.innerType ?? def?.schema ?? def?.type ?? def?.out ?? def?.in;
|
|
143
143
|
|
|
144
144
|
if (!inner || inner === current || (!isOptionalSchema(current) && !isNullableSchema(current))) {
|
|
145
145
|
break;
|
|
@@ -178,9 +178,7 @@ function schemaTypeLabel(value: unknown): string {
|
|
|
178
178
|
: Array.isArray(def?.options)
|
|
179
179
|
? def.options
|
|
180
180
|
: [];
|
|
181
|
-
label = rawValues.length > 0
|
|
182
|
-
? rawValues.map((entry) => JSON.stringify(entry)).join(' | ')
|
|
183
|
-
: 'enum';
|
|
181
|
+
label = rawValues.length > 0 ? rawValues.map((entry) => JSON.stringify(entry)).join(' | ') : 'enum';
|
|
184
182
|
} else if (typeName === 'ZodLiteral' || typeName === 'literal') {
|
|
185
183
|
label = JSON.stringify(def?.value ?? def?.literal ?? 'literal');
|
|
186
184
|
} else if (typeName === 'ZodAny' || typeName === 'any') {
|
|
@@ -255,9 +253,7 @@ export function validateShadcnElementProps(spec: unknown): JsonRenderValidationR
|
|
|
255
253
|
if (!definition || !hasSafeParse(definition.props)) continue;
|
|
256
254
|
|
|
257
255
|
const elementProps = asRecord(element.props) ?? {};
|
|
258
|
-
const parsed = definition.props.safeParse(
|
|
259
|
-
normalizePropsForSchema(definition.props, elementProps),
|
|
260
|
-
);
|
|
256
|
+
const parsed = definition.props.safeParse(normalizePropsForSchema(definition.props, elementProps));
|
|
261
257
|
if (parsed.success) continue;
|
|
262
258
|
|
|
263
259
|
for (const issue of parsed.error?.issues ?? []) {
|
|
@@ -120,8 +120,9 @@ export function useChartFrameHeight(explicitHeight: number | null | undefined, f
|
|
|
120
120
|
|
|
121
121
|
// Standalone "Open as site" tab (#65): fill the full browser viewport — there is no
|
|
122
122
|
// card chrome below the chart, so drop the ~44px reserve and use a larger floor.
|
|
123
|
-
const isSite =
|
|
124
|
-
|
|
123
|
+
const isSite =
|
|
124
|
+
typeof window !== 'undefined' &&
|
|
125
|
+
(window as { __PMX_CANVAS_JSON_RENDER_DISPLAY__?: string }).__PMX_CANVAS_JSON_RENDER_DISPLAY__ === 'site';
|
|
125
126
|
|
|
126
127
|
useEffect(() => {
|
|
127
128
|
const frame = frameRef.current;
|
|
@@ -169,16 +170,21 @@ export function useChartFrameHeight(explicitHeight: number | null | undefined, f
|
|
|
169
170
|
// the node/viewport height. That makes the document's scrollHeight stable so the
|
|
170
171
|
// node can grow to it once and converge (no fill-down feedback loop). When NOT in
|
|
171
172
|
// content-fit (strictSize / user-resized nodes), it fills the frame down as before.
|
|
172
|
-
const fitContent =
|
|
173
|
-
|
|
173
|
+
const fitContent =
|
|
174
|
+
typeof window !== 'undefined' &&
|
|
175
|
+
(window as { __PMX_CANVAS_FIT_CONTENT__?: boolean }).__PMX_CANVAS_FIT_CONTENT__ === true;
|
|
174
176
|
// Site mode (#65): fill the viewport (autoHeight), ignoring an explicit/configured
|
|
175
177
|
// chart height that would otherwise cap it to a shallow card. Content-fit is off in
|
|
176
178
|
// site mode (the server skips it), so site never takes the intrinsic-height branch.
|
|
177
179
|
const height = isSite
|
|
178
180
|
? autoHeight
|
|
179
181
|
: fitContent
|
|
180
|
-
?
|
|
181
|
-
|
|
182
|
+
? typeof explicitHeight === 'number'
|
|
183
|
+
? explicitHeight
|
|
184
|
+
: fallbackHeight
|
|
185
|
+
: typeof explicitHeight === 'number'
|
|
186
|
+
? Math.min(explicitHeight, autoHeight)
|
|
187
|
+
: autoHeight;
|
|
182
188
|
return {
|
|
183
189
|
frameRef,
|
|
184
190
|
height,
|
|
@@ -251,11 +257,7 @@ function ChartLineChart({ props }: BaseComponentProps<CartesianChartProps>) {
|
|
|
251
257
|
* 'max'/'min' pick the tallest/shortest yKey value; a number is used as-is
|
|
252
258
|
* (clamped to range); null/out-of-range means no bar is emphasized.
|
|
253
259
|
*/
|
|
254
|
-
function resolveHighlightIndex(
|
|
255
|
-
data: Record<string, unknown>[],
|
|
256
|
-
yKey: string,
|
|
257
|
-
highlight: BarHighlight,
|
|
258
|
-
): number {
|
|
260
|
+
function resolveHighlightIndex(data: Record<string, unknown>[], yKey: string, highlight: BarHighlight): number {
|
|
259
261
|
if (highlight === null || data.length === 0) return -1;
|
|
260
262
|
if (typeof highlight === 'number') {
|
|
261
263
|
return highlight >= 0 && highlight < data.length ? highlight : -1;
|
|
@@ -299,9 +301,7 @@ function barCellFill(
|
|
|
299
301
|
case 'series':
|
|
300
302
|
default:
|
|
301
303
|
// Tufte-safe emphasis: one accent bar, the rest a muted version of it.
|
|
302
|
-
return index === highlightIndex
|
|
303
|
-
? accent
|
|
304
|
-
: `color-mix(in oklch, ${accent} 32%, transparent)`;
|
|
304
|
+
return index === highlightIndex ? accent : `color-mix(in oklch, ${accent} 32%, transparent)`;
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
|
|
@@ -317,8 +317,7 @@ function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>) {
|
|
|
317
317
|
min: values.length ? Math.min(...values) : 0,
|
|
318
318
|
max: values.length ? Math.max(...values) : 0,
|
|
319
319
|
};
|
|
320
|
-
const highlightIndex =
|
|
321
|
-
mode === 'series' ? resolveHighlightIndex(data, props.yKey, highlight) : -1;
|
|
320
|
+
const highlightIndex = mode === 'series' ? resolveHighlightIndex(data, props.yKey, highlight) : -1;
|
|
322
321
|
return (
|
|
323
322
|
<RechartsBarChart data={data} margin={chartMargin}>
|
|
324
323
|
<CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
|
|
@@ -327,10 +326,7 @@ function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>) {
|
|
|
327
326
|
<Tooltip contentStyle={tooltipStyle} cursor={false} />
|
|
328
327
|
<Bar dataKey={props.yKey} radius={[4, 4, 0, 0]}>
|
|
329
328
|
{data.map((_, i) => (
|
|
330
|
-
<Cell
|
|
331
|
-
key={i}
|
|
332
|
-
fill={barCellFill(mode, accent, i, values[i], range, highlightIndex)}
|
|
333
|
-
/>
|
|
329
|
+
<Cell key={i} fill={barCellFill(mode, accent, i, values[i], range, highlightIndex)} />
|
|
334
330
|
))}
|
|
335
331
|
</Bar>
|
|
336
332
|
</RechartsBarChart>
|
|
@@ -136,16 +136,7 @@ function ChartRadarChart({ props }: BaseComponentProps<RadarChartProps>) {
|
|
|
136
136
|
{props.showLegend !== false && <Legend wrapperStyle={legendMargin} />}
|
|
137
137
|
{metrics.map((metric, i) => {
|
|
138
138
|
const color = CHART_COLORS[i % CHART_COLORS.length];
|
|
139
|
-
return
|
|
140
|
-
<Radar
|
|
141
|
-
key={metric}
|
|
142
|
-
name={metric}
|
|
143
|
-
dataKey={metric}
|
|
144
|
-
stroke={color}
|
|
145
|
-
fill={color}
|
|
146
|
-
fillOpacity={0.25}
|
|
147
|
-
/>
|
|
148
|
-
);
|
|
139
|
+
return <Radar key={metric} name={metric} dataKey={metric} stroke={color} fill={color} fillOpacity={0.25} />;
|
|
149
140
|
})}
|
|
150
141
|
</RechartsRadarChart>
|
|
151
142
|
</ResponsiveContainer>
|
|
@@ -167,7 +158,7 @@ function ChartStackedBarChart({ props }: BaseComponentProps<StackedBarChartProps
|
|
|
167
158
|
const series = (props.series ?? []).filter((s) => typeof s === 'string' && s.length > 0);
|
|
168
159
|
const chartData = props.aggregate
|
|
169
160
|
? mergeAggregated(props.data ?? [], props.xKey, series, props.aggregate)
|
|
170
|
-
: props.data ?? [];
|
|
161
|
+
: (props.data ?? []);
|
|
171
162
|
const { frameRef, height } = useChartFrameHeight(props.height, 300);
|
|
172
163
|
|
|
173
164
|
return (
|
|
@@ -212,11 +203,12 @@ function mergeAggregated(
|
|
|
212
203
|
bucket[s].push(Number.isNaN(n) ? 0 : n);
|
|
213
204
|
}
|
|
214
205
|
}
|
|
215
|
-
const reducer =
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
206
|
+
const reducer =
|
|
207
|
+
aggregate === 'count'
|
|
208
|
+
? (vs: number[]) => vs.length
|
|
209
|
+
: aggregate === 'avg'
|
|
210
|
+
? (vs: number[]) => vs.reduce((a, b) => a + b, 0) / vs.length
|
|
211
|
+
: (vs: number[]) => vs.reduce((a, b) => a + b, 0);
|
|
220
212
|
return Array.from(grouped.entries()).map(([key, buckets]) => {
|
|
221
213
|
const out: Record<string, unknown> = { [xKey]: key };
|
|
222
214
|
for (const s of series) out[s] = reducer(buckets[s] ?? []);
|
|
@@ -130,8 +130,7 @@ export const extraChartComponentDefinitions = {
|
|
|
130
130
|
height: z.number().nullable(),
|
|
131
131
|
showLegend: z.boolean().optional(),
|
|
132
132
|
}),
|
|
133
|
-
description:
|
|
134
|
-
'Combined bar + line chart for paired metrics (e.g. counts + a derived rate) on the same axis.',
|
|
133
|
+
description: 'Combined bar + line chart for paired metrics (e.g. counts + a derived rate) on the same axis.',
|
|
135
134
|
example: {
|
|
136
135
|
title: 'Visits and conversion',
|
|
137
136
|
data: [
|
|
@@ -100,9 +100,7 @@ function ChartSparkline({ props }: BaseComponentProps<SparklineProps>) {
|
|
|
100
100
|
role="img"
|
|
101
101
|
aria-label={props.title ?? 'sparkline'}
|
|
102
102
|
>
|
|
103
|
-
{props.fill && areaPath &&
|
|
104
|
-
<path d={areaPath} fill={stroke} fillOpacity={0.12} stroke="none" />
|
|
105
|
-
)}
|
|
103
|
+
{props.fill && areaPath && <path d={areaPath} fill={stroke} fillOpacity={0.12} stroke="none" />}
|
|
106
104
|
{points.length > 1 && (
|
|
107
105
|
<path d={linePath} fill="none" stroke={stroke} strokeWidth={1.5} vectorEffect="non-scaling-stroke" />
|
|
108
106
|
)}
|
|
@@ -166,7 +164,13 @@ function ChartDotPlot({ props }: BaseComponentProps<DotPlotProps>) {
|
|
|
166
164
|
return (
|
|
167
165
|
<div ref={frameRef} className="pmx-chart pmx-chart--dot-plot" style={{ height }}>
|
|
168
166
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
169
|
-
<svg
|
|
167
|
+
<svg
|
|
168
|
+
className="pmx-chart__dot-plot-svg"
|
|
169
|
+
width="100%"
|
|
170
|
+
height={rows.length * rowH}
|
|
171
|
+
role="img"
|
|
172
|
+
aria-label={props.title ?? 'dot plot'}
|
|
173
|
+
>
|
|
170
174
|
{rows.map((row, i) => {
|
|
171
175
|
const cy = i * rowH + rowH / 2;
|
|
172
176
|
// Reference rule runs from the axis origin to the dot, so the line's
|
|
@@ -245,12 +249,17 @@ function ChartBulletChart({ props }: BaseComponentProps<BulletChartProps>) {
|
|
|
245
249
|
return (
|
|
246
250
|
<div ref={frameRef} className="pmx-chart pmx-chart--bullet" style={{ height }}>
|
|
247
251
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
248
|
-
<svg
|
|
252
|
+
<svg
|
|
253
|
+
className="pmx-chart__bullet-svg"
|
|
254
|
+
width="100%"
|
|
255
|
+
height={rows.length * rowH}
|
|
256
|
+
role="img"
|
|
257
|
+
aria-label={props.title ?? 'bullet chart'}
|
|
258
|
+
>
|
|
249
259
|
{rows.map((row, i) => {
|
|
250
260
|
const top = i * rowH;
|
|
251
261
|
const cy = top + rowH / 2;
|
|
252
|
-
const domainMax =
|
|
253
|
-
Math.max(row.value, row.target ?? 0, ...(row.ranges ?? [0])) || 1;
|
|
262
|
+
const domainMax = Math.max(row.value, row.target ?? 0, ...(row.ranges ?? [0])) || 1;
|
|
254
263
|
const ranges = row.ranges ?? [];
|
|
255
264
|
const left = labelW + padX;
|
|
256
265
|
const rightInset = padX;
|
|
@@ -261,9 +270,11 @@ function ChartBulletChart({ props }: BaseComponentProps<BulletChartProps>) {
|
|
|
261
270
|
const barH = Math.min(20, rowH * 0.5);
|
|
262
271
|
const measureH = barH * 0.4;
|
|
263
272
|
// Qualitative bands: lightest (worst) to darkest (best) grayscale.
|
|
264
|
-
const bandShades = [
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
const bandShades = [
|
|
274
|
+
'color-mix(in oklch, var(--muted) 35%, transparent)',
|
|
275
|
+
'color-mix(in oklch, var(--muted) 60%, transparent)',
|
|
276
|
+
'color-mix(in oklch, var(--muted) 90%, transparent)',
|
|
277
|
+
];
|
|
267
278
|
return (
|
|
268
279
|
<g key={`${row.label}-${i}`}>
|
|
269
280
|
<text x={labelW} y={cy} textAnchor="end" dominantBaseline="central" fontSize={12} fill={INK}>
|
|
@@ -285,7 +296,14 @@ function ChartBulletChart({ props }: BaseComponentProps<BulletChartProps>) {
|
|
|
285
296
|
{/* Per-row scale ticks at each band boundary so the reader does not
|
|
286
297
|
compare bar lengths across rows that may be independently scaled. */}
|
|
287
298
|
{ranges.map((hi, idx) => (
|
|
288
|
-
<text
|
|
299
|
+
<text
|
|
300
|
+
key={`tick-${idx}`}
|
|
301
|
+
x={xAt(hi)}
|
|
302
|
+
y={cy + barH / 2 + 10}
|
|
303
|
+
textAnchor="middle"
|
|
304
|
+
fontSize={9}
|
|
305
|
+
fill={MUTED}
|
|
306
|
+
>
|
|
289
307
|
{hi}
|
|
290
308
|
</text>
|
|
291
309
|
))}
|
|
@@ -345,7 +363,13 @@ function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
|
|
|
345
363
|
return (
|
|
346
364
|
<div ref={frameRef} className="pmx-chart pmx-chart--slopegraph" style={{ height }}>
|
|
347
365
|
{props.title && <div className="pmx-chart__title">{props.title}</div>}
|
|
348
|
-
<svg
|
|
366
|
+
<svg
|
|
367
|
+
className="pmx-chart__slopegraph-svg"
|
|
368
|
+
width="100%"
|
|
369
|
+
height={plotH}
|
|
370
|
+
role="img"
|
|
371
|
+
aria-label={props.title ?? 'slopegraph'}
|
|
372
|
+
>
|
|
349
373
|
<text x={leftX} y={14} textAnchor="end" fontSize={12} fontWeight={600} fill={MUTED}>
|
|
350
374
|
{props.beforeLabel ?? props.beforeKey}
|
|
351
375
|
</text>
|
|
@@ -363,14 +387,7 @@ function ChartSlopegraph({ props }: BaseComponentProps<SlopegraphProps>) {
|
|
|
363
387
|
const lineColor = props.colorByDirection ? (rose ? stroke : MUTED) : stroke;
|
|
364
388
|
return (
|
|
365
389
|
<g key={`${row.label}-${i}`}>
|
|
366
|
-
<line
|
|
367
|
-
x1={leftX}
|
|
368
|
-
y1={y1}
|
|
369
|
-
x2={rightX}
|
|
370
|
-
y2={y2}
|
|
371
|
-
stroke={lineColor}
|
|
372
|
-
strokeWidth={1.5}
|
|
373
|
-
/>
|
|
390
|
+
<line x1={leftX} y1={y1} x2={rightX} y2={y2} stroke={lineColor} strokeWidth={1.5} />
|
|
374
391
|
<circle cx={leftX} cy={y1} r={2.5} fill={lineColor} />
|
|
375
392
|
<circle cx={rightX} cy={y2} r={2.5} fill={lineColor} />
|
|
376
393
|
<text x={leftX - 8} y={y1} textAnchor="end" dominantBaseline="central" fontSize={11} fill={INK}>
|
|
@@ -25,7 +25,13 @@ export const tufteChartComponentDefinitions = {
|
|
|
25
25
|
'Word-sized sparkline: a single trend line with no axes, grid, or labels. Optional end dot, min/max markers, light area fill, and an inline last value. The canonical Tufte primitive for showing a trajectory in minimal space.',
|
|
26
26
|
example: {
|
|
27
27
|
title: 'Latency p95',
|
|
28
|
-
data: [
|
|
28
|
+
data: [
|
|
29
|
+
{ t: 0, ms: 120 },
|
|
30
|
+
{ t: 1, ms: 138 },
|
|
31
|
+
{ t: 2, ms: 117 },
|
|
32
|
+
{ t: 3, ms: 152 },
|
|
33
|
+
{ t: 4, ms: 109 },
|
|
34
|
+
],
|
|
29
35
|
valueKey: 'ms',
|
|
30
36
|
color: null,
|
|
31
37
|
fill: true,
|
|
@@ -107,7 +113,7 @@ export const tufteChartComponentDefinitions = {
|
|
|
107
113
|
height: z.number().nullable(),
|
|
108
114
|
}),
|
|
109
115
|
description:
|
|
110
|
-
|
|
116
|
+
'Tufte slopegraph: two value columns (before/after) with a connecting line per category. Lines use one neutral ink by default; set colorByDirection to accent rising lines and mute falling ones. Ideal for paired change across many items.',
|
|
111
117
|
example: {
|
|
112
118
|
title: 'Coverage before/after refactor',
|
|
113
119
|
data: [
|