pmx-canvas 0.1.9 → 0.1.10
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 +71 -0
- package/dist/canvas/index.js +27 -27
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +92 -92
- package/dist/types/client/canvas/auto-fit.d.ts +1 -1
- package/dist/types/json-render/catalog.d.ts +316 -310
- package/package.json +1 -1
- package/src/cli/agent.ts +26 -4
- package/src/cli/index.ts +1 -1
- package/src/client/canvas/auto-fit.ts +2 -2
- package/src/json-render/catalog.ts +9 -0
- package/src/json-render/renderer/index.css +61 -0
- package/src/json-render/renderer/index.tsx +22 -0
- package/src/json-render/server.ts +0 -11
- package/src/server/canvas-validation.ts +9 -2
- package/src/server/diagram-presets.ts +48 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmx-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Spatial canvas workbench for coding agents — infinite 2D canvas with agent-native CLI, MCP integration, nodes, edges, file watching, and snapshots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server/index.ts",
|
package/src/cli/agent.ts
CHANGED
|
@@ -241,6 +241,14 @@ function optionalPositiveFiniteFlagWithAliases(
|
|
|
241
241
|
return undefined;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
function optionalBooleanFlag(flags: Record<string, string | true>, name: string, hint: string): boolean | undefined {
|
|
245
|
+
const val = flags[name];
|
|
246
|
+
if (val === undefined) return undefined;
|
|
247
|
+
if (val === true || val === 'true') return true;
|
|
248
|
+
if (val === 'false') return false;
|
|
249
|
+
die(`Invalid value for --${name}: ${String(val)}`, hint);
|
|
250
|
+
}
|
|
251
|
+
|
|
244
252
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
245
253
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
246
254
|
}
|
|
@@ -1216,6 +1224,7 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1216
1224
|
'pmx-canvas node update <node-id> --width 840 --height 620',
|
|
1217
1225
|
'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
|
|
1218
1226
|
'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
|
|
1227
|
+
'pmx-canvas node update <node-id> --pinned true',
|
|
1219
1228
|
'pmx-canvas node update <node-id> --lock-arrange',
|
|
1220
1229
|
], async (args) => {
|
|
1221
1230
|
const { positional, flags } = parseFlags(args);
|
|
@@ -1234,6 +1243,17 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1234
1243
|
const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
|
|
1235
1244
|
const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 840');
|
|
1236
1245
|
const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 620');
|
|
1246
|
+
const nodeHeight = optionalPositiveFiniteFlagWithAliases(
|
|
1247
|
+
flags,
|
|
1248
|
+
'Use a positive number, e.g. --node-height 620',
|
|
1249
|
+
'node-height',
|
|
1250
|
+
'nodeHeight',
|
|
1251
|
+
);
|
|
1252
|
+
if (height !== undefined && nodeHeight !== undefined) {
|
|
1253
|
+
die('Use either --height/--node-height, not both.');
|
|
1254
|
+
}
|
|
1255
|
+
const frameHeight = height ?? nodeHeight;
|
|
1256
|
+
const pinned = optionalBooleanFlag(flags, 'pinned', 'Use --pinned true or --pinned false');
|
|
1237
1257
|
if (flags['lock-arrange'] && flags['unlock-arrange']) {
|
|
1238
1258
|
die('Use either --lock-arrange or --unlock-arrange, not both.');
|
|
1239
1259
|
}
|
|
@@ -1243,7 +1263,7 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1243
1263
|
? false
|
|
1244
1264
|
: undefined;
|
|
1245
1265
|
|
|
1246
|
-
if (x !== undefined || y !== undefined || width !== undefined ||
|
|
1266
|
+
if (x !== undefined || y !== undefined || width !== undefined || frameHeight !== undefined || arrangeLocked !== undefined) {
|
|
1247
1267
|
const existing = await api('GET', `/api/canvas/node/${encodeURIComponent(id)}`) as {
|
|
1248
1268
|
position: { x: number; y: number };
|
|
1249
1269
|
size: { width: number; height: number };
|
|
@@ -1257,10 +1277,10 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1257
1277
|
};
|
|
1258
1278
|
}
|
|
1259
1279
|
|
|
1260
|
-
if (width !== undefined ||
|
|
1280
|
+
if (width !== undefined || frameHeight !== undefined) {
|
|
1261
1281
|
body.size = {
|
|
1262
1282
|
width: width ?? existing.size.width,
|
|
1263
|
-
height:
|
|
1283
|
+
height: frameHeight ?? existing.size.height,
|
|
1264
1284
|
};
|
|
1265
1285
|
}
|
|
1266
1286
|
|
|
@@ -1269,10 +1289,12 @@ cmd('node update', 'Update a node by ID', [
|
|
|
1269
1289
|
}
|
|
1270
1290
|
}
|
|
1271
1291
|
|
|
1292
|
+
if (pinned !== undefined) body.pinned = pinned;
|
|
1293
|
+
|
|
1272
1294
|
if (Object.keys(body).length === 0) {
|
|
1273
1295
|
die(
|
|
1274
1296
|
'No updates specified',
|
|
1275
|
-
'Use --title, --content, --x, --y, --width, --height, --lock-arrange, --unlock-arrange, or --stdin',
|
|
1297
|
+
'Use --title, --content, --x, --y, --width, --height, --pinned, --lock-arrange, --unlock-arrange, or --stdin',
|
|
1276
1298
|
);
|
|
1277
1299
|
}
|
|
1278
1300
|
|
package/src/cli/index.ts
CHANGED
|
@@ -30,7 +30,7 @@ if (args.includes('--version') || args.includes('-v')) {
|
|
|
30
30
|
// If first arg is a known subcommand (not a --flag), route to the agent CLI.
|
|
31
31
|
const AGENT_COMMANDS = new Set([
|
|
32
32
|
'node', 'edge', 'search', 'layout', 'status', 'arrange', 'focus',
|
|
33
|
-
'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
|
|
33
|
+
'fit', 'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
|
|
34
34
|
'clear', 'code-graph', 'spatial', 'watch', 'web-artifact', 'external-app', 'graph', 'batch', 'validate', 'serve',
|
|
35
35
|
]);
|
|
36
36
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { CanvasNodeState } from '../types';
|
|
2
2
|
|
|
3
|
-
export const AUTO_FIT_MAX_HEIGHT = 600;
|
|
4
3
|
export const AUTO_FIT_TITLEBAR_HEIGHT = 37;
|
|
4
|
+
export const AUTO_FIT_MAX_HEIGHT = 600;
|
|
5
5
|
|
|
6
6
|
function isExtAppNode(node: CanvasNodeState): boolean {
|
|
7
7
|
return node.type === 'mcp-app' && node.data.mode === 'ext-app';
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function hasExplicitStructuredFrame(node: CanvasNodeState): boolean {
|
|
11
|
-
return
|
|
11
|
+
return node.type === 'graph' || node.type === 'json-render';
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function shouldAutoFitNode(node: CanvasNodeState): boolean {
|
|
@@ -7,13 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { defineCatalog } from '@json-render/core';
|
|
10
|
+
import { z } from 'zod';
|
|
10
11
|
import { schema } from './schema.js';
|
|
11
12
|
import { shadcnComponentDefinitions } from '@json-render/shadcn/catalog';
|
|
12
13
|
import { chartComponentDefinitions } from './charts/definitions';
|
|
13
14
|
import { extraChartComponentDefinitions } from './charts/extra-definitions';
|
|
14
15
|
|
|
16
|
+
const badgeDefinition = shadcnComponentDefinitions.Badge;
|
|
17
|
+
|
|
15
18
|
export const allComponentDefinitions = {
|
|
16
19
|
...shadcnComponentDefinitions,
|
|
20
|
+
Badge: {
|
|
21
|
+
...badgeDefinition,
|
|
22
|
+
props: badgeDefinition.props.extend({
|
|
23
|
+
variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'success', 'info', 'warning', 'error', 'danger']).nullable(),
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
17
26
|
...chartComponentDefinitions,
|
|
18
27
|
...extraChartComponentDefinitions,
|
|
19
28
|
};
|
|
@@ -143,6 +143,67 @@ button {
|
|
|
143
143
|
cursor: pointer;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
.pmx-badge {
|
|
147
|
+
display: inline-flex;
|
|
148
|
+
width: fit-content;
|
|
149
|
+
align-items: center;
|
|
150
|
+
justify-content: center;
|
|
151
|
+
gap: 0.25rem;
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
white-space: nowrap;
|
|
154
|
+
border: 1px solid transparent;
|
|
155
|
+
border-radius: 9999px;
|
|
156
|
+
padding: 0.125rem 0.5rem;
|
|
157
|
+
font-size: 0.75rem;
|
|
158
|
+
font-weight: 500;
|
|
159
|
+
line-height: 1.25rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.pmx-badge--default {
|
|
163
|
+
background: var(--primary);
|
|
164
|
+
color: var(--primary-foreground);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.pmx-badge--secondary {
|
|
168
|
+
background: var(--secondary);
|
|
169
|
+
color: var(--secondary-foreground);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.pmx-badge--destructive {
|
|
173
|
+
background: var(--destructive);
|
|
174
|
+
color: var(--destructive-foreground);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.pmx-badge--outline {
|
|
178
|
+
border-color: var(--border);
|
|
179
|
+
color: var(--foreground);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.pmx-badge--success {
|
|
183
|
+
border-color: color-mix(in oklch, var(--chart-2) 45%, transparent);
|
|
184
|
+
background: color-mix(in oklch, var(--chart-2) 16%, transparent);
|
|
185
|
+
color: var(--chart-2);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.pmx-badge--info {
|
|
189
|
+
border-color: color-mix(in oklch, var(--chart-1) 45%, transparent);
|
|
190
|
+
background: color-mix(in oklch, var(--chart-1) 14%, transparent);
|
|
191
|
+
color: var(--chart-1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.pmx-badge--warning {
|
|
195
|
+
border-color: color-mix(in oklch, var(--chart-3) 50%, transparent);
|
|
196
|
+
background: color-mix(in oklch, var(--chart-3) 18%, transparent);
|
|
197
|
+
color: var(--chart-3);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.pmx-badge--error,
|
|
201
|
+
.pmx-badge--danger {
|
|
202
|
+
border-color: color-mix(in oklch, var(--destructive) 55%, transparent);
|
|
203
|
+
background: color-mix(in oklch, var(--destructive) 18%, transparent);
|
|
204
|
+
color: var(--destructive);
|
|
205
|
+
}
|
|
206
|
+
|
|
146
207
|
/* -- Chart components -- */
|
|
147
208
|
|
|
148
209
|
.pmx-chart {
|
|
@@ -15,9 +15,31 @@ import { catalog } from '../catalog';
|
|
|
15
15
|
import { chartComponents } from '../charts/components';
|
|
16
16
|
import { extraChartComponents } from '../charts/extra-components';
|
|
17
17
|
|
|
18
|
+
type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'info' | 'warning' | 'error' | 'danger';
|
|
19
|
+
type BadgeProps = {
|
|
20
|
+
text: string;
|
|
21
|
+
variant?: BadgeVariant | null;
|
|
22
|
+
className?: string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function Badge({ props }: { props: BadgeProps }) {
|
|
26
|
+
const variant = props.variant;
|
|
27
|
+
const resolvedVariant = variant ?? 'default';
|
|
28
|
+
return (
|
|
29
|
+
<span
|
|
30
|
+
data-slot="badge"
|
|
31
|
+
data-variant={resolvedVariant}
|
|
32
|
+
className={`pmx-badge pmx-badge--${resolvedVariant}`}
|
|
33
|
+
>
|
|
34
|
+
{props.text}
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
const { registry } = defineRegistry(catalog as never, {
|
|
19
40
|
components: {
|
|
20
41
|
...shadcnComponents,
|
|
42
|
+
Badge,
|
|
21
43
|
...chartComponents,
|
|
22
44
|
...extraChartComponents,
|
|
23
45
|
} as never,
|
|
@@ -262,14 +262,6 @@ function normalizeButtonVariant(value: unknown): unknown {
|
|
|
262
262
|
return value;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
function normalizeBadgeVariant(value: unknown): unknown {
|
|
266
|
-
if (value === 'success') return 'default';
|
|
267
|
-
if (value === 'info') return 'secondary';
|
|
268
|
-
if (value === 'warning') return 'outline';
|
|
269
|
-
if (value === 'error' || value === 'danger') return 'destructive';
|
|
270
|
-
return value;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
265
|
function deriveElementName(elementKey: string): string {
|
|
274
266
|
const normalized = elementKey.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
275
267
|
return normalized || 'field';
|
|
@@ -344,9 +336,6 @@ function normalizeElementProps(
|
|
|
344
336
|
if ('label' in props) {
|
|
345
337
|
delete props.label;
|
|
346
338
|
}
|
|
347
|
-
if ('variant' in props) {
|
|
348
|
-
props.variant = normalizeBadgeVariant(props.variant);
|
|
349
|
-
}
|
|
350
339
|
}
|
|
351
340
|
|
|
352
341
|
if (type === 'Select' || type === 'Radio') {
|
|
@@ -49,6 +49,13 @@ function fullyContains(group: CanvasNodeState, child: CanvasNodeState): boolean
|
|
|
49
49
|
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function isGroupChildPair(group: CanvasNodeState, child: CanvasNodeState): boolean {
|
|
53
|
+
if (group.type !== 'group') return false;
|
|
54
|
+
if (child.data.parentGroup === group.id) return true;
|
|
55
|
+
const children = group.data.children;
|
|
56
|
+
return Array.isArray(children) && children.includes(child.id);
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
function pair(a: CanvasNodeState, b: CanvasNodeState): CanvasValidationPair {
|
|
53
60
|
return {
|
|
54
61
|
aId: a.id,
|
|
@@ -78,11 +85,11 @@ export function validateCanvasLayout(layout: CanvasLayout): CanvasValidationResu
|
|
|
78
85
|
const b = layout.nodes[j]!;
|
|
79
86
|
if (!overlaps(a, b)) continue;
|
|
80
87
|
|
|
81
|
-
if (a
|
|
88
|
+
if (isGroupChildPair(a, b)) {
|
|
82
89
|
(fullyContains(a, b) ? containments : containmentViolations).push(containment(a, b));
|
|
83
90
|
continue;
|
|
84
91
|
}
|
|
85
|
-
if (b
|
|
92
|
+
if (isGroupChildPair(b, a)) {
|
|
86
93
|
(fullyContains(b, a) ? containments : containmentViolations).push(containment(b, a));
|
|
87
94
|
continue;
|
|
88
95
|
}
|
|
@@ -115,6 +115,52 @@ function elementHasCameraUpdate(elements: Array<Record<string, unknown>>): boole
|
|
|
115
115
|
return elements.some((element) => element.type === 'cameraUpdate');
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function normalizeExcalidrawBoundText(elements: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
|
119
|
+
const elementsById = new Map<string, Record<string, unknown>>();
|
|
120
|
+
for (const element of elements) {
|
|
121
|
+
if (typeof element.id === 'string') elementsById.set(element.id, element);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let changed = false;
|
|
125
|
+
const boundElementIdsByContainer = new Map<string, Set<string>>();
|
|
126
|
+
|
|
127
|
+
for (const element of elements) {
|
|
128
|
+
if (element.type !== 'text' || typeof element.id !== 'string' || typeof element.containerId !== 'string') continue;
|
|
129
|
+
if (!elementsById.has(element.containerId)) continue;
|
|
130
|
+
const ids = boundElementIdsByContainer.get(element.containerId) ?? new Set<string>();
|
|
131
|
+
ids.add(element.id);
|
|
132
|
+
boundElementIdsByContainer.set(element.containerId, ids);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const normalized = elements.map((element) => {
|
|
136
|
+
if (typeof element.id !== 'string') return element;
|
|
137
|
+
const boundTextIds = boundElementIdsByContainer.get(element.id);
|
|
138
|
+
if (!boundTextIds || boundTextIds.size === 0) return element;
|
|
139
|
+
|
|
140
|
+
const existing = Array.isArray(element.boundElements)
|
|
141
|
+
? element.boundElements.filter(isRecord)
|
|
142
|
+
: [];
|
|
143
|
+
const existingTextIds = new Set(
|
|
144
|
+
existing
|
|
145
|
+
.filter((boundElement) => boundElement.type === 'text' && typeof boundElement.id === 'string')
|
|
146
|
+
.map((boundElement) => boundElement.id as string),
|
|
147
|
+
);
|
|
148
|
+
const missing = [...boundTextIds].filter((id) => !existingTextIds.has(id));
|
|
149
|
+
if (missing.length === 0) return element;
|
|
150
|
+
|
|
151
|
+
changed = true;
|
|
152
|
+
return {
|
|
153
|
+
...element,
|
|
154
|
+
boundElements: [
|
|
155
|
+
...existing,
|
|
156
|
+
...missing.map((id) => ({ type: 'text', id })),
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return changed ? normalized : elements;
|
|
162
|
+
}
|
|
163
|
+
|
|
118
164
|
function resolveExcalidrawCameraSize(width: number, height: number): { width: number; height: number } {
|
|
119
165
|
const requiredWidth = Math.max(EXCALIDRAW_MIN_CAMERA_WIDTH, width);
|
|
120
166
|
const requiredHeight = Math.max(EXCALIDRAW_MIN_CAMERA_HEIGHT, height);
|
|
@@ -211,13 +257,13 @@ export function normalizeExcalidrawElements(elements: unknown): string {
|
|
|
211
257
|
export function normalizeExcalidrawElementsForToolInput(elements: unknown): string {
|
|
212
258
|
const parsed = parseExcalidrawElements(elements);
|
|
213
259
|
const seeded = parsed.length > 0 ? parsed : [...DEFAULT_EXCALIDRAW_ELEMENTS];
|
|
214
|
-
return JSON.stringify(withInferredCameraUpdate(seeded));
|
|
260
|
+
return JSON.stringify(withInferredCameraUpdate(normalizeExcalidrawBoundText(seeded)));
|
|
215
261
|
}
|
|
216
262
|
|
|
217
263
|
export function normalizeExcalidrawCheckpointDataForToolInput(data: unknown): string | null {
|
|
218
264
|
const elements = parseExcalidrawCheckpointElements(data);
|
|
219
265
|
|
|
220
|
-
return elements ? JSON.stringify(withInferredCameraUpdate(elements)) : null;
|
|
266
|
+
return elements ? JSON.stringify(withInferredCameraUpdate(normalizeExcalidrawBoundText(elements))) : null;
|
|
221
267
|
}
|
|
222
268
|
|
|
223
269
|
export function buildExcalidrawRestoreCheckpointToolInput(checkpointId: string, data?: unknown): string {
|