pmx-canvas 0.1.9 → 0.1.11

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/dist/canvas/index.js +44 -44
  3. package/dist/json-render/index.css +1 -1
  4. package/dist/json-render/index.js +115 -115
  5. package/dist/types/client/canvas/auto-fit.d.ts +1 -1
  6. package/dist/types/json-render/catalog.d.ts +326 -310
  7. package/dist/types/json-render/charts/components.d.ts +18 -0
  8. package/dist/types/json-render/charts/definitions.d.ts +4 -0
  9. package/dist/types/json-render/charts/extra-components.d.ts +3 -0
  10. package/dist/types/json-render/charts/extra-definitions.d.ts +6 -0
  11. package/dist/types/json-render/server.d.ts +4 -0
  12. package/dist/types/server/canvas-operations.d.ts +2 -0
  13. package/dist/types/server/index.d.ts +2 -0
  14. package/package.json +1 -1
  15. package/skills/pmx-canvas/SKILL.md +9 -0
  16. package/src/cli/agent.ts +103 -5
  17. package/src/cli/index.ts +6 -3
  18. package/src/client/canvas/CanvasNode.tsx +3 -1
  19. package/src/client/canvas/auto-fit.ts +3 -3
  20. package/src/json-render/catalog.ts +9 -0
  21. package/src/json-render/charts/components.tsx +18 -10
  22. package/src/json-render/charts/definitions.ts +4 -0
  23. package/src/json-render/charts/extra-components.tsx +23 -16
  24. package/src/json-render/charts/extra-definitions.ts +6 -0
  25. package/src/json-render/renderer/index.css +61 -0
  26. package/src/json-render/renderer/index.tsx +22 -0
  27. package/src/json-render/server.ts +11 -11
  28. package/src/mcp/server.ts +10 -0
  29. package/src/server/canvas-operations.ts +21 -1
  30. package/src/server/canvas-schema.ts +5 -0
  31. package/src/server/canvas-validation.ts +9 -2
  32. package/src/server/diagram-presets.ts +82 -4
  33. package/src/server/index.ts +7 -1
  34. package/src/server/server.ts +33 -2
@@ -26,6 +26,8 @@ interface PieChartProps {
26
26
  nameKey: string;
27
27
  valueKey: string;
28
28
  height?: number | null;
29
+ showLegend?: boolean | null;
30
+ showLabels?: boolean | null;
29
31
  }
