pmx-canvas 0.3.0 → 0.3.2

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 (161) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/canvas/index.js +65 -65
  3. package/dist/types/cli/agent.d.ts +9 -1
  4. package/dist/types/cli/daemon.d.ts +74 -0
  5. package/dist/types/cli/watch.d.ts +2 -2
  6. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  7. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  8. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  9. package/dist/types/client/nodes/ExtAppFrame.d.ts +9 -1
  10. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  11. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  12. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  13. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  14. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  15. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  16. package/dist/types/server/canvas-schema.d.ts +1 -1
  17. package/dist/types/server/html-primitives.d.ts +1 -1
  18. package/dist/types/server/index.d.ts +4 -4
  19. package/dist/types/server/operations/index.d.ts +1 -1
  20. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  21. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  22. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  23. package/dist/types/server/server.d.ts +8 -0
  24. package/docs/cli.md +10 -1
  25. package/docs/http-api.md +28 -0
  26. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  27. package/docs/screenshot.png +0 -0
  28. package/docs/tech-debt-assessment-2026-07.md +2 -2
  29. package/package.json +5 -2
  30. package/skills/pmx-canvas/SKILL.md +15 -10
  31. package/skills/pmx-canvas/references/full-reference.md +17 -3
  32. package/src/cli/agent.ts +1951 -1571
  33. package/src/cli/daemon.ts +460 -0
  34. package/src/cli/index.ts +80 -323
  35. package/src/cli/watch.ts +2 -10
  36. package/src/client/App.tsx +48 -46
  37. package/src/client/canvas/AttentionHistory.tsx +11 -1
  38. package/src/client/canvas/CanvasNode.tsx +41 -29
  39. package/src/client/canvas/CanvasViewport.tsx +101 -66
  40. package/src/client/canvas/CommandPalette.tsx +61 -27
  41. package/src/client/canvas/ContextMenu.tsx +13 -20
  42. package/src/client/canvas/ContextPinBar.tsx +1 -5
  43. package/src/client/canvas/ContextPinHud.tsx +1 -6
  44. package/src/client/canvas/DockedNode.tsx +4 -4
  45. package/src/client/canvas/EdgeLayer.tsx +37 -36
  46. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  47. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  48. package/src/client/canvas/IntentLayer.tsx +31 -14
  49. package/src/client/canvas/Minimap.tsx +11 -16
  50. package/src/client/canvas/SelectionBar.tsx +4 -11
  51. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  52. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  53. package/src/client/canvas/auto-fit.ts +15 -14
  54. package/src/client/canvas/snap-guides.ts +12 -12
  55. package/src/client/canvas/use-node-resize.ts +1 -5
  56. package/src/client/canvas/use-pan-zoom.ts +25 -26
  57. package/src/client/ext-app/bridge.ts +3 -12
  58. package/src/client/icons.tsx +63 -20
  59. package/src/client/nodes/ContextNode.tsx +14 -25
  60. package/src/client/nodes/ExtAppFrame.tsx +194 -80
  61. package/src/client/nodes/FileNode.tsx +74 -62
  62. package/src/client/nodes/GroupNode.tsx +4 -6
  63. package/src/client/nodes/HtmlNode.tsx +76 -46
  64. package/src/client/nodes/ImageNode.tsx +18 -27
  65. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  66. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  67. package/src/client/nodes/LedgerNode.tsx +10 -4
  68. package/src/client/nodes/MarkdownNode.tsx +3 -10
  69. package/src/client/nodes/McpAppNode.tsx +26 -22
  70. package/src/client/nodes/MdFormatBar.tsx +10 -7
  71. package/src/client/nodes/PromptNode.tsx +23 -51
  72. package/src/client/nodes/ResponseNode.tsx +3 -13
  73. package/src/client/nodes/StatusNode.tsx +5 -9
  74. package/src/client/nodes/StatusSummary.tsx +2 -8
  75. package/src/client/nodes/WebpageNode.tsx +20 -14
  76. package/src/client/nodes/iframe-document-url.ts +25 -16
  77. package/src/client/nodes/image-warnings.ts +1 -7
  78. package/src/client/nodes/md-format.ts +20 -5
  79. package/src/client/state/attention-bridge.ts +4 -9
  80. package/src/client/state/attention-store.ts +1 -7
  81. package/src/client/state/canvas-store.ts +52 -36
  82. package/src/client/state/intent-bridge.ts +176 -112
  83. package/src/client/state/intent-store.ts +4 -1
  84. package/src/client/state/sse-bridge.ts +53 -70
  85. package/src/json-render/catalog.ts +12 -16
  86. package/src/json-render/charts/components.tsx +16 -20
  87. package/src/json-render/charts/extra-components.tsx +8 -16
  88. package/src/json-render/charts/extra-definitions.ts +1 -2
  89. package/src/json-render/charts/tufte-components.tsx +37 -20
  90. package/src/json-render/charts/tufte-definitions.ts +8 -2
  91. package/src/json-render/renderer/index.tsx +42 -22
  92. package/src/json-render/schema.ts +6 -3
  93. package/src/json-render/server.ts +33 -39
  94. package/src/mcp/canvas-access.ts +35 -21
  95. package/src/mcp/server.ts +132 -70
  96. package/src/server/agent-context.ts +63 -36
  97. package/src/server/ax-context.ts +7 -5
  98. package/src/server/ax-interaction.ts +176 -43
  99. package/src/server/ax-state-manager.ts +182 -39
  100. package/src/server/ax-state.ts +142 -47
  101. package/src/server/canvas-db.ts +213 -95
  102. package/src/server/canvas-operations.ts +180 -123
  103. package/src/server/canvas-provenance.ts +1 -4
  104. package/src/server/canvas-schema.ts +454 -73
  105. package/src/server/canvas-serialization.ts +27 -35
  106. package/src/server/canvas-state.ts +150 -58
  107. package/src/server/chart-template.ts +4 -5
  108. package/src/server/code-graph.ts +19 -6
  109. package/src/server/diagram-presets.ts +28 -29
  110. package/src/server/ext-app-lookup.ts +3 -12
  111. package/src/server/html-node-summary.ts +19 -10
  112. package/src/server/html-primitives.ts +326 -97
  113. package/src/server/html-surface.ts +6 -9
  114. package/src/server/image-source.ts +6 -4
  115. package/src/server/index.ts +320 -217
  116. package/src/server/intent-registry.ts +2 -5
  117. package/src/server/mcp-app-candidate.ts +5 -10
  118. package/src/server/mcp-app-host.ts +14 -38
  119. package/src/server/mcp-app-runtime.ts +12 -20
  120. package/src/server/mutation-history.ts +15 -5
  121. package/src/server/operations/composites.ts +1 -3
  122. package/src/server/operations/http.ts +2 -3
  123. package/src/server/operations/index.ts +7 -1
  124. package/src/server/operations/invoker.ts +4 -3
  125. package/src/server/operations/mcp.ts +22 -30
  126. package/src/server/operations/ops/annotation.ts +122 -10
  127. package/src/server/operations/ops/app.ts +98 -73
  128. package/src/server/operations/ops/ax-await.ts +17 -10
  129. package/src/server/operations/ops/ax-read.ts +347 -0
  130. package/src/server/operations/ops/ax-shared.ts +2 -7
  131. package/src/server/operations/ops/ax-state.ts +32 -14
  132. package/src/server/operations/ops/ax-timeline.ts +32 -19
  133. package/src/server/operations/ops/ax-work.ts +54 -37
  134. package/src/server/operations/ops/batch.ts +41 -15
  135. package/src/server/operations/ops/canvas-wire.ts +91 -0
  136. package/src/server/operations/ops/edges.ts +37 -25
  137. package/src/server/operations/ops/ext-app.ts +346 -0
  138. package/src/server/operations/ops/groups.ts +49 -20
  139. package/src/server/operations/ops/intent.ts +18 -12
  140. package/src/server/operations/ops/json-render.ts +239 -98
  141. package/src/server/operations/ops/nodes.ts +298 -109
  142. package/src/server/operations/ops/query.ts +46 -28
  143. package/src/server/operations/ops/snapshots.ts +35 -26
  144. package/src/server/operations/ops/validate.ts +2 -1
  145. package/src/server/operations/ops/viewport.ts +60 -16
  146. package/src/server/operations/ops/webview.ts +44 -18
  147. package/src/server/operations/registry.ts +2 -3
  148. package/src/server/operations/types.ts +7 -5
  149. package/src/server/operations/webview-runner.ts +1 -3
  150. package/src/server/placement.ts +8 -18
  151. package/src/server/server.ts +122 -1028
  152. package/src/server/spatial-analysis.ts +39 -25
  153. package/src/server/trace-manager.ts +3 -8
  154. package/src/server/web-artifacts.ts +23 -27
  155. package/src/server/webpage-node.ts +5 -13
  156. package/src/shared/auto-arrange.ts +12 -5
  157. package/src/shared/content-height-reporter.ts +8 -6
  158. package/src/shared/ext-app-tool-result.ts +2 -6
  159. package/src/shared/placement.ts +1 -4
  160. package/src/shared/semantic-attention.ts +39 -37
  161. package/src/shared/surface.ts +8 -4
