pmx-canvas 0.6.3 → 0.6.5
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 +31 -0
- package/Readme.md +27 -6
- package/dist/canvas/global.css +90 -4
- package/dist/canvas/index.js +93 -91
- package/dist/canvas/mermaid-entry.js +258 -258
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/record.d.ts +12 -0
- package/dist/types/client/canvas/FeedbackDialog.d.ts +4 -0
- package/dist/types/client/canvas/Presentation.d.ts +13 -0
- package/dist/types/client/canvas/use-node-drag.d.ts +2 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +24 -1
- package/dist/types/client/state/presentation.d.ts +3 -0
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/json-render/catalog.d.ts +2 -2
- package/dist/types/json-render/server.d.ts +2 -0
- package/dist/types/server/canvas-db.d.ts +2 -0
- package/dist/types/server/canvas-operations.d.ts +22 -0
- package/dist/types/server/canvas-schema.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +6 -0
- package/dist/types/server/canvas-validation.d.ts +3 -0
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/operations/ops/app.d.ts +2 -0
- package/dist/types/server/operations/ops/json-render.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +3 -1
- package/dist/types/server/web-artifacts.d.ts +3 -0
- package/dist/types/shared/tour.d.ts +72 -0
- package/docs/cli.md +96 -2
- package/docs/http-api.md +79 -4
- package/docs/mcp.md +25 -1
- package/docs/node-types.md +123 -1
- package/docs/product-context-vision-2026-09.md +1 -1
- package/docs/product-vision-2026-09.md +135 -42
- package/docs/product-vision-review-2026-09.md +277 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +19 -4
- package/skills/pmx-canvas/evals/evals.json +46 -0
- package/skills/pmx-canvas/references/full-reference.md +1 -1
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/src/cli/agent.ts +5 -0
- package/src/cli/commands/nodes.ts +10 -0
- package/src/cli/commands/record.ts +218 -0
- package/src/cli/index.ts +2 -0
- package/src/client/App.tsx +7 -1
- package/src/client/canvas/AgentPresenceLayer.tsx +1 -1
- package/src/client/canvas/CanvasNode.tsx +8 -0
- package/src/client/canvas/FeedbackDialog.tsx +86 -0
- package/src/client/canvas/IntentLayer.tsx +9 -2
- package/src/client/canvas/Presentation.tsx +108 -0
- package/src/client/canvas/ToolRail.tsx +225 -189
- package/src/client/canvas/TopBar.tsx +28 -2
- package/src/client/canvas/use-node-drag.ts +8 -3
- package/src/client/mermaid-entry.ts +39 -0
- package/src/client/nodes/HtmlNode.tsx +12 -13
- package/src/client/nodes/McpAppNode.tsx +105 -13
- package/src/client/nodes/MermaidNode.tsx +3 -2
- package/src/client/state/intent-store.ts +5 -1
- package/src/client/state/presentation.ts +7 -0
- package/src/client/state/session-store.ts +3 -0
- package/src/client/state/sse-bridge.ts +3 -0
- package/src/client/theme/global.css +90 -4
- package/src/json-render/server.ts +29 -0
- package/src/server/agent-presence.ts +15 -9
- package/src/server/canvas-db.ts +15 -0
- package/src/server/canvas-operations.ts +115 -22
- package/src/server/canvas-schema.ts +10 -0
- package/src/server/canvas-state.ts +39 -4
- package/src/server/canvas-validation.ts +9 -1
- package/src/server/demo-state.json +11 -7
- package/src/server/html-primitives.ts +199 -91
- package/src/server/html-surface.ts +8 -3
- package/src/server/index.ts +12 -0
- package/src/server/operations/composites.ts +3 -1
- package/src/server/operations/ops/app.ts +10 -0
- package/src/server/operations/ops/batch.ts +12 -3
- package/src/server/operations/ops/groups.ts +14 -7
- package/src/server/operations/ops/json-render.ts +37 -5
- package/src/server/operations/ops/nodes.ts +42 -16
- package/src/server/operations/ops/viewport.ts +32 -0
- package/src/server/operations/registry.ts +6 -0
- package/src/server/scope-fence.ts +1 -1
- package/src/server/server.ts +9 -6
- package/src/server/web-artifacts.ts +9 -2
- package/src/server/workboard.ts +3 -3
- package/src/shared/tour.ts +87 -0
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
emptyStreamingSpec,
|
|
34
34
|
GRAPH_NODE_SIZE,
|
|
35
35
|
inferJsonRenderNodeTitle,
|
|
36
|
+
jsonRenderFormWarnings,
|
|
36
37
|
JSON_RENDER_NODE_SIZE,
|
|
37
38
|
normalizeAndValidateJsonRenderSpec,
|
|
38
39
|
type GraphNodeInput,
|
|
@@ -118,6 +119,29 @@ export const MCP_APP_NODE_DEFAULT_SIZE = { width: 960, height: 600 };
|
|
|
118
119
|
export const IMAGE_NODE_DEFAULT_SIZE = { width: 480, height: 360 };
|
|
119
120
|
export const LEDGER_NODE_DEFAULT_SIZE = { width: 420, height: 280 };
|
|
120
121
|
|
|
122
|
+
export interface CanvasSizeAdjustment {
|
|
123
|
+
requested: { width: number | null; height: number | null };
|
|
124
|
+
applied: { width: number; height: number };
|
|
125
|
+
reason: 'defaulted' | 'clamped-to-minimum' | 'defaulted-and-clamped-to-minimum' | 'fit-to-children';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function describeCreateSize(
|
|
129
|
+
requestedWidth: number | undefined,
|
|
130
|
+
requestedHeight: number | undefined,
|
|
131
|
+
resolvedWidth: number,
|
|
132
|
+
resolvedHeight: number,
|
|
133
|
+
applied: { width: number; height: number },
|
|
134
|
+
): CanvasSizeAdjustment | undefined {
|
|
135
|
+
const defaulted = requestedWidth === undefined || requestedHeight === undefined;
|
|
136
|
+
const clamped = applied.width !== resolvedWidth || applied.height !== resolvedHeight;
|
|
137
|
+
if (!defaulted && !clamped) return undefined;
|
|
138
|
+
return {
|
|
139
|
+
requested: { width: requestedWidth ?? null, height: requestedHeight ?? null },
|
|
140
|
+
applied,
|
|
141
|
+
reason: defaulted && clamped ? 'defaulted-and-clamped-to-minimum' : clamped ? 'clamped-to-minimum' : 'defaulted',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
121
145
|
// Per-type minimum creation sizes + clamp live in canvas-validation.ts
|
|
122
146
|
// (NODE_MIN_CREATE_SIZES / clampCreateNodeSize) — shared with the validate
|
|
123
147
|
// surface's sizeWarnings.
|
|
@@ -923,17 +947,15 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
|
|
|
923
947
|
id: string;
|
|
924
948
|
node: CanvasNodeState;
|
|
925
949
|
needsCodeGraphRecompute: boolean;
|
|
950
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
926
951
|
} {
|
|
927
952
|
if (input.type === 'json-render' || input.type === 'graph') {
|
|
928
953
|
throw new Error(`Use the dedicated ${input.type} node APIs for structured viewer nodes.`);
|
|
929
954
|
}
|
|
930
955
|
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
input.height ?? input.defaultHeight ?? 600,
|
|
935
|
-
input.strictSize,
|
|
936
|
-
);
|
|
956
|
+
const resolvedWidth = input.width ?? input.defaultWidth ?? 720;
|
|
957
|
+
const resolvedHeight = input.height ?? input.defaultHeight ?? 600;
|
|
958
|
+
const { width, height } = clampCreateNodeSize(input.type, resolvedWidth, resolvedHeight, input.strictSize);
|
|
937
959
|
const position =
|
|
938
960
|
input.x !== undefined && input.y !== undefined
|
|
939
961
|
? { x: input.x, y: input.y }
|
|
@@ -959,7 +981,16 @@ export function addCanvasNode(input: CanvasAddNodeInput): {
|
|
|
959
981
|
watchFileForNode(id, filePath);
|
|
960
982
|
}
|
|
961
983
|
|
|
962
|
-
|
|
984
|
+
const sizeAdjustment = describeCreateSize(input.width, input.height, resolvedWidth, resolvedHeight, {
|
|
985
|
+
width,
|
|
986
|
+
height,
|
|
987
|
+
});
|
|
988
|
+
return {
|
|
989
|
+
id,
|
|
990
|
+
node: storedNode,
|
|
991
|
+
needsCodeGraphRecompute: input.type === 'file',
|
|
992
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
993
|
+
};
|
|
963
994
|
}
|
|
964
995
|
|
|
965
996
|
export function resolveCanvasNode(nodeRef: CanvasNodeLookupInput):
|
|
@@ -1415,7 +1446,8 @@ export function saveCanvasSnapshotWithReuse(name: string): { snapshot: CanvasSna
|
|
|
1415
1446
|
diff.removedNodes.length === 0 &&
|
|
1416
1447
|
diff.modifiedNodes.length === 0 &&
|
|
1417
1448
|
diff.addedEdges.length === 0 &&
|
|
1418
|
-
diff.removedEdges.length === 0
|
|
1449
|
+
diff.removedEdges.length === 0 &&
|
|
1450
|
+
JSON.stringify(newestData.tour) === JSON.stringify(canvasState.getTour());
|
|
1419
1451
|
if (identical) return { snapshot: newest, reused: true };
|
|
1420
1452
|
}
|
|
1421
1453
|
}
|
|
@@ -1508,7 +1540,11 @@ export function removeCanvasEdge(id: string): { removed: boolean } {
|
|
|
1508
1540
|
return { removed: canvasState.removeEdge(id) };
|
|
1509
1541
|
}
|
|
1510
1542
|
|
|
1511
|
-
export function createCanvasGroup(input: CanvasCreateGroupInput): {
|
|
1543
|
+
export function createCanvasGroup(input: CanvasCreateGroupInput): {
|
|
1544
|
+
id: string;
|
|
1545
|
+
node: CanvasNodeState;
|
|
1546
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
1547
|
+
} {
|
|
1512
1548
|
let x = input.x;
|
|
1513
1549
|
let y = input.y;
|
|
1514
1550
|
let width = input.width ?? 600;
|
|
@@ -1566,22 +1602,42 @@ export function createCanvasGroup(input: CanvasCreateGroupInput): { id: string;
|
|
|
1566
1602
|
if (!node) {
|
|
1567
1603
|
throw new Error(`Group "${id}" was not created.`);
|
|
1568
1604
|
}
|
|
1569
|
-
|
|
1605
|
+
const adjusted = input.width !== node.size.width || input.height !== node.size.height;
|
|
1606
|
+
const fitted = childIds.length > 0 && ((input.x === undefined && input.y === undefined) || !explicitFrame);
|
|
1607
|
+
return {
|
|
1608
|
+
id,
|
|
1609
|
+
node,
|
|
1610
|
+
...(adjusted
|
|
1611
|
+
? {
|
|
1612
|
+
sizeAdjustment: {
|
|
1613
|
+
requested: { width: input.width ?? null, height: input.height ?? null },
|
|
1614
|
+
applied: node.size,
|
|
1615
|
+
reason: fitted ? ('fit-to-children' as const) : ('defaulted' as const),
|
|
1616
|
+
},
|
|
1617
|
+
}
|
|
1618
|
+
: {}),
|
|
1619
|
+
};
|
|
1570
1620
|
}
|
|
1571
1621
|
|
|
1572
1622
|
export function groupCanvasNodes(
|
|
1573
1623
|
groupId: string,
|
|
1574
1624
|
childIds: string[],
|
|
1575
1625
|
options: { childLayout?: CanvasArrangeMode } = {},
|
|
1576
|
-
): { ok: boolean } {
|
|
1626
|
+
): { ok: boolean; sizeAdjustment?: CanvasSizeAdjustment } {
|
|
1627
|
+
const before = canvasState.getNode(groupId)?.size;
|
|
1628
|
+
const ok = canvasState.groupNodes(groupId, childIds, {
|
|
1629
|
+
// Preserve existing child positions unless an explicit layout is asked
|
|
1630
|
+
// for — matching createCanvasGroup and the batch group.add path. Without
|
|
1631
|
+
// this, grouping silently auto-packs the nodes into a grid.
|
|
1632
|
+
preservePositions: options.childLayout === undefined,
|
|
1633
|
+
...(options.childLayout ? { layout: options.childLayout } : {}),
|
|
1634
|
+
});
|
|
1635
|
+
const applied = canvasState.getNode(groupId)?.size;
|
|
1577
1636
|
return {
|
|
1578
|
-
ok
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
preservePositions: options.childLayout === undefined,
|
|
1583
|
-
...(options.childLayout ? { layout: options.childLayout } : {}),
|
|
1584
|
-
}),
|
|
1637
|
+
ok,
|
|
1638
|
+
...(before && applied && (before.width !== applied.width || before.height !== applied.height)
|
|
1639
|
+
? { sizeAdjustment: { requested: before, applied, reason: 'fit-to-children' as const } }
|
|
1640
|
+
: {}),
|
|
1585
1641
|
};
|
|
1586
1642
|
}
|
|
1587
1643
|
|
|
@@ -1600,6 +1656,8 @@ export function createCanvasJsonRenderNode(input: JsonRenderNodeInput): {
|
|
|
1600
1656
|
url: string;
|
|
1601
1657
|
spec: JsonRenderSpec;
|
|
1602
1658
|
node: CanvasNodeState;
|
|
1659
|
+
warnings: string[];
|
|
1660
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
1603
1661
|
} {
|
|
1604
1662
|
const spec = normalizeAndValidateJsonRenderSpec(input.spec);
|
|
1605
1663
|
const { width, height } = clampCreateNodeSize(
|
|
@@ -1628,7 +1686,24 @@ export function createCanvasJsonRenderNode(input: JsonRenderNodeInput): {
|
|
|
1628
1686
|
};
|
|
1629
1687
|
|
|
1630
1688
|
canvasState.addJsonRenderNode(node);
|
|
1631
|
-
|
|
1689
|
+
const sizeAdjustment = describeCreateSize(
|
|
1690
|
+
input.width,
|
|
1691
|
+
input.height,
|
|
1692
|
+
input.width ?? JSON_RENDER_NODE_SIZE.width,
|
|
1693
|
+
input.height ?? JSON_RENDER_NODE_SIZE.height,
|
|
1694
|
+
{
|
|
1695
|
+
width,
|
|
1696
|
+
height,
|
|
1697
|
+
},
|
|
1698
|
+
);
|
|
1699
|
+
return {
|
|
1700
|
+
id,
|
|
1701
|
+
url: String(node.data.url),
|
|
1702
|
+
spec,
|
|
1703
|
+
node,
|
|
1704
|
+
warnings: jsonRenderFormWarnings(spec),
|
|
1705
|
+
...(sizeAdjustment ? { sizeAdjustment } : {}),
|
|
1706
|
+
};
|
|
1632
1707
|
}
|
|
1633
1708
|
|
|
1634
1709
|
/**
|
|
@@ -1643,7 +1718,7 @@ export function createCanvasStreamingJsonRenderNode(input: {
|
|
|
1643
1718
|
width?: number;
|
|
1644
1719
|
height?: number;
|
|
1645
1720
|
strictSize?: boolean;
|
|
1646
|
-
}): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState } {
|
|
1721
|
+
}): { id: string; url: string; spec: JsonRenderSpec; node: CanvasNodeState; sizeAdjustment?: CanvasSizeAdjustment } {
|
|
1647
1722
|
const spec = emptyStreamingSpec();
|
|
1648
1723
|
const { width, height } = clampCreateNodeSize(
|
|
1649
1724
|
'json-render',
|
|
@@ -1673,7 +1748,17 @@ export function createCanvasStreamingJsonRenderNode(input: {
|
|
|
1673
1748
|
};
|
|
1674
1749
|
|
|
1675
1750
|
canvasState.addJsonRenderNode(node);
|
|
1676
|
-
|
|
1751
|
+
const sizeAdjustment = describeCreateSize(
|
|
1752
|
+
input.width,
|
|
1753
|
+
input.height,
|
|
1754
|
+
input.width ?? JSON_RENDER_NODE_SIZE.width,
|
|
1755
|
+
input.height ?? JSON_RENDER_NODE_SIZE.height,
|
|
1756
|
+
{
|
|
1757
|
+
width,
|
|
1758
|
+
height,
|
|
1759
|
+
},
|
|
1760
|
+
);
|
|
1761
|
+
return { id, url: String(node.data.url), spec, node, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
|
|
1677
1762
|
}
|
|
1678
1763
|
|
|
1679
1764
|
/**
|
|
@@ -1717,6 +1802,7 @@ export function createCanvasGraphNode(input: GraphNodeInput): {
|
|
|
1717
1802
|
url: string;
|
|
1718
1803
|
spec: JsonRenderSpec;
|
|
1719
1804
|
node: CanvasNodeState;
|
|
1805
|
+
sizeAdjustment?: CanvasSizeAdjustment;
|
|
1720
1806
|
} {
|
|
1721
1807
|
const title = input.title?.trim() || 'Graph';
|
|
1722
1808
|
const spec = buildGraphSpec(input);
|
|
@@ -1747,7 +1833,14 @@ export function createCanvasGraphNode(input: GraphNodeInput): {
|
|
|
1747
1833
|
};
|
|
1748
1834
|
|
|
1749
1835
|
canvasState.addGraphNode(node);
|
|
1750
|
-
|
|
1836
|
+
const sizeAdjustment = describeCreateSize(
|
|
1837
|
+
input.width,
|
|
1838
|
+
input.heightPx,
|
|
1839
|
+
input.width ?? GRAPH_NODE_SIZE.width,
|
|
1840
|
+
input.heightPx ?? GRAPH_NODE_SIZE.height,
|
|
1841
|
+
{ width, height },
|
|
1842
|
+
);
|
|
1843
|
+
return { id, url: String(node.data.url), spec, node, ...(sizeAdjustment ? { sizeAdjustment } : {}) };
|
|
1751
1844
|
}
|
|
1752
1845
|
|
|
1753
1846
|
/**
|
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { describeJsonRenderCatalog, type JsonRenderComponentDescriptor } from '../json-render/catalog.js';
|
|
4
4
|
import {
|
|
5
5
|
buildGraphSpec,
|
|
6
|
+
jsonRenderFormWarnings,
|
|
6
7
|
normalizeAndValidateJsonRenderSpec,
|
|
7
8
|
normalizeGraphType,
|
|
8
9
|
type GraphNodeInput,
|
|
@@ -38,6 +39,7 @@ export interface StructuredValidationResult {
|
|
|
38
39
|
ok: true;
|
|
39
40
|
type: 'json-render' | 'graph' | 'html-primitive';
|
|
40
41
|
normalizedSpec?: JsonRenderSpec;
|
|
42
|
+
warnings?: string[];
|
|
41
43
|
normalizedPrimitive?: {
|
|
42
44
|
kind: string;
|
|
43
45
|
title: string;
|
|
@@ -235,6 +237,13 @@ const CANVAS_CREATE_TYPES: CanvasCreateTypeSchema[] = [
|
|
|
235
237
|
fields: [
|
|
236
238
|
{ name: 'title', type: 'string', required: false, description: 'Optional node title.' },
|
|
237
239
|
{ name: 'content', type: 'string', required: false, description: 'Mermaid diagram source text.' },
|
|
240
|
+
{
|
|
241
|
+
name: 'data.fit',
|
|
242
|
+
type: '"contain" | "none"',
|
|
243
|
+
required: false,
|
|
244
|
+
description:
|
|
245
|
+
'Default contain: shrink overflowing diagrams proportionally, without enlarging. none: 100% with scrolling. Open as site always uses 100%. CLI: --fit contain|none.',
|
|
246
|
+
},
|
|
238
247
|
],
|
|
239
248
|
example: {
|
|
240
249
|
type: 'mermaid',
|
|
@@ -1013,6 +1022,7 @@ export function validateStructuredCanvasPayload(input: {
|
|
|
1013
1022
|
ok: true,
|
|
1014
1023
|
type: 'json-render',
|
|
1015
1024
|
normalizedSpec,
|
|
1025
|
+
warnings: jsonRenderFormWarnings(normalizedSpec),
|
|
1016
1026
|
summary: {
|
|
1017
1027
|
root: normalizedSpec.root,
|
|
1018
1028
|
elementCount: Object.keys(normalizedSpec.elements).length,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
|
+
import { tourSchema, type Tour } from '../shared/tour.js';
|
|
16
17
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, unlinkSync } from 'node:fs';
|
|
17
18
|
import { isAbsolute, join, dirname, relative } from 'node:path';
|
|
18
19
|
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
@@ -223,6 +224,7 @@ export interface CanvasAnnotation {
|
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
export interface CanvasLayout {
|
|
227
|
+
tour?: Tour;
|
|
226
228
|
viewport: ViewportState;
|
|
227
229
|
theme: CanvasTheme;
|
|
228
230
|
nodes: CanvasNodeState[];
|
|
@@ -333,6 +335,7 @@ class CanvasStateManager {
|
|
|
333
335
|
private edges = new Map<string, CanvasEdge>();
|
|
334
336
|
private annotations = new Map<string, CanvasAnnotation>();
|
|
335
337
|
private _viewport: ViewportState = { x: 0, y: 0, scale: 1 };
|
|
338
|
+
private _tour: Tour | undefined;
|
|
336
339
|
private _theme: CanvasTheme = 'dark';
|
|
337
340
|
private _contextPinnedNodeIds = new Set<string>();
|
|
338
341
|
private _workspaceRoot = process.cwd();
|
|
@@ -899,6 +902,7 @@ class CanvasStateManager {
|
|
|
899
902
|
const payload = this.externalizePersistedStateBlobs({
|
|
900
903
|
version: 1,
|
|
901
904
|
theme: this._theme,
|
|
905
|
+
tour: this._tour,
|
|
902
906
|
viewport: this._viewport,
|
|
903
907
|
nodes: Array.from(this.nodes.values()),
|
|
904
908
|
edges: Array.from(this.edges.values()),
|
|
@@ -957,6 +961,7 @@ class CanvasStateManager {
|
|
|
957
961
|
}
|
|
958
962
|
|
|
959
963
|
private applyPersistedState(state: PersistedCanvasState): void {
|
|
964
|
+
this._tour = state.tour ? structuredClone(state.tour) : undefined;
|
|
960
965
|
this.nodes.clear();
|
|
961
966
|
this.edges.clear();
|
|
962
967
|
this.annotations.clear();
|
|
@@ -1091,7 +1096,7 @@ class CanvasStateManager {
|
|
|
1091
1096
|
contextPins: Array.from(this._contextPinnedNodeIds),
|
|
1092
1097
|
ax: this.getAxState(),
|
|
1093
1098
|
});
|
|
1094
|
-
saveSnapshotToDB(this._db, snapshot, payload);
|
|
1099
|
+
saveSnapshotToDB(this._db, snapshot, { ...payload, tour: this._tour });
|
|
1095
1100
|
snapshot.nodeCount = payload.nodes.length;
|
|
1096
1101
|
snapshot.edgeCount = payload.edges.length;
|
|
1097
1102
|
return snapshot;
|
|
@@ -1176,6 +1181,7 @@ class CanvasStateManager {
|
|
|
1176
1181
|
|
|
1177
1182
|
const previousState: PersistedCanvasState = this.externalizePersistedStateBlobs({
|
|
1178
1183
|
version: 1,
|
|
1184
|
+
tour: this._tour,
|
|
1179
1185
|
theme: this._theme,
|
|
1180
1186
|
viewport: structuredClone(this._viewport),
|
|
1181
1187
|
nodes: Array.from(this.nodes.values(), (node) => structuredClone(node)),
|
|
@@ -1186,6 +1192,7 @@ class CanvasStateManager {
|
|
|
1186
1192
|
});
|
|
1187
1193
|
const nextState: PersistedCanvasState = {
|
|
1188
1194
|
version: 1,
|
|
1195
|
+
tour: resolved.state.tour,
|
|
1189
1196
|
theme: normalizeCanvasTheme(resolved.state.theme, this._theme),
|
|
1190
1197
|
viewport: structuredClone(resolved.state.viewport),
|
|
1191
1198
|
nodes: Array.isArray(resolved.state.nodes) ? resolved.state.nodes.map((node) => structuredClone(node)) : [],
|
|
@@ -1233,9 +1240,13 @@ class CanvasStateManager {
|
|
|
1233
1240
|
}
|
|
1234
1241
|
|
|
1235
1242
|
/** Read a snapshot's data without restoring it (for diff). Resolves by ID or name. */
|
|
1236
|
-
getSnapshotData(
|
|
1237
|
-
|
|
1238
|
-
|
|
1243
|
+
getSnapshotData(idOrName: string): {
|
|
1244
|
+
name: string;
|
|
1245
|
+
nodes: CanvasNodeState[];
|
|
1246
|
+
edges: CanvasEdge[];
|
|
1247
|
+
annotations: CanvasAnnotation[];
|
|
1248
|
+
tour?: Tour;
|
|
1249
|
+
} | null {
|
|
1239
1250
|
const resolved = this.readResolvedSnapshot(idOrName);
|
|
1240
1251
|
if (!resolved) return null;
|
|
1241
1252
|
const state = {
|
|
@@ -1246,6 +1257,7 @@ class CanvasStateManager {
|
|
|
1246
1257
|
};
|
|
1247
1258
|
return {
|
|
1248
1259
|
name: resolved.snapshot.name,
|
|
1260
|
+
...(state.tour ? { tour: structuredClone(state.tour) } : {}),
|
|
1249
1261
|
nodes: Array.isArray(state.nodes) ? state.nodes.map((node) => structuredClone(node)) : [],
|
|
1250
1262
|
edges: Array.isArray(state.edges) ? state.edges.map((edge) => structuredClone(edge)) : [],
|
|
1251
1263
|
annotations: Array.isArray(state.annotations)
|
|
@@ -1620,6 +1632,7 @@ class CanvasStateManager {
|
|
|
1620
1632
|
|
|
1621
1633
|
getLayout(): CanvasLayout {
|
|
1622
1634
|
return {
|
|
1635
|
+
tour: this.getTour(),
|
|
1623
1636
|
viewport: structuredClone(this._viewport),
|
|
1624
1637
|
theme: this._theme,
|
|
1625
1638
|
nodes: Array.from(this.nodes.values(), (node) => structuredClone(this.nodeForRead(node))),
|
|
@@ -1630,6 +1643,7 @@ class CanvasStateManager {
|
|
|
1630
1643
|
|
|
1631
1644
|
getLayoutForPersistence(): CanvasLayout {
|
|
1632
1645
|
return {
|
|
1646
|
+
tour: this.getTour(),
|
|
1633
1647
|
viewport: structuredClone(this._viewport),
|
|
1634
1648
|
theme: this._theme,
|
|
1635
1649
|
nodes: Array.from(this.nodes.values(), (node) => structuredClone(this.externalizeNodeDataBlobs(node))),
|
|
@@ -1638,6 +1652,24 @@ class CanvasStateManager {
|
|
|
1638
1652
|
};
|
|
1639
1653
|
}
|
|
1640
1654
|
|
|
1655
|
+
getTour(): Tour | undefined {
|
|
1656
|
+
return this._tour ? structuredClone(this._tour) : undefined;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
setTour(tour: Tour | null): void {
|
|
1660
|
+
const previous = this.getTour();
|
|
1661
|
+
this._tour = tour === null ? undefined : tourSchema.parse(tour);
|
|
1662
|
+
const next = this.getTour();
|
|
1663
|
+
this.scheduleSave();
|
|
1664
|
+
this.notifyChange('nodes');
|
|
1665
|
+
this.recordMutation({
|
|
1666
|
+
operationType: 'viewport',
|
|
1667
|
+
description: 'Updated board tour',
|
|
1668
|
+
forward: this.suppressed(() => this.setTour(next ?? null)),
|
|
1669
|
+
inverse: this.suppressed(() => this.setTour(previous ?? null)),
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1641
1673
|
applyUpdates(updates: CanvasNodeUpdate[], options: ApplyUpdatesOptions = {}): { applied: number; skipped: number } {
|
|
1642
1674
|
let applied = 0;
|
|
1643
1675
|
let skipped = 0;
|
|
@@ -2246,6 +2278,8 @@ class CanvasStateManager {
|
|
|
2246
2278
|
}
|
|
2247
2279
|
|
|
2248
2280
|
clear(): void {
|
|
2281
|
+
const oldTour = this.getTour();
|
|
2282
|
+
this._tour = undefined;
|
|
2249
2283
|
const oldNodes = Array.from(this.nodes.values()).map((n) => structuredClone(n));
|
|
2250
2284
|
const oldEdges = Array.from(this.edges.values()).map((e) => structuredClone(e));
|
|
2251
2285
|
const oldAnnotations = Array.from(this.annotations.values()).map((annotation) => structuredClone(annotation));
|
|
@@ -2274,6 +2308,7 @@ class CanvasStateManager {
|
|
|
2274
2308
|
for (const e of oldEdges) this.addEdge(structuredClone(e));
|
|
2275
2309
|
for (const annotation of oldAnnotations) this.addAnnotation(structuredClone(annotation));
|
|
2276
2310
|
this.setContextPins(oldPins);
|
|
2311
|
+
this.setTour(oldTour ?? null);
|
|
2277
2312
|
this.ax.applyPersistedAx(oldAxState);
|
|
2278
2313
|
this.setViewport(oldViewport);
|
|
2279
2314
|
this.notifyChange('ax');
|
|
@@ -36,6 +36,8 @@ export interface CanvasValidationResult {
|
|
|
36
36
|
collisions: CanvasValidationPair[];
|
|
37
37
|
containments: CanvasContainmentIssue[];
|
|
38
38
|
containmentViolations: CanvasContainmentIssue[];
|
|
39
|
+
/** Group frames overlapping nodes that are not members (advisory). */
|
|
40
|
+
groupFrameOverlaps: CanvasValidationPair[];
|
|
39
41
|
missingEdgeEndpoints: Array<{ edgeId: string; from: string; to: string }>;
|
|
40
42
|
/** Nodes below their type's readable minimum (advisory — does not fail `ok`). */
|
|
41
43
|
sizeWarnings: CanvasSizeWarning[];
|
|
@@ -45,6 +47,7 @@ export interface CanvasValidationResult {
|
|
|
45
47
|
collisions: number;
|
|
46
48
|
containments: number;
|
|
47
49
|
containmentViolations: number;
|
|
50
|
+
groupFrameOverlaps: number;
|
|
48
51
|
missingEdgeEndpoints: number;
|
|
49
52
|
sizeWarnings: number;
|
|
50
53
|
};
|
|
@@ -97,6 +100,7 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
97
100
|
const collisions: CanvasValidationPair[] = [];
|
|
98
101
|
const containments: CanvasContainmentIssue[] = [];
|
|
99
102
|
const containmentViolations: CanvasContainmentIssue[] = [];
|
|
103
|
+
const groupFrameOverlaps: CanvasValidationPair[] = [];
|
|
100
104
|
|
|
101
105
|
for (let i = 0; i < layout.nodes.length; i++) {
|
|
102
106
|
const a = layout.nodes[i]!;
|
|
@@ -113,7 +117,9 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
113
117
|
continue;
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
|
|
120
|
+
const overlap = pair(a, b);
|
|
121
|
+
collisions.push(overlap);
|
|
122
|
+
if (a.type === 'group' || b.type === 'group') groupFrameOverlaps.push(overlap);
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
|
|
@@ -150,6 +156,7 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
150
156
|
collisions,
|
|
151
157
|
containments,
|
|
152
158
|
containmentViolations,
|
|
159
|
+
groupFrameOverlaps,
|
|
153
160
|
missingEdgeEndpoints,
|
|
154
161
|
sizeWarnings,
|
|
155
162
|
summary: {
|
|
@@ -158,6 +165,7 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
158
165
|
collisions: collisions.length,
|
|
159
166
|
containments: containments.length,
|
|
160
167
|
containmentViolations: containmentViolations.length,
|
|
168
|
+
groupFrameOverlaps: groupFrameOverlaps.length,
|
|
161
169
|
missingEdgeEndpoints: missingEdgeEndpoints.length,
|
|
162
170
|
sizeWarnings: sizeWarnings.length,
|
|
163
171
|
},
|