30
32
  export declare const axisStyle: {
31
33
  fontSize: number;
@@ -38,6 +40,22 @@ export declare const tooltipStyle: {
38
40
  color: string;
39
41
  fontSize: number;
40
42
  };
43
+ export declare const chartMargin: {
44
+ top: number;
45
+ right: number;
46
+ bottom: number;
47
+ left: number;
48
+ };
49
+ export declare const polarChartMargin: {
50
+ top: number;
51
+ right: number;
52
+ bottom: number;
53
+ left: number;
54
+ };
55
+ export declare const axisTickMargin = 8;
56
+ export declare const legendMargin: {
57
+ top: number;
58
+ };
41
59
  /** Shared wrapper for cartesian charts (Line + Bar). */
42
60
  export declare function CartesianChart({ props, children, className, }: {
43
61
  props: CartesianChartProps;
@@ -81,6 +81,8 @@ export declare const chartComponentDefinitions: {
81
81
  nameKey: z.ZodString;
82
82
  valueKey: z.ZodString;
83
83
  height: z.ZodNullable<z.ZodNumber>;
84
+ showLegend: z.ZodOptional<z.ZodBoolean>;
85
+ showLabels: z.ZodOptional<z.ZodBoolean>;
84
86
  }, z.core.$strip>;
85
87
  readonly description: "Pie chart for showing proportions. Provide data as an array of objects with nameKey and valueKey fields.";
86
88
  readonly example: {
@@ -98,6 +100,8 @@ export declare const chartComponentDefinitions: {
98
100
  readonly nameKey: "name";
99
101
  readonly valueKey: "share";
100
102
  readonly height: null;
103
+ readonly showLegend: true;
104
+ readonly showLabels: true;
101
105
  };
102
106
  };
103
107
  };
@@ -26,6 +26,7 @@ interface RadarChartProps {
26
26
  axisKey: string;
27
27
  metrics: string[];
28
28
  height?: number | null;
29
+ showLegend?: boolean | null;
29
30
  }
30
31
  declare function ChartRadarChart({ props }: BaseComponentProps<RadarChartProps>): import("react/jsx-runtime").JSX.Element;
31
32
  interface StackedBarChartProps {
@@ -35,6 +36,7 @@ interface StackedBarChartProps {
35
36
  series: string[];
36
37
  aggregate?: 'sum' | 'count' | 'avg' | null;
37
38
  height?: number | null;
39
+ showLegend?: boolean | null;
38
40
  }
39
41
  declare function ChartStackedBarChart({ props }: BaseComponentProps<StackedBarChartProps>): import("react/jsx-runtime").JSX.Element;
40
42
  interface ComposedChartProps {
@@ -46,6 +48,7 @@ interface ComposedChartProps {
46
48
  barColor?: string | null;
47
49
  lineColor?: string | null;
48
50
  height?: number | null;
51
+ showLegend?: boolean | null;
49
52
  }
50
53
  declare function ChartComposedChart({ props }: BaseComponentProps<ComposedChartProps>): import("react/jsx-runtime").JSX.Element;
51
54
  export declare const extraChartComponents: {
@@ -83,6 +83,7 @@ export declare const extraChartComponentDefinitions: {
83
83
  axisKey: z.ZodString;
84
84
  metrics: z.ZodArray<z.ZodString>;
85
85
  height: z.ZodNullable<z.ZodNumber>;
86
+ showLegend: z.ZodOptional<z.ZodBoolean>;
86
87
  }, z.core.$strip>;
87
88
  readonly description: "Radar chart for comparing multiple metrics across categories. Each metric in `metrics` is plotted as its own polygon.";
88
89
  readonly example: {
@@ -103,6 +104,7 @@ export declare const extraChartComponentDefinitions: {
103
104
  readonly axisKey: "skill";
104
105
  readonly metrics: readonly ["alice", "bob"];
105
106
  readonly height: null;
107
+ readonly showLegend: true;
106
108
  };
107
109
  };
108
110
  readonly StackedBarChart: {
@@ -117,6 +119,7 @@ export declare const extraChartComponentDefinitions: {
117
119
  avg: "avg";
118
120
  }>>;
119
121
  height: z.ZodNullable<z.ZodNumber>;
122
+ showLegend: z.ZodOptional<z.ZodBoolean>;
120
123
  }, z.core.$strip>;
121
124
  readonly description: "Stacked bar chart for compositional data. Each entry in `series` is plotted as its own bar segment per x value.";
122
125
  readonly example: {
@@ -141,6 +144,7 @@ export declare const extraChartComponentDefinitions: {
141
144
  readonly series: readonly ["north", "south", "east"];
142
145
  readonly aggregate: null;
143
146
  readonly height: null;
147
+ readonly showLegend: true;
144
148
  };
145
149
  };
146
150
  readonly ComposedChart: {
@@ -153,6 +157,7 @@ export declare const extraChartComponentDefinitions: {
153
157
  barColor: z.ZodNullable<z.ZodString>;
154
158
  lineColor: z.ZodNullable<z.ZodString>;
155
159
  height: z.ZodNullable<z.ZodNumber>;
160
+ showLegend: z.ZodOptional<z.ZodBoolean>;
156
161
  }, z.core.$strip>;
157
162
  readonly description: "Combined bar + line chart for paired metrics (e.g. counts + a derived rate) on the same axis.";
158
163
  readonly example: {
@@ -176,6 +181,7 @@ export declare const extraChartComponentDefinitions: {
176
181
  readonly barColor: null;
177
182
  readonly lineColor: null;
178
183
  readonly height: null;
184
+ readonly showLegend: true;
179
185
  };
180
186
  };
181
187
  };
@@ -10,6 +10,7 @@ export interface JsonRenderNodeInput {
10
10
  y?: number;
11
11
  width?: number;
12
12
  height?: number;
13
+ strictSize?: boolean;
13
14
  }
14
15
  export interface GraphNodeInput {
15
16
  title?: string;
@@ -30,10 +31,13 @@ export interface GraphNodeInput {
30
31
  barColor?: string;
31
32
  lineColor?: string;
32
33
  height?: number;
34
+ showLegend?: boolean;
35
+ showLabels?: boolean;
33
36
  x?: number;
34
37
  y?: number;
35
38
  width?: number;
36
39
  heightPx?: number;
40
+ strictSize?: boolean;
37
41
  }
38
42
  export declare const JSON_RENDER_NODE_SIZE: {
39
43
  width: number;
@@ -32,6 +32,7 @@ export interface CanvasStructuredNodeUpdateInput extends Omit<CanvasGraphNodeUpd
32
32
  content?: unknown;
33
33
  data?: unknown;
34
34
  arrangeLocked?: unknown;
35
+ strictSize?: boolean;
35
36
  chartHeight?: unknown;
36
37
  }
37
38
  interface CanvasAddNodeInput {
@@ -46,6 +47,7 @@ interface CanvasAddNodeInput {
46
47
  defaultWidth?: number;
47
48
  defaultHeight?: number;
48
49
  fileMode?: 'path' | 'inline' | 'auto';
50
+ strictSize?: boolean;
49
51
  }
50
52
  interface CanvasCreateGroupInput {
51
53
  title?: string;
@@ -27,6 +27,7 @@ export declare class PmxCanvas extends EventEmitter {
27
27
  y?: number;
28
28
  width?: number;
29
29
  height?: number;
30
+ strictSize?: boolean;
30
31
  }): string;
31
32
  addWebpageNode(input: {
32
33
  title?: string;
@@ -35,6 +36,7 @@ export declare class PmxCanvas extends EventEmitter {
35
36
  y?: number;
36
37
  width?: number;
37
38
  height?: number;
39
+ strictSize?: boolean;
38
40
  }): Promise<{
39
41
  ok: boolean;
40
42
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmx-canvas",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",
@@ -144,6 +144,11 @@ pmx-canvas open
144
144
  pmx-canvas arrange --layout flow
145
145
  pmx-canvas focus <node-id> --no-pan # Select/raise without moving the user's viewport
146
146
  pmx-canvas fit --width 1440 --height 900 # Fit the whole board for screenshots/review
147
+ pmx-canvas screenshot --output ./canvas.png # Shorthand for `webview screenshot`
148
+ pmx-canvas json-render --schema --summary # Inspect json-render component catalog
149
+ pmx-canvas json-render --example --component Table
150
+ pmx-canvas node add --type markdown --title "Long doc" --strict-size # Scroll instead of auto-fit
151
+ pmx-canvas node add --type graph --graphType pie --data-file metrics.json --show-legend false --show-labels false
147
152
  pmx-canvas node update <node-id> --spec-file ./dashboard.json
148
153
  pmx-canvas validate spec --type json-render --spec-file ./dashboard.json --summary
149
154
  pmx-canvas web-artifact build --title "Dashboard" --app-file ./App.tsx --deps recharts --include-logs
@@ -170,6 +175,10 @@ pmx-canvas spatial
170
175
  - `search`, `layout`, `status`, `arrange`, `focus` — inspect and navigate the canvas. Prefer
171
176
  `focus --no-pan` when you only need to select/raise a node without hijacking the human's camera.
172
177
  - `fit [id ...]` — set the server viewport to fit the whole canvas or selected nodes before screenshots or whole-board review
178
+ - `screenshot --output <path>` — top-level shortcut for `webview screenshot`; supports `--format png|jpeg|webp` and `--quality`
179
+ - `json-render --schema|--examples` — inspect the json-render component catalog with `--component`/`--field` filters; same data as `node schema --type json-render` in a more direct shape
180
+ - `--strict-size` (alias `--scroll-overflow`) on `node add`/`node update` — keep explicit width/height fixed and scroll overflowing content instead of letting the renderer auto-fit. Useful for long markdown, dense webpages, and dashboards that should fit a tile-sized frame.
181
+ - `--show-legend false` / `--show-labels false` on `node add --type graph` and `graph add` — hide chart legends and pie slice labels for compact graph nodes in tile-style boards.
173
182
  - `open` — open the current workbench in the browser
174
183
  - `pin --list|--clear|<ids...>` — manage context pins
175
184
  - `undo`, `redo`, `history` — time travel
package/src/cli/agent.ts CHANGED
@@ -155,7 +155,7 @@ function parseFlags(args: string[]): { positional: string[]; flags: Record<strin
155
155
  const BOOL_FLAGS = new Set([
156
156
  'help', 'h', 'ids', 'stdin', 'yes', 'list', 'clear', 'set', 'animated', 'dry-run',
157
157
  'no-open-in-canvas', 'lock-arrange', 'unlock-arrange', 'json', 'compact', 'summary',
158
- 'verbose', 'include-logs', 'no-pan',
158
+ 'verbose', 'include-logs', 'no-pan', 'schema', 'example', 'examples', 'strict-size', 'scroll-overflow',
159
159
  ]);
160
160
  for (let i = 0; i < args.length; i++) {
161
161
  const arg = args[i];
@@ -241,6 +241,18 @@ 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
+
252
+ function applyStrictSizeFlags(body: Record<string, unknown>, flags: Record<string, string | true>): void {
253
+ if (flags['strict-size'] || flags['scroll-overflow']) body.strictSize = true;
254
+ }
255
+
244
256
  function isRecord(value: unknown): value is Record<string, unknown> {
245
257
  return !!value && typeof value === 'object' && !Array.isArray(value);
246
258
  }
@@ -598,6 +610,7 @@ async function buildJsonRenderRequestBody(
598
610
  width: 'Use a positive number, e.g. --width 840',
599
611
  height: 'Use a positive number, e.g. --height 620',
600
612
  });
613
+ applyStrictSizeFlags(body, flags);
601
614
  return body;
602
615
  }
603
616
 
@@ -657,6 +670,10 @@ async function buildGraphRequestBody(
657
670
  if (color) body.color = color;
658
671
  if (barColor) body.barColor = barColor;
659
672
  if (lineColor) body.lineColor = lineColor;
673
+ const showLegend = optionalBooleanFlag(flags, 'show-legend', 'Use --show-legend true or --show-legend false');
674
+ const showLabels = optionalBooleanFlag(flags, 'show-labels', 'Use --show-labels true or --show-labels false');
675
+ if (showLegend !== undefined) body.showLegend = showLegend;
676
+ if (showLabels !== undefined) body.showLabels = showLabels;
660
677
 
661
678
  const chartHeight = optionalPositiveFiniteFlag(flags, 'chart-height', 'Use a positive number, e.g. --chart-height 300');
662
679
  const x = optionalFiniteFlag(flags, 'x', 'Use a finite number, e.g. --x 500');
@@ -674,6 +691,7 @@ async function buildGraphRequestBody(
674
691
  if (y !== undefined) body.y = y;
675
692
  if (width !== undefined) body.width = width;
676
693
  if (nodeHeight !== undefined) body.nodeHeight = nodeHeight;
694
+ applyStrictSizeFlags(body, flags);
677
695
  return body;
678
696
  }
679
697
 
@@ -1059,6 +1077,7 @@ cmd('node add', 'Add a node to the canvas', [
1059
1077
  width: 'Use a positive number, e.g. --width 500',
1060
1078
  height: 'Use a positive number, e.g. --height 280',
1061
1079
  });
1080
+ applyStrictSizeFlags(body, flags);
1062
1081
 
1063
1082
  // Support --stdin for piping content
1064
1083
  if (flags.stdin) {
@@ -1073,6 +1092,37 @@ cmd('node add', 'Add a node to the canvas', [
1073
1092
  output(result);
1074
1093
  });
1075
1094
 
1095
+ cmd('json-render', 'Show json-render schema and canonical examples', [
1096
+ 'pmx-canvas json-render --schema --summary',
1097
+ 'pmx-canvas json-render --examples',
1098
+ 'pmx-canvas json-render --example --component Table',
1099
+ 'pmx-canvas json-render --schema --component Badge --field variant',
1100
+ ], async (args) => {
1101
+ const { flags } = parseFlags(args);
1102
+ if (flags.help || flags.h) return showCommandHelp('json-render');
1103
+
1104
+ const schema = await loadCanvasSchema();
1105
+ const componentName = getStringFlag(flags, 'component');
1106
+ const fieldName = getStringFlag(flags, 'field');
1107
+
1108
+ if (flags.example || flags.examples) {
1109
+ if (fieldName) die('--field is only supported with --schema.', 'Use: pmx-canvas json-render --schema --component Table --field rows');
1110
+ if (componentName) {
1111
+ const component = schema.jsonRender.components.find((entry) => entry.type === componentName);
1112
+ if (!component) die(`Unknown json-render component: ${componentName}`, 'Run: pmx-canvas json-render --schema --summary');
1113
+ output({ component: component.type, example: component.example });
1114
+ return;
1115
+ }
1116
+ output({
1117
+ rootShape: schema.jsonRender.rootShape,
1118
+ examples: Object.fromEntries(schema.jsonRender.components.map((entry) => [entry.type, entry.example])),
1119
+ });
1120
+ return;
1121
+ }
1122
+
1123
+ output(filterJsonRenderSchemaView(schema.jsonRender, flags));
1124
+ });
1125
+
1076
1126
  cmd('graph add', 'Add a graph node to the canvas', [
1077
1127
  'pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value',
1078
1128
  'pmx-canvas graph add --graphType composed --data \'[{"day":"Mon","visits":10,"conversion":0.4}]\' --xKey day --barKey visits --lineKey conversion',
@@ -1089,6 +1139,7 @@ cmd('node schema', 'Describe server-supported node create schemas and canonical
1089
1139
  'pmx-canvas node schema',
1090
1140
  'pmx-canvas node schema --type webpage',
1091
1141
  'pmx-canvas node schema --type json-render',
1142
+ 'pmx-canvas json-render --schema --summary',
1092
1143
  'pmx-canvas node schema --type json-render --component Table',
1093
1144
  'pmx-canvas node schema --type webpage --field url',
1094
1145
  'pmx-canvas node schema --summary',
@@ -1216,6 +1267,7 @@ cmd('node update', 'Update a node by ID', [
1216
1267
  'pmx-canvas node update <node-id> --width 840 --height 620',
1217
1268
  'pmx-canvas node update <node-id> --spec-file ./dashboard.json',
1218
1269
  'pmx-canvas node update <graph-id> --data-file ./metrics.json --chart-height 420',
1270
+ 'pmx-canvas node update <node-id> --pinned true',
1219
1271
  'pmx-canvas node update <node-id> --lock-arrange',
1220
1272
  ], async (args) => {
1221
1273
  const { positional, flags } = parseFlags(args);
@@ -1234,6 +1286,17 @@ cmd('node update', 'Update a node by ID', [
1234
1286
  const y = optionalFiniteFlag(flags, 'y', 'Use a finite number, e.g. --y 300');
1235
1287
  const width = optionalPositiveFiniteFlag(flags, 'width', 'Use a positive number, e.g. --width 840');
1236
1288
  const height = optionalPositiveFiniteFlag(flags, 'height', 'Use a positive number, e.g. --height 620');
1289
+ const nodeHeight = optionalPositiveFiniteFlagWithAliases(
1290
+ flags,
1291
+ 'Use a positive number, e.g. --node-height 620',
1292
+ 'node-height',
1293
+ 'nodeHeight',
1294
+ );
1295
+ if (height !== undefined && nodeHeight !== undefined) {
1296
+ die('Use either --height/--node-height, not both.');
1297
+ }
1298
+ const frameHeight = height ?? nodeHeight;
1299
+ const pinned = optionalBooleanFlag(flags, 'pinned', 'Use --pinned true or --pinned false');
1237
1300
  if (flags['lock-arrange'] && flags['unlock-arrange']) {
1238
1301
  die('Use either --lock-arrange or --unlock-arrange, not both.');
1239
1302
  }
@@ -1243,7 +1306,9 @@ cmd('node update', 'Update a node by ID', [
1243
1306
  ? false
1244
1307
  : undefined;
1245
1308
 
1246
- if (x !== undefined || y !== undefined || width !== undefined || height !== undefined || arrangeLocked !== undefined) {
1309
+ applyStrictSizeFlags(body, flags);
1310
+
1311
+ if (x !== undefined || y !== undefined || width !== undefined || frameHeight !== undefined || arrangeLocked !== undefined) {
1247
1312
  const existing = await api('GET', `/api/canvas/node/${encodeURIComponent(id)}`) as {
1248
1313
  position: { x: number; y: number };
1249
1314
  size: { width: number; height: number };
@@ -1257,10 +1322,10 @@ cmd('node update', 'Update a node by ID', [
1257
1322
  };
1258
1323
  }
1259
1324
 
1260
- if (width !== undefined || height !== undefined) {
1325
+ if (width !== undefined || frameHeight !== undefined) {
1261
1326
  body.size = {
1262
1327
  width: width ?? existing.size.width,
1263
- height: height ?? existing.size.height,
1328
+ height: frameHeight ?? existing.size.height,
1264
1329
  };
1265
1330
  }
1266
1331
 
@@ -1269,10 +1334,12 @@ cmd('node update', 'Update a node by ID', [
1269
1334
  }
1270
1335
  }
1271
1336
 
1337
+ if (pinned !== undefined) body.pinned = pinned;
1338
+
1272
1339
  if (Object.keys(body).length === 0) {
1273
1340
  die(
1274
1341
  'No updates specified',
1275
- 'Use --title, --content, --x, --y, --width, --height, --lock-arrange, --unlock-arrange, or --stdin',
1342
+ 'Use --title, --content, --x, --y, --width, --height, --strict-size, --pinned, --lock-arrange, --unlock-arrange, or --stdin',
1276
1343
  );
1277
1344
  }
1278
1345
 
@@ -2035,6 +2102,16 @@ cmd('webview screenshot', 'Capture a screenshot from the active Bun.WebView auto
2035
2102
  });
2036
2103
  });
2037
2104
 
2105
+ cmd('screenshot', 'Capture a screenshot from the active Bun.WebView automation session', [
2106
+ 'pmx-canvas screenshot --output ./canvas.png',
2107
+ 'pmx-canvas screenshot --output ./canvas.webp --format webp --quality 80',
2108
+ ], async (args) => {
2109
+ if (args.includes('--help') || args.includes('-h')) return showCommandHelp('screenshot');
2110
+ const screenshotCommand = COMMANDS['webview screenshot'];
2111
+ if (!screenshotCommand) die('Internal error: webview screenshot command is unavailable.');
2112
+ await screenshotCommand.run(args);
2113
+ });
2114
+
2038
2115
  // ── code-graph ───────────────────────────────────────────────
2039
2116
  cmd('code-graph', 'Show auto-detected file dependency graph', [
2040
2117
  'pmx-canvas code-graph',
@@ -2171,11 +2248,21 @@ function showCommandHelp(name: string): void {
2171
2248
  console.log(' pmx-canvas node add --help --type json-render --component Table');
2172
2249
  console.log(' pmx-canvas node add --help --type graph');
2173
2250
  console.log(' pmx-canvas node add --help --type webpage --json');
2251
+ console.log(' Use --strict-size to keep explicit width/height fixed and scroll overflowing content.');
2252
+ }
2253
+ if (name === 'json-render') {
2254
+ console.log('\nOptions:');
2255
+ console.log(' --schema Show json-render catalog schema (default)');
2256
+ console.log(' --summary Show compact component summaries');
2257
+ console.log(' --component <name> Focus on one component');
2258
+ console.log(' --field <name> Focus on one component prop');
2259
+ console.log(' --example, --examples Print canonical component examples');
2174
2260
  }
2175
2261
  if (name === 'node add' || name === 'graph add' || name === 'validate spec') {
2176
2262
  console.log('\nGraph flags:');
2177
2263
  console.log(' Graph fields accept kebab-case CLI flags and camelCase schema names, e.g. --graph-type/--graphType and --x-key/--xKey');
2178
2264
  console.log(' Use --node-height/--nodeHeight for canvas frame height; use --chart-height for chart content height. --height is kept as a frame-height alias for compatibility.');
2265
+ console.log(' Pass --show-legend false to hide legends in compact node layouts.');
2179
2266
  }
2180
2267
  if (name === 'node schema') {
2181
2268
  console.log('\nFilters:');
@@ -2205,6 +2292,13 @@ function showCommandHelp(name: string): void {
2205
2292
  console.log(' --padding <px> World-space padding around fitted nodes (default 60)');
2206
2293
  console.log(' --max-scale <scale> Maximum zoom scale (default 1)');
2207
2294
  }
2295
+ if (name === 'screenshot' || name === 'webview screenshot') {
2296
+ console.log('\nOptions:');
2297
+ console.log(' --output <path> Required output image path');
2298
+ console.log(' --format <type> png, jpeg, or webp');
2299
+ console.log(' --quality <number> Encoder quality for lossy formats');
2300
+ console.log(' Requires an active automation session: pmx-canvas webview start');
2301
+ }
2208
2302
  if (name === 'external-app add') {
2209
2303
  console.log('\nOptions:');
2210
2304
  console.log(' --kind excalidraw External app kind to create');
@@ -2236,6 +2330,7 @@ Node commands:
2236
2330
  pmx-canvas node get <id> Get a node by ID
2237
2331
  pmx-canvas node update <id> [opts] Update a node
2238
2332
  pmx-canvas node remove <id> Remove a node
2333
+ pmx-canvas json-render Show json-render schema/examples
2239
2334
  pmx-canvas graph add [options] Add a graph node
2240
2335
 
2241
2336
  Edge commands:
@@ -2255,6 +2350,7 @@ Canvas commands:
2255
2350
  pmx-canvas watch [options] Watch semantic canvas changes over SSE
2256
2351
  pmx-canvas focus <id> Pan viewport to node
2257
2352
  pmx-canvas fit [id ...] Fit viewport to canvas or selected nodes
2353
+ pmx-canvas screenshot Save automation screenshot to disk
2258
2354
  pmx-canvas external-app add Add hosted external apps like Excalidraw
2259
2355
  pmx-canvas webview status Show WebView automation status
2260
2356
  pmx-canvas webview start [options] Start or replace automation session
@@ -2303,6 +2399,8 @@ Examples:
2303
2399
  pmx-canvas node add --type markdown --title "API Design" --content "# REST API"
2304
2400
  pmx-canvas node add --type webpage --url "https://example.com/docs"
2305
2401
  pmx-canvas node add --type json-render --title "Dashboard" --spec-file ./dashboard.json
2402
+ pmx-canvas json-render --schema --summary
2403
+ pmx-canvas json-render --example --component Table
2306
2404
  pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
2307
2405
  pmx-canvas node add --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
2308
2406
  pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value
package/src/cli/index.ts CHANGED
@@ -29,8 +29,8 @@ if (args.includes('--version') || args.includes('-v')) {
29
29
  // ── Agent CLI subcommands ────────────────────────────────────
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
- 'node', 'edge', 'search', 'layout', 'status', 'arrange', 'focus',
33
- 'pin', 'undo', 'redo', 'history', 'snapshot', 'diff', 'group', 'webview', 'open',
32
+ 'node', 'edge', 'json-render', 'search', 'layout', 'status', 'arrange', 'focus',
33
+ 'fit', 'screenshot', '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
 
@@ -486,8 +486,10 @@ Server options:
486
486
 
487
487
  Agent CLI (works against running server):
488
488
  node add|list|get|update|remove Manage nodes
489
+ json-render Show json-render schema/examples
489
490
  graph add Add graph nodes (alias for node add --type graph)
490
491
  edge add|list|remove Manage edges
492
+ screenshot Save a WebView automation screenshot
491
493
  webview status|start|evaluate|resize|screenshot|stop
492
494
  Manage Bun.WebView automation session
493
495
  search <query> Search nodes
@@ -540,6 +542,7 @@ Examples:
540
542
  pmx-canvas node add --type markdown --title "Hello World" Add a node
541
543
  pmx-canvas node add --type webpage --url "https://example.com" Add a webpage node
542
544
  pmx-canvas node add --type json-render --title "Dashboard" --spec-file ./dashboard.json
545
+ pmx-canvas json-render --schema --summary Show json-render schema info
543
546
  pmx-canvas node add --type web-artifact --title "Dashboard" --app-file ./App.tsx
544
547
  pmx-canvas graph add --graph-type bar --data-file ./metrics.json --x-key label --y-key value
545
548
  pmx-canvas node list List all nodes
@@ -549,7 +552,7 @@ Examples:
549
552
  pmx-canvas validate spec --type graph --graph-type bar --data-file ./metrics.json --x-key label --y-key value
550
553
  pmx-canvas open Open the workbench in a browser
551
554
  pmx-canvas webview status Show WebView automation status
552
- pmx-canvas webview screenshot --output ./canvas.png Save a WebView screenshot
555
+ pmx-canvas screenshot --output ./canvas.png Save a WebView screenshot
553
556
  pmx-canvas search "auth" Find nodes
554
557
  pmx-canvas arrange --layout column Auto-arrange
555
558
  pmx-canvas batch --file ./canvas-ops.json Run batch canvas ops
@@ -179,12 +179,13 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
179
179
  autoFitPersistTimer.current = null;
180
180
  }
181
181
  };
182
- }, [node.id, node.type, node.data.mode, node.collapsed, node.dockPosition, node.size.width, node.size.height]);
182
+ }, [node.id, node.type, node.data.mode, node.data.strictSize, node.collapsed, node.dockPosition, node.size.width, node.size.height]);
183
183
 
184
184
  const isPinned = node.pinned;
185
185
  const isTrace = node.type === 'trace';
186
186
  const isTraceRunning = isTrace && node.data.status === 'running';
187
187
  const isGroup = node.type === 'group';
188
+ const isStrictSize = node.data.strictSize === true;
188
189
  const viewportScale = Math.max(viewport.value.scale, 0.01);
189
190
  const chromeScale = viewportScale < 1 ? Math.min(2.2, 1 / viewportScale) : 1;
190
191
 
@@ -213,6 +214,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
213
214
  isTrace ? 'trace-node' : '',
214
215
  isTraceRunning ? 'trace-running' : '',
215
216
  isGroup ? 'group-node' : '',
217
+ isStrictSize ? 'strict-size' : '',
216
218
  ]
217
219
  .filter(Boolean)
218
220
  .join(' ');
@@ -1,18 +1,18 @@
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 (node.type === 'graph' || node.type === 'json-render') && node.size.height > AUTO_FIT_MAX_HEIGHT;
11
+ return node.type === 'graph' || node.type === 'json-render';
12
12
  }
13
13
 
14
14
  export function shouldAutoFitNode(node: CanvasNodeState): boolean {
15
- return !node.collapsed && !node.dockPosition && node.type !== 'group' && !isExtAppNode(node) && !hasExplicitStructuredFrame(node);
15
+ return !node.collapsed && !node.dockPosition && node.data.strictSize !== true && node.type !== 'group' && !isExtAppNode(node) && !hasExplicitStructuredFrame(node);
16
16
  }
17
17
 
18
18
  export function computeAutoFitHeight(node: CanvasNodeState, contentHeight: number): number | null {
@@ -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
  };
@@ -84,6 +84,8 @@ interface PieChartProps {
84
84
  nameKey: string;
85
85
  valueKey: string;
86
86
  height?: number | null;
87
+ showLegend?: boolean | null;
88
+ showLabels?: boolean | null;
87
89
  }
88
90
 
89
91
  export const axisStyle = {
@@ -99,6 +101,11 @@ export const tooltipStyle = {
99
101
  fontSize: 13,
100
102
  };
101
103
 
104
+ export const chartMargin = { top: 14, right: 28, bottom: 28, left: 10 };
105
+ export const polarChartMargin = { top: 18, right: 40, bottom: 30, left: 40 };
106
+ export const axisTickMargin = 8;
107
+ export const legendMargin = { top: 10 };
108
+
102
109
  /** Shared wrapper for cartesian charts (Line + Bar). */
103
110
  export function CartesianChart({
104
111
  props,
@@ -127,10 +134,10 @@ function ChartLineChart({ props }: BaseComponentProps<CartesianChartProps>) {
127
134
  return (
128
135
  <CartesianChart props={props} className="pmx-chart--line">
129
136
  {(data) => (
130
- <RechartsLineChart data={data}>
137
+ <RechartsLineChart data={data} margin={chartMargin}>
131
138
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
132
- <XAxis dataKey={props.xKey} tick={axisStyle} />
133
- <YAxis tick={axisStyle} />
139
+ <XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
140
+ <YAxis tick={axisStyle} tickMargin={axisTickMargin} />
134
141
  <Tooltip contentStyle={tooltipStyle} />
135
142
  <Line
136
143
  type="monotone"
@@ -151,10 +158,10 @@ function ChartBarChart({ props }: BaseComponentProps<CartesianChartProps>) {
151
158
  return (
152
159
  <CartesianChart props={props} className="pmx-chart--bar">
153
160
  {(data) => (
154
- <RechartsBarChart data={data}>
161
+ <RechartsBarChart data={data} margin={chartMargin}>
155
162
  <CartesianGrid strokeDasharray="3 3" stroke="var(--border, #e5e5e5)" />
156
- <XAxis dataKey={props.xKey} tick={axisStyle} />
157
- <YAxis tick={axisStyle} />
163
+ <XAxis dataKey={props.xKey} tick={axisStyle} tickMargin={axisTickMargin} />
164
+ <YAxis tick={axisStyle} tickMargin={axisTickMargin} />
158
165
  <Tooltip contentStyle={tooltipStyle} cursor={false} />
159
166
  <Bar dataKey={props.yKey} fill={fill} radius={[4, 4, 0, 0]} />
160
167
  </RechartsBarChart>
@@ -171,17 +178,18 @@ function ChartPieChart({ props }: BaseComponentProps<PieChartProps>) {
171
178
  <div className="pmx-chart pmx-chart--pie">
172
179
  {props.title && <div className="pmx-chart__title">{props.title}</div>}
173
180
  <ResponsiveContainer width="100%" height={h}>
174
- <RechartsPieChart>
181
+ <RechartsPieChart margin={polarChartMargin}>
175
182
  <Tooltip contentStyle={tooltipStyle} />
176
- <Legend />
183
+ {props.showLegend !== false && <Legend wrapperStyle={legendMargin} />}
177
184
  <Pie
178
185
  data={data}
179
186
  dataKey={props.valueKey}
180
187
  nameKey={props.nameKey}
181
188
  cx="50%"
182
189
  cy="50%"
183
- outerRadius="80%"
184
- label
190
+ outerRadius="64%"
191
+ label={props.showLabels !== false}
192
+ labelLine={false}
185
193
  >
186
194
  {data.map((_, i) => (
187
195
  <Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
@@ -63,6 +63,8 @@ export const chartComponentDefinitions = {
63
63
  nameKey: z.string(),
64
64
  valueKey: z.string(),
65
65
  height: z.number().nullable(),
66
+ showLegend: z.boolean().optional(),
67
+ showLabels: z.boolean().optional(),
66
68
  }),
67
69
  description:
68
70
  'Pie chart for showing proportions. Provide data as an array of objects with nameKey and valueKey fields.',
@@ -76,6 +78,8 @@ export const chartComponentDefinitions = {
76
78
  nameKey: 'name',
77
79
  valueKey: 'share',
78
80
  height: null,
81
+ showLegend: true,
82
+ showLabels: true,
79
83
  },
80
84
  },
81
85
  } as const;