@@ -23,7 +23,9 @@ export const allComponentDefinitions = {
23
23
  Badge: {
24
24
  ...badgeDefinition,
25
25
  props: badgeDefinition.props.extend({
26
- variant: z.enum(['default', 'secondary', 'destructive', 'outline', 'success', 'info', 'warning', 'error', 'danger']).nullable(),
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(schema as never, {
44
- components: allComponentDefinitions,
45
- } as never);
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 = typeof window !== 'undefined'
124
- && (window as { __PMX_CANVAS_JSON_RENDER_DISPLAY__?: string }).__PMX_CANVAS_JSON_RENDER_DISPLAY__ === 'site';
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 = typeof window !== 'undefined'
173
- && (window as { __PMX_CANVAS_FIT_CONTENT__?: boolean }).__PMX_CANVAS_FIT_CONTENT__ === true;
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
- ? (typeof explicitHeight === 'number' ? explicitHeight : fallbackHeight)
181
- : (typeof explicitHeight === 'number' ? Math.min(explicitHeight, autoHeight) : autoHeight);
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 = aggregate === 'count'
216
- ? (vs: number[]) => vs.length
217
- : aggregate === 'avg'
218
- ? (vs: number[]) => vs.reduce((a, b) => a + b, 0) / vs.length
219
- : (vs: number[]) => vs.reduce((a, b) => a + b, 0);
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 className="pmx-chart__dot-plot-svg" width="100%" height={rows.length * rowH} role="img" aria-label={props.title ?? 'dot plot'}>
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 className="pmx-chart__bullet-svg" width="100%" height={rows.length * rowH} role="img" aria-label={props.title ?? 'bullet chart'}>
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 = ['color-mix(in oklch, var(--muted) 35%, transparent)',
265
- 'color-mix(in oklch, var(--muted) 60%, transparent)',
266
- 'color-mix(in oklch, var(--muted) 90%, transparent)'];
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 key={`tick-${idx}`} x={xAt(hi)} y={cy + barH / 2 + 10} textAnchor="middle" fontSize={9} fill={MUTED}>
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 className="pmx-chart__slopegraph-svg" width="100%" height={plotH} role="img" aria-label={props.title ?? 'slopegraph'}>
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: [{ t: 0, ms: 120 }, { t: 1, ms: 138 }, { t: 2, ms: 117 }, { t: 3, ms: 152 }, { t: 4, ms: 109 }],
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
- "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.",
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: [
@@ -19,7 +19,16 @@ import { tufteChartComponents } from '../charts/tufte-components';
19
19
  import { pmxCanvasDirectives } from '../directives';
20
20
  import { JsonRenderDevtools } from '@json-render/devtools-react';
21
21
 
22
- type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'info' | 'warning' | 'error' | 'danger';
22
+ type BadgeVariant =
23
+ | 'default'
24
+ | 'secondary'
25
+ | 'destructive'
26
+ | 'outline'
27
+ | 'success'
28
+ | 'info'
29
+ | 'warning'
30
+ | 'error'
31
+ | 'danger';
23
32
  type BadgeProps = {
24
33
  text: string;
25
34
  variant?: BadgeVariant | null;
@@ -30,11 +39,7 @@ function Badge({ props }: { props: BadgeProps }) {
30
39
  const variant = props.variant;
31
40
  const resolvedVariant = variant ?? 'default';
32
41
  return (
33
- <span
34
- data-slot="badge"
35
- data-variant={resolvedVariant}
36
- className={`pmx-badge pmx-badge--${resolvedVariant}`}
37
- >
42
+ <span data-slot="badge" data-variant={resolvedVariant} className={`pmx-badge pmx-badge--${resolvedVariant}`}>
38
43
  {props.text}
39
44
  </span>
40
45
  );
@@ -111,9 +116,17 @@ function AxStateSync() {
111
116
  // validates + submits through the capability-gated endpoint). Convention-based
112
117
  // opt-in: spec authors name the action handler after the AX interaction type.
113
118
  const AX_INTERACTION_HANDLER_NAMES = [
114
- 'ax.event.record', 'ax.steer', 'ax.work.create', 'ax.work.update',
115
- 'ax.evidence.add', 'ax.approval.request', 'ax.review.add', 'ax.focus.set',
116
- 'ax.elicitation.request', 'ax.mode.request', 'ax.command.invoke',
119
+ 'ax.event.record',
120
+ 'ax.steer',
121
+ 'ax.work.create',
122
+ 'ax.work.update',
123
+ 'ax.evidence.add',
124
+ 'ax.approval.request',
125
+ 'ax.review.add',
126
+ 'ax.focus.set',
127
+ 'ax.elicitation.request',
128
+ 'ax.mode.request',
129
+ 'ax.command.invoke',
117
130
  ] as const;
118
131
 
119
132
  function buildAxHandlers(): Record<string, (params: Record<string, unknown>) => void> {
@@ -126,12 +139,15 @@ function buildAxHandlers(): Record<string, (params: Record<string, unknown>) =>
126
139
  // is no JS surface for a Promise-style ack here, so we don't stamp a correlationId.
127
140
  for (const type of AX_INTERACTION_HANDLER_NAMES) {
128
141
  handlers[type] = (params: Record<string, unknown>) => {
129
- window.parent.postMessage({
130
- source: 'pmx-canvas-ax',
131
- token,
132
- nodeId,
133
- interaction: { type, payload: params && typeof params === 'object' ? params : {} },
134
- }, '*');
142
+ window.parent.postMessage(
143
+ {
144
+ source: 'pmx-canvas-ax',
145
+ token,
146
+ nodeId,
147
+ interaction: { type, payload: params && typeof params === 'object' ? params : {} },
148
+ },
149
+ '*',
150
+ );
135
151
  };
136
152
  }
137
153
  return handlers;
@@ -176,16 +192,22 @@ function App() {
176
192
 
177
193
  // Seed AX state under a reserved `/ax` key so specs can bind { "$state": "/ax/workItems" }.
178
194
  const axState = window.__PMX_CANVAS_AX_STATE__;
179
- const initialState = axState !== undefined && axState !== null
180
- ? { ...(spec.state ?? {}), ax: axState }
181
- : spec.state ?? undefined;
195
+ const initialState =
196
+ axState !== undefined && axState !== null ? { ...(spec.state ?? {}), ax: axState } : (spec.state ?? undefined);
182
197
 
183
198
  // Standalone "Open as site" tab (#65): fill the browser viewport instead of the
184
199
  // in-canvas card height. The chart child flex-grows; useChartFrameHeight measures
185
200
  // the full viewport in this mode. Embedded/expanded keep the padded min-height box.
186
201
  const isSite = window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ === 'site';
187
202
  const containerStyle = isSite
188
- ? { display: 'flex', flexDirection: 'column' as const, height: '100dvh', minHeight: '100dvh', padding: 0, boxSizing: 'border-box' as const }
203
+ ? {
204
+ display: 'flex',
205
+ flexDirection: 'column' as const,
206
+ height: '100dvh',
207
+ minHeight: '100dvh',
208
+ padding: 0,
209
+ boxSizing: 'border-box' as const,
210
+ }
189
211
  : { minHeight: '100vh', padding: 16, boxSizing: 'border-box' as const };
190
212
  return (
191
213
  <div style={containerStyle}>
@@ -199,9 +221,7 @@ function App() {
199
221
  <div style={isSite ? { flex: 1, minHeight: 0 } : undefined}>
200
222
  <Renderer spec={spec} registry={registry} loading={false} />
201
223
  </div>
202
- {window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ ? (
203
- <JsonRenderDevtools position="right" />
204
- ) : null}
224
+ {window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ ? <JsonRenderDevtools position="right" /> : null}
205
225
  </JSONUIProvider>
206
226
  </div>
207
227
  );
@@ -34,11 +34,13 @@ export const schema = defineSchema(
34
34
  builtInActions: [
35
35
  {
36
36
  name: 'setState',
37
- description: 'Update a value in the state model at the given statePath. Params: { statePath: string, value: any }',
37
+ description:
38
+ 'Update a value in the state model at the given statePath. Params: { statePath: string, value: any }',
38
39
  },
39
40
  {
40
41
  name: 'pushState',
41
- description: 'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {"$state":"/path"} refs and "$id" for auto IDs.',
42
+ description:
43
+ 'Append an item to an array in state. Params: { statePath: string, value: any, clearStatePath?: string }. Value can contain {"$state":"/path"} refs and "$id" for auto IDs.',
42
44
  },
43
45
  {
44
46
  name: 'removeState',
@@ -46,7 +48,8 @@ export const schema = defineSchema(
46
48
  },
47
49
  {
48
50
  name: 'validateForm',
49
- description: 'Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record<string, string[]> }.',
51
+ description:
52
+ 'Validate all registered form fields and write the result to state. Params: { statePath?: string }. Defaults to /formValidation. Result: { valid: boolean, errors: Record<string, string[]> }.',
50
53
  },
51
54
  ],
52
55
  defaultRules: [
@@ -212,9 +212,7 @@ async function ensureJsonRenderBundle(): Promise<void> {
212
212
  // Avoid live source-vs-dist rebuild checks here because Bun's bundler can stall on
213
213
  // the @json-render/shadcn dependency graph during request-time viewer generation.
214
214
  const needsBuild =
215
- !existsSync(jsPath) ||
216
- !existsSync(cssPath) ||
217
- process.env.PMX_CANVAS_FORCE_JSON_RENDER_REBUILD === '1';
215
+ !existsSync(jsPath) || !existsSync(cssPath) || process.env.PMX_CANVAS_FORCE_JSON_RENDER_REBUILD === '1';
218
216
 
219
217
  if (needsBuild) {
220
218
  await rebuildJsonRenderBundle();
@@ -273,7 +271,7 @@ function normalizeItemArray(value: unknown): unknown {
273
271
  if (hasString(item)) return { label: item, value: item };
274
272
  const record = asRecord(item);
275
273
  const label = hasString(record?.label) ? record.label : hasString(record?.text) ? record.text : null;
276
- const resolvedValue = hasString(record?.value) ? record.value : label ?? `option-${index + 1}`;
274
+ const resolvedValue = hasString(record?.value) ? record.value : (label ?? `option-${index + 1}`);
277
275
  return {
278
276
  label: label ?? resolvedValue,
279
277
  value: resolvedValue,
@@ -283,11 +281,7 @@ function normalizeItemArray(value: unknown): unknown {
283
281
 
284
282
  function normalizeStringMatrix(value: unknown): unknown {
285
283
  if (!Array.isArray(value)) return value;
286
- return value.map((row) => (
287
- Array.isArray(row)
288
- ? row.map((cell) => String(cell ?? ''))
289
- : [String(row ?? '')]
290
- ));
284
+ return value.map((row) => (Array.isArray(row) ? row.map((cell) => String(cell ?? '')) : [String(row ?? '')]));
291
285
  }
292
286
 
293
287
  function normalizeButtonVariant(value: unknown): unknown {
@@ -347,12 +341,7 @@ function normalizeElementProps(
347
341
  const props = (stripNullishDeep(rawProps) as Record<string, unknown> | undefined) ?? {};
348
342
 
349
343
  for (const key of COERCIBLE_STRING_PROPS) {
350
- if (
351
- key in props &&
352
- typeof props[key] !== 'string' &&
353
- props[key] !== undefined &&
354
- !isDynamicPropValue(props[key])
355
- ) {
344
+ if (key in props && typeof props[key] !== 'string' && props[key] !== undefined && !isDynamicPropValue(props[key])) {
356
345
  props[key] = String(props[key]);
357
346
  }
358
347
  }
@@ -516,7 +505,9 @@ export function inferJsonRenderNodeTitle(spec: JsonRenderSpec, fallback = 'json-
516
505
  export function normalizeAndValidateJsonRenderSpec(spec: unknown): JsonRenderSpec {
517
506
  const specRecord = asRecord(normalizeJsonRenderInput(spec));
518
507
  if (!specRecord || typeof specRecord.root !== 'string' || !asRecord(specRecord.elements)) {
519
- throw new Error('Missing root and elements in spec. Pass a complete {root, elements} document, or a single bare component object with a type field.');
508
+ throw new Error(
509
+ 'Missing root and elements in spec. Pass a complete {root, elements} document, or a single bare component object with a type field.',
510
+ );
520
511
  }
521
512
 
522
513
  // Reject an unrecognized $-keyed expression object in any element prop BEFORE
@@ -576,10 +567,7 @@ function collectDataKeys(data: Array<Record<string, unknown>>): Set<string> {
576
567
  * as "value") without an explicit valueKey, instead of failing the data-key
577
568
  * check on a reasonable guess.
578
569
  */
579
- function firstPresentDataKey(
580
- data: Array<Record<string, unknown>>,
581
- candidates: string[],
582
- ): string | undefined {
570
+ function firstPresentDataKey(data: Array<Record<string, unknown>>, candidates: string[]): string | undefined {
583
571
  const keys = collectDataKeys(data);
584
572
  return candidates.find((key) => keys.has(key));
585
573
  }
@@ -653,7 +641,9 @@ function assertGraphDataKeys(
653
641
  const missing = [...required].filter((key) => !available.has(key));
654
642
  if (missing.length === 0) return;
655
643
  const availableList = [...available].sort().join(', ') || '(none)';
656
- throw new Error(`Graph data key mismatch for ${chartType}: missing ${missing.join(', ')}. Available keys: ${availableList}.`);
644
+ throw new Error(
645
+ `Graph data key mismatch for ${chartType}: missing ${missing.join(', ')}. Available keys: ${availableList}.`,
646
+ );
657
647
  }
658
648
 
659
649
  export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
@@ -685,11 +675,11 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
685
675
  }
686
676
  case 'RadarChart': {
687
677
  const axisKey = input.axisKey ?? input.xKey ?? 'axis';
688
- const metrics = input.metrics?.length
689
- ? input.metrics
690
- : inferKeysFromData(input.data, [axisKey]);
678
+ const metrics = input.metrics?.length ? input.metrics : inferKeysFromData(input.data, [axisKey]);
691
679
  if (metrics.length === 0) {
692
- throw new Error('RadarChart requires at least one metric key (provide `metrics` or include numeric columns in `data`).');
680
+ throw new Error(
681
+ 'RadarChart requires at least one metric key (provide `metrics` or include numeric columns in `data`).',
682
+ );
693
683
  }
694
684
  chartProps.axisKey = axisKey;
695
685
  chartProps.metrics = metrics;
@@ -698,11 +688,11 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
698
688
  }
699
689
  case 'StackedBarChart': {
700
690
  const xKey = input.xKey ?? 'label';
701
- const series = input.series?.length
702
- ? input.series
703
- : inferKeysFromData(input.data, [xKey]);
691
+ const series = input.series?.length ? input.series : inferKeysFromData(input.data, [xKey]);
704
692
  if (series.length === 0) {
705
- throw new Error('StackedBarChart requires at least one series key (provide `series` or include numeric columns in `data`).');
693
+ throw new Error(
694
+ 'StackedBarChart requires at least one series key (provide `series` or include numeric columns in `data`).',
695
+ );
706
696
  }
707
697
  chartProps.xKey = xKey;
708
698
  chartProps.series = series;
@@ -713,9 +703,10 @@ export function buildGraphSpec(input: GraphNodeInput): JsonRenderSpec {
713
703
  case 'ComposedChart': {
714
704
  chartProps.xKey = input.xKey ?? 'label';
715
705
  chartProps.barKey = input.barKey ?? input.yKey ?? 'value';
716
- chartProps.lineKey = input.lineKey
717
- ?? inferKeysFromData(input.data, [chartProps.xKey as string, chartProps.barKey as string])[0]
718
- ?? 'rate';
706
+ chartProps.lineKey =
707
+ input.lineKey ??
708
+ inferKeysFromData(input.data, [chartProps.xKey as string, chartProps.barKey as string])[0] ??
709
+ 'rate';
719
710
  chartProps.barColor = input.barColor ?? null;
720
711
  chartProps.lineColor = input.lineColor ?? null;
721
712
  chartProps.showLegend = input.showLegend !== false;
@@ -952,7 +943,8 @@ export async function buildJsonRenderViewerHtml(options: {
952
943
  * node grows to it. Off for strictSize / user-resized nodes (they fill-down). */
953
944
  fitContent?: boolean;
954
945
  }): Promise<string> {
955
- const sanitizeAxValue = (v?: string): string => (typeof v === 'string' ? v.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80) : '');
946
+ const sanitizeAxValue = (v?: string): string =>
947
+ typeof v === 'string' ? v.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 80) : '';
956
948
  try {
957
949
  await ensureJsonRenderBundle();
958
950
  const dir = bundleDir();
@@ -967,12 +959,14 @@ export async function buildJsonRenderViewerHtml(options: {
967
959
  ...(options.theme ? [`window.__PMX_CANVAS_JSON_RENDER_THEME__ = ${JSON.stringify(options.theme)};`] : []),
968
960
  ...(options.display ? [`window.__PMX_CANVAS_JSON_RENDER_DISPLAY__ = ${JSON.stringify(options.display)};`] : []),
969
961
  ...(options.devtools ? ['window.__PMX_CANVAS_JSON_RENDER_DEVTOOLS__ = true;'] : []),
970
- ...(options.nodeId && options.axToken ? [
971
- `window.__PMX_CANVAS_JSON_RENDER_NODE_ID__ = ${JSON.stringify(sanitizeAxValue(options.nodeId))};`,
972
- `window.__PMX_CANVAS_AX_TOKEN__ = ${JSON.stringify(sanitizeAxValue(options.axToken))};`,
973
- // Read-side AX state: seed for initial render + bound under /ax for specs.
974
- `window.__PMX_CANVAS_AX_STATE__ = ${JSON.stringify(options.axState ?? null).replace(/</g, '\\u003c')};`,
975
- ] : []),
962
+ ...(options.nodeId && options.axToken
963
+ ? [
964
+ `window.__PMX_CANVAS_JSON_RENDER_NODE_ID__ = ${JSON.stringify(sanitizeAxValue(options.nodeId))};`,
965
+ `window.__PMX_CANVAS_AX_TOKEN__ = ${JSON.stringify(sanitizeAxValue(options.axToken))};`,
966
+ // Read-side AX state: seed for initial render + bound under /ax for specs.
967
+ `window.__PMX_CANVAS_AX_STATE__ = ${JSON.stringify(options.axState ?? null).replace(/</g, '\\u003c')};`,
968
+ ]
969
+ : []),
976
970
  ...(options.fitContent ? ['window.__PMX_CANVAS_FIT_CONTENT__ = true;'] : []),
977
971
  jsBundle,
978
972
  ].join('\n');