pmx-canvas 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/dist/canvas/index.js +2 -2
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/docs/http-api.md +28 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
- package/docs/tech-debt-assessment-2026-07.md +2 -2
- package/package.json +5 -2
- package/skills/pmx-canvas/SKILL.md +3 -1
- package/skills/pmx-canvas/references/full-reference.md +10 -3
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +35 -21
- package/src/mcp/server.ts +132 -70
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +182 -39
- package/src/server/ax-state.ts +142 -47
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +454 -73
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +150 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +19 -6
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +3 -12
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +320 -217
- package/src/server/intent-registry.ts +2 -5
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +1 -3
- package/src/server/operations/http.ts +2 -3
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +22 -30
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +98 -73
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +298 -109
- package/src/server/operations/ops/query.ts +46 -28
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +108 -1027
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -20,7 +20,7 @@ export const HTML_PRIMITIVE_KINDS = [
|
|
|
20
20
|
'prompt-tuner',
|
|
21
21
|
] as const;
|
|
22
22
|
|
|
23
|
-
export type HtmlPrimitiveKind = typeof HTML_PRIMITIVE_KINDS[number];
|
|
23
|
+
export type HtmlPrimitiveKind = (typeof HTML_PRIMITIVE_KINDS)[number];
|
|
24
24
|
|
|
25
25
|
export interface HtmlPrimitiveDescriptor {
|
|
26
26
|
kind: HtmlPrimitiveKind;
|
|
@@ -55,7 +55,11 @@ export interface HtmlPrimitiveSemanticMetadata {
|
|
|
55
55
|
presentationTheme?: string | Record<string, string>;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
type PrimitiveRenderer = (input: {
|
|
58
|
+
type PrimitiveRenderer = (input: {
|
|
59
|
+
title: string;
|
|
60
|
+
data: Record<string, unknown>;
|
|
61
|
+
descriptor: HtmlPrimitiveDescriptor;
|
|
62
|
+
}) => string;
|
|
59
63
|
|
|
60
64
|
const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
61
65
|
{
|
|
@@ -70,7 +74,13 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
70
74
|
title: 'Implementation Options',
|
|
71
75
|
data: {
|
|
72
76
|
items: [
|
|
73
|
-
{
|
|
77
|
+
{
|
|
78
|
+
title: 'Small patch',
|
|
79
|
+
summary: 'Least disruption.',
|
|
80
|
+
tradeoff: 'Limited future flexibility.',
|
|
81
|
+
pros: ['Fast'],
|
|
82
|
+
cons: ['May need follow-up'],
|
|
83
|
+
},
|
|
74
84
|
],
|
|
75
85
|
},
|
|
76
86
|
},
|
|
@@ -81,7 +91,8 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
81
91
|
description: 'Implementation plan with milestones, data flow, code checkpoints, and risks.',
|
|
82
92
|
useWhen: 'Use when a markdown plan would be long and the human needs sequence, dependencies, and risk at a glance.',
|
|
83
93
|
defaultSize: { width: 1040, height: 760 },
|
|
84
|
-
dataShape:
|
|
94
|
+
dataShape:
|
|
95
|
+
'{ milestones: [{ title, detail, status }], flow: [{ from, to, label }], risks: [{ risk, mitigation }], snippets: [{ label, code }] }',
|
|
85
96
|
example: {
|
|
86
97
|
kind: 'plan-timeline',
|
|
87
98
|
title: 'Feature Plan',
|
|
@@ -102,7 +113,15 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
102
113
|
kind: 'review-sheet',
|
|
103
114
|
title: 'Streaming Review',
|
|
104
115
|
data: {
|
|
105
|
-
findings: [
|
|
116
|
+
findings: [
|
|
117
|
+
{
|
|
118
|
+
severity: 'warning',
|
|
119
|
+
title: 'Backpressure boundary',
|
|
120
|
+
file: 'src/server.ts',
|
|
121
|
+
line: 42,
|
|
122
|
+
detail: 'Confirm queue flush before close.',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
106
125
|
},
|
|
107
126
|
},
|
|
108
127
|
},
|
|
@@ -112,13 +131,20 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
112
131
|
description: 'Reviewer-ready PR narrative with motivation, before/after, file tour, test plan, and rollout notes.',
|
|
113
132
|
useWhen: 'Use when a change needs a high-signal review guide rather than a flat pull request description.',
|
|
114
133
|
defaultSize: { width: 1040, height: 760 },
|
|
115
|
-
dataShape:
|
|
134
|
+
dataShape:
|
|
135
|
+
'{ summary, why, before: string[], after: string[], files: [{ path, why, focus }], reviewFocus: string[], tests: string[], rollout: string[] }',
|
|
116
136
|
example: {
|
|
117
137
|
kind: 'pr-writeup',
|
|
118
138
|
title: 'HTML Primitive PR',
|
|
119
139
|
data: {
|
|
120
140
|
summary: 'Adds generated HTML primitives across HTTP, MCP, CLI, and SDK.',
|
|
121
|
-
files: [
|
|
141
|
+
files: [
|
|
142
|
+
{
|
|
143
|
+
path: 'src/server/html-primitives.ts',
|
|
144
|
+
why: 'Primitive catalog and renderers.',
|
|
145
|
+
focus: 'Escaping and export behavior.',
|
|
146
|
+
},
|
|
147
|
+
],
|
|
122
148
|
},
|
|
123
149
|
},
|
|
124
150
|
},
|
|
@@ -142,15 +168,19 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
142
168
|
kind: 'code-walkthrough',
|
|
143
169
|
title: 'Code Walkthrough',
|
|
144
170
|
description: 'Guided code-path map with modules, ordered file steps, snippets, key files, and gotchas.',
|
|
145
|
-
useWhen:
|
|
171
|
+
useWhen:
|
|
172
|
+
'Use to explain an unfamiliar package, request path, or implementation slice after inspecting source files.',
|
|
146
173
|
defaultSize: { width: 1040, height: 760 },
|
|
147
|
-
dataShape:
|
|
174
|
+
dataShape:
|
|
175
|
+
'{ summary, modules: [{ id, title, detail, role }], edges: [{ from, to, label }], steps: [{ title, file, detail, code }], keyFiles: [{ path, description }], gotchas: string[] }',
|
|
148
176
|
example: {
|
|
149
177
|
kind: 'code-walkthrough',
|
|
150
178
|
title: 'Canvas Creation Path',
|
|
151
179
|
data: {
|
|
152
180
|
modules: [{ id: 'api', title: 'HTTP API', detail: 'Receives node create requests.', role: 'entry' }],
|
|
153
|
-
steps: [
|
|
181
|
+
steps: [
|
|
182
|
+
{ title: 'Route request', file: 'src/server/server.ts', detail: 'Normalize input before mutating state.' },
|
|
183
|
+
],
|
|
154
184
|
},
|
|
155
185
|
},
|
|
156
186
|
},
|
|
@@ -165,7 +195,14 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
165
195
|
kind: 'design-sheet',
|
|
166
196
|
title: 'Visual Directions',
|
|
167
197
|
data: {
|
|
168
|
-
directions: [
|
|
198
|
+
directions: [
|
|
199
|
+
{
|
|
200
|
+
title: 'Editorial',
|
|
201
|
+
tone: 'calm, high contrast',
|
|
202
|
+
palette: ['#f8f1e7', '#16120f', '#d65a31'],
|
|
203
|
+
rationale: 'Readable and opinionated.',
|
|
204
|
+
},
|
|
205
|
+
],
|
|
169
206
|
},
|
|
170
207
|
},
|
|
171
208
|
},
|
|
@@ -188,10 +225,13 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
188
225
|
{
|
|
189
226
|
kind: 'interaction-prototype',
|
|
190
227
|
title: 'Interaction Prototype',
|
|
191
|
-
description:
|
|
192
|
-
|
|
228
|
+
description:
|
|
229
|
+
'Throwaway interaction or motion sandbox with a live stage, controls, annotations, and copyable config.',
|
|
230
|
+
useWhen:
|
|
231
|
+
'Use for animation tuning, click-through sketches, draggable behavior studies, and interaction questions that prose cannot answer.',
|
|
193
232
|
defaultSize: { width: 1040, height: 760 },
|
|
194
|
-
dataShape:
|
|
233
|
+
dataShape:
|
|
234
|
+
'{ scenario, controls: [{ key, label, value, min?, max?, unit? }], screens: [{ title, detail }], annotations: [{ title, detail }], questions: string[], snippet? }',
|
|
195
235
|
example: {
|
|
196
236
|
kind: 'interaction-prototype',
|
|
197
237
|
title: 'Sidebar Motion Study',
|
|
@@ -234,17 +274,25 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
234
274
|
{
|
|
235
275
|
kind: 'presentation',
|
|
236
276
|
title: 'HTML Presentation',
|
|
237
|
-
description:
|
|
238
|
-
|
|
277
|
+
description:
|
|
278
|
+
'PowerPoint-style fullscreen-ready HTML presentation with slide navigation, progress, speaker notes, and presentation metadata.',
|
|
279
|
+
useWhen:
|
|
280
|
+
'Use when the human asks for a presentation, pitch deck, briefing, workshop walkthrough, or PowerPoint-like deliverable.',
|
|
239
281
|
defaultSize: { width: 1120, height: 700 },
|
|
240
|
-
dataShape:
|
|
282
|
+
dataShape:
|
|
283
|
+
'{ subtitle?, theme?: "canvas"|"midnight"|"paper"|"aurora"|{ bg?, panel?, surface?, border?, text?, textSecondary?, textMuted?, accent? }, slides: [{ title, kicker?, body?, bullets?: string[], metrics?: [{ label, value, detail? }], note? }] }',
|
|
241
284
|
example: {
|
|
242
285
|
kind: 'presentation',
|
|
243
286
|
title: 'Project Briefing',
|
|
244
287
|
data: {
|
|
245
288
|
subtitle: 'A meeting-ready narrative for review.',
|
|
246
289
|
slides: [
|
|
247
|
-
{
|
|
290
|
+
{
|
|
291
|
+
title: 'Why this matters',
|
|
292
|
+
kicker: '01',
|
|
293
|
+
body: 'Frame the decision and outcome.',
|
|
294
|
+
bullets: ['Human-readable', 'Fullscreen-ready'],
|
|
295
|
+
},
|
|
248
296
|
{ title: 'What changes', kicker: '02', bullets: ['Show the before/after', 'End with clear next steps'] },
|
|
249
297
|
],
|
|
250
298
|
},
|
|
@@ -254,14 +302,22 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
254
302
|
kind: 'illustration-set',
|
|
255
303
|
title: 'Illustration Set',
|
|
256
304
|
description: 'Inline SVG figure sheet with captions and per-figure SVG copy/export controls.',
|
|
257
|
-
useWhen:
|
|
305
|
+
useWhen:
|
|
306
|
+
'Use for blog figures, architecture illustrations, conceptual diagrams, and vector sketches that should be tweakable or pasteable.',
|
|
258
307
|
defaultSize: { width: 1040, height: 760 },
|
|
259
|
-
dataShape:
|
|
308
|
+
dataShape:
|
|
309
|
+
'{ figures: [{ title, caption, shapes: [{ type, x, y, width, height, cx, cy, r, x1, y1, x2, y2, text, color }] }] }',
|
|
260
310
|
example: {
|
|
261
311
|
kind: 'illustration-set',
|
|
262
312
|
title: 'Article Figures',
|
|
263
313
|
data: {
|
|
264
|
-
figures: [
|
|
314
|
+
figures: [
|
|
315
|
+
{
|
|
316
|
+
title: 'Feedback Loop',
|
|
317
|
+
caption: 'Human and agent exchange context.',
|
|
318
|
+
shapes: [{ type: 'rect', x: 40, y: 50, width: 130, height: 70, text: 'Human' }],
|
|
319
|
+
},
|
|
320
|
+
],
|
|
265
321
|
},
|
|
266
322
|
},
|
|
267
323
|
},
|
|
@@ -271,7 +327,8 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
271
327
|
description: 'Readable explainer with TLDR, collapsible steps, annotated snippets, FAQ, and glossary.',
|
|
272
328
|
useWhen: 'Use to teach how a feature, algorithm, or code path works after inspecting repo context.',
|
|
273
329
|
defaultSize: { width: 980, height: 760 },
|
|
274
|
-
dataShape:
|
|
330
|
+
dataShape:
|
|
331
|
+
'{ summary, steps: [{ title, detail }], snippets: [{ label, code, note }], faq: [{ q, a }], glossary: [{ term, definition }] }',
|
|
275
332
|
example: {
|
|
276
333
|
kind: 'explainer',
|
|
277
334
|
title: 'Rate Limiter Explainer',
|
|
@@ -284,10 +341,12 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
284
341
|
{
|
|
285
342
|
kind: 'incident-report',
|
|
286
343
|
title: 'Incident Report',
|
|
287
|
-
description:
|
|
344
|
+
description:
|
|
345
|
+
'Post-incident report with impact metrics, minute-by-minute timeline, root cause, log excerpts, and action checklist.',
|
|
288
346
|
useWhen: 'Use for incident summaries, post-mortems, reliability reviews, and follow-up tracking.',
|
|
289
347
|
defaultSize: { width: 1040, height: 760 },
|
|
290
|
-
dataShape:
|
|
348
|
+
dataShape:
|
|
349
|
+
'{ severity, status, duration, summary, impact: [{ label, value, tone }], timeline: [{ time, event, detail, tone }], rootCause, logs?: string, actions: [{ done, owner, description, due }] }',
|
|
291
350
|
example: {
|
|
292
351
|
kind: 'incident-report',
|
|
293
352
|
title: 'API Latency Incident',
|
|
@@ -304,7 +363,8 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
304
363
|
description: 'Skimmable report with metrics, shipped/slipped lists, blockers, and next actions.',
|
|
305
364
|
useWhen: 'Use for weekly updates, project health, incident summaries, and leadership-ready status.',
|
|
306
365
|
defaultSize: { width: 980, height: 720 },
|
|
307
|
-
dataShape:
|
|
366
|
+
dataShape:
|
|
367
|
+
'{ metrics: [{ label, value, tone }], shipped: string[], slipped: string[], risks: string[], next: string[] }',
|
|
308
368
|
example: {
|
|
309
369
|
kind: 'status-report',
|
|
310
370
|
title: 'Weekly Canvas Status',
|
|
@@ -325,7 +385,14 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
325
385
|
kind: 'triage-board',
|
|
326
386
|
title: 'Ticket Triage',
|
|
327
387
|
data: {
|
|
328
|
-
items: [
|
|
388
|
+
items: [
|
|
389
|
+
{
|
|
390
|
+
title: 'Fix flaky smoke test',
|
|
391
|
+
detail: 'Fails on CI timeout.',
|
|
392
|
+
column: 'Now',
|
|
393
|
+
rationale: 'Blocks release.',
|
|
394
|
+
},
|
|
395
|
+
],
|
|
329
396
|
},
|
|
330
397
|
},
|
|
331
398
|
},
|
|
@@ -340,7 +407,9 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
|
|
|
340
407
|
kind: 'config-editor',
|
|
341
408
|
title: 'Feature Flags',
|
|
342
409
|
data: {
|
|
343
|
-
flags: [
|
|
410
|
+
flags: [
|
|
411
|
+
{ key: 'newCheckout', label: 'New checkout', area: 'Checkout', enabled: false, requires: ['paymentsV2'] },
|
|
412
|
+
],
|
|
344
413
|
},
|
|
345
414
|
},
|
|
346
415
|
},
|
|
@@ -381,36 +450,57 @@ function strings(value: unknown): string[] {
|
|
|
381
450
|
return value.map((item) => text(item).trim()).filter(Boolean);
|
|
382
451
|
}
|
|
383
452
|
|
|
384
|
-
function fieldRecords(
|
|
453
|
+
function fieldRecords(
|
|
454
|
+
data: Record<string, unknown>,
|
|
455
|
+
key: string,
|
|
456
|
+
fallback: Record<string, unknown>[],
|
|
457
|
+
): Record<string, unknown>[] {
|
|
385
458
|
const found = records(data[key]);
|
|
386
459
|
return found.length > 0 ? found : fallback;
|
|
387
460
|
}
|
|
388
461
|
|
|
389
462
|
const DEFAULT_DECK_SLIDES: Record<string, unknown>[] = [
|
|
390
|
-
{
|
|
391
|
-
|
|
463
|
+
{
|
|
464
|
+
title: 'HTML keeps humans in the loop',
|
|
465
|
+
kicker: 'Thesis',
|
|
466
|
+
bullets: ['Higher information density', 'Visual clarity', 'Two-way interaction'],
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
title: 'Use the lightest tier that works',
|
|
470
|
+
bullets: [
|
|
471
|
+
'json-render for structured UI',
|
|
472
|
+
'html primitives for rich documents',
|
|
473
|
+
'web artifacts for full React apps',
|
|
474
|
+
],
|
|
475
|
+
},
|
|
392
476
|
];
|
|
393
477
|
|
|
394
478
|
const DEFAULT_PRESENTATION_SLIDES: Record<string, unknown>[] = [
|
|
395
|
-
{
|
|
396
|
-
|
|
479
|
+
{
|
|
480
|
+
title: 'Set the frame',
|
|
481
|
+
kicker: '01',
|
|
482
|
+
body: 'Open with the decision, audience, and outcome this presentation supports.',
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
title: 'Show the evidence',
|
|
486
|
+
kicker: '02',
|
|
487
|
+
bullets: ['Use concrete facts', 'Keep one idea per slide', 'Make risks visible'],
|
|
488
|
+
},
|
|
397
489
|
{ title: 'Close with action', kicker: '03', bullets: ['Decision needed', 'Owner and next step', 'Timing'] },
|
|
398
490
|
];
|
|
399
491
|
|
|
400
|
-
function presentationSlides(
|
|
492
|
+
function presentationSlides(
|
|
493
|
+
data: Record<string, unknown>,
|
|
494
|
+
fallback = DEFAULT_PRESENTATION_SLIDES,
|
|
495
|
+
): Record<string, unknown>[] {
|
|
401
496
|
return fieldRecords(data, 'slides', fallback);
|
|
402
497
|
}
|
|
403
498
|
|
|
404
|
-
function enrichPresentationData(
|
|
405
|
-
kind: HtmlPrimitiveKind,
|
|
406
|
-
data: Record<string, unknown>,
|
|
407
|
-
): Record<string, unknown> {
|
|
499
|
+
function enrichPresentationData(kind: HtmlPrimitiveKind, data: Record<string, unknown>): Record<string, unknown> {
|
|
408
500
|
if (kind !== 'deck' && kind !== 'presentation') return data;
|
|
409
501
|
const slides = presentationSlides(data, kind === 'deck' ? DEFAULT_DECK_SLIDES : DEFAULT_PRESENTATION_SLIDES);
|
|
410
502
|
const slideTitles = slides.map((slide, index) => itemTitle(slide, `Slide ${index + 1}`));
|
|
411
|
-
const speakerNotes = slides
|
|
412
|
-
.map((slide) => text(slide.note).trim())
|
|
413
|
-
.filter(Boolean);
|
|
503
|
+
const speakerNotes = slides.map((slide) => text(slide.note).trim()).filter(Boolean);
|
|
414
504
|
const theme = kind === 'presentation' ? presentationThemeMetadata(data) : undefined;
|
|
415
505
|
return {
|
|
416
506
|
...data,
|
|
@@ -517,7 +607,9 @@ function isPresentationThemeName(value: string): value is PresentationThemeName
|
|
|
517
607
|
|
|
518
608
|
function parsePresentationThemeName(value: string, field = 'theme'): PresentationThemeName {
|
|
519
609
|
if (isPresentationThemeName(value)) return value;
|
|
520
|
-
throw new Error(
|
|
610
|
+
throw new Error(
|
|
611
|
+
`Invalid presentation ${field} "${value}". Use canvas, midnight, paper, aurora, or a custom theme object.`,
|
|
612
|
+
);
|
|
521
613
|
}
|
|
522
614
|
|
|
523
615
|
function presentationTheme(data: Record<string, unknown>): PresentationThemeTokens {
|
|
@@ -526,9 +618,7 @@ function presentationTheme(data: Record<string, unknown>): PresentationThemeToke
|
|
|
526
618
|
return PRESENTATION_THEMES[parsePresentationThemeName(raw)];
|
|
527
619
|
}
|
|
528
620
|
if (!isRecord(raw)) return PRESENTATION_THEMES.canvas;
|
|
529
|
-
const baseName = typeof raw.base === 'string'
|
|
530
|
-
? parsePresentationThemeName(raw.base, 'theme base')
|
|
531
|
-
: 'canvas';
|
|
621
|
+
const baseName = typeof raw.base === 'string' ? parsePresentationThemeName(raw.base, 'theme base') : 'canvas';
|
|
532
622
|
const base = PRESENTATION_THEMES[baseName];
|
|
533
623
|
const readColor = (key: string, fallback: string): string => {
|
|
534
624
|
const value = text(raw[key]);
|
|
@@ -570,7 +660,9 @@ function number(value: unknown, fallback = 0): number {
|
|
|
570
660
|
function inlineSvgShape(shape: Record<string, unknown>, index: number): string {
|
|
571
661
|
const color = safeCssColor(text(shape.color, '#4a9eff'));
|
|
572
662
|
const label = text(shape.text, text(shape.label));
|
|
573
|
-
const labelText = label
|
|
663
|
+
const labelText = label
|
|
664
|
+
? `<text x="${number(shape.x, number(shape.cx, 80)) + 12}" y="${number(shape.y, number(shape.cy, 80)) + 28}" fill="var(--color-text, #e8edf2)" font-size="13" font-weight="700">${escapeHtml(label)}</text>`
|
|
665
|
+
: '';
|
|
574
666
|
switch (text(shape.type, 'rect')) {
|
|
575
667
|
case 'circle':
|
|
576
668
|
return `<circle cx="${number(shape.cx, 80 + index * 40)}" cy="${number(shape.cy, 80)}" r="${number(shape.r, 32)}" fill="${color}" opacity="0.22" stroke="${color}" stroke-width="2" />${label ? `<text x="${number(shape.cx, 80 + index * 40)}" y="${number(shape.cy, 80) + 4}" text-anchor="middle" fill="var(--color-text, #e8edf2)" font-size="13" font-weight="700">${escapeHtml(label)}</text>` : ''}`;
|
|
@@ -597,13 +689,23 @@ function itemTitle(item: Record<string, unknown>, fallback: string): string {
|
|
|
597
689
|
|
|
598
690
|
function badge(value: string): string {
|
|
599
691
|
const normalized = value.toLowerCase();
|
|
600
|
-
const tone =
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
692
|
+
const tone =
|
|
693
|
+
normalized.includes('block') ||
|
|
694
|
+
normalized.includes('fail') ||
|
|
695
|
+
normalized.includes('danger') ||
|
|
696
|
+
normalized.includes('critical')
|
|
697
|
+
? 'danger'
|
|
698
|
+
: normalized.includes('warn') ||
|
|
699
|
+
normalized.includes('risk') ||
|
|
700
|
+
normalized.includes('progress') ||
|
|
701
|
+
normalized.includes('later')
|
|
702
|
+
? 'warn'
|
|
703
|
+
: normalized.includes('ok') ||
|
|
704
|
+
normalized.includes('done') ||
|
|
705
|
+
normalized.includes('ship') ||
|
|
706
|
+
normalized.includes('green')
|
|
707
|
+
? 'ok'
|
|
708
|
+
: 'info';
|
|
607
709
|
return `<span class="badge ${tone}">${escapeHtml(value)}</span>`;
|
|
608
710
|
}
|
|
609
711
|
|
|
@@ -727,11 +829,31 @@ ${input.script ?? ''}
|
|
|
727
829
|
|
|
728
830
|
function renderChoiceGrid({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
729
831
|
const items = fieldRecords(data, 'items', [
|
|
730
|
-
{
|
|
731
|
-
|
|
732
|
-
|
|
832
|
+
{
|
|
833
|
+
title: 'Option A',
|
|
834
|
+
summary: 'Conservative path with minimal code churn.',
|
|
835
|
+
tradeoff: 'Lower upside, lower risk.',
|
|
836
|
+
pros: ['Quick to ship', 'Easy to review'],
|
|
837
|
+
cons: ['Less flexible'],
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
title: 'Option B',
|
|
841
|
+
summary: 'Balanced refactor with clearer seams.',
|
|
842
|
+
tradeoff: 'More code touched.',
|
|
843
|
+
pros: ['Better long-term shape'],
|
|
844
|
+
cons: ['Requires more tests'],
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
title: 'Option C',
|
|
848
|
+
summary: 'Purpose-built new layer.',
|
|
849
|
+
tradeoff: 'Best UX, highest implementation cost.',
|
|
850
|
+
pros: ['Distinctive output'],
|
|
851
|
+
cons: ['More maintenance'],
|
|
852
|
+
},
|
|
733
853
|
]);
|
|
734
|
-
const body = `<section class="grid">${items
|
|
854
|
+
const body = `<section class="grid">${items
|
|
855
|
+
.map(
|
|
856
|
+
(item, index) => `
|
|
735
857
|
<article class="card ${index === 0 ? 'emphasis' : ''}">
|
|
736
858
|
<div class="small">Choice ${index + 1}</div>
|
|
737
859
|
<h2>${escapeHtml(itemTitle(item, `Option ${index + 1}`))}</h2>
|
|
@@ -739,7 +861,9 @@ function renderChoiceGrid({ title, data, descriptor }: Parameters<PrimitiveRende
|
|
|
739
861
|
${text(item.tradeoff) ? `<p><strong>Tradeoff:</strong> ${escapeHtml(text(item.tradeoff))}</p>` : ''}
|
|
740
862
|
<div class="two"><div><h3>Pros</h3>${list(strings(item.pros))}</div><div><h3>Cons</h3>${list(strings(item.cons))}</div></div>
|
|
741
863
|
${codeBlock(item.code)}
|
|
742
|
-
</article
|
|
864
|
+
</article>`,
|
|
865
|
+
)
|
|
866
|
+
.join('')}</section>`;
|
|
743
867
|
return page({ title, kind: 'choice-grid', summary: descriptor.description, data, body });
|
|
744
868
|
}
|
|
745
869
|
|
|
@@ -755,10 +879,16 @@ function renderPlanTimeline({ title, data, descriptor }: Parameters<PrimitiveRen
|
|
|
755
879
|
{ from: 'Primitive catalog', to: 'HTML node', label: 'renders iframe HTML' },
|
|
756
880
|
{ from: 'Human', to: 'Agent', label: 'exports edited JSON/prompt' },
|
|
757
881
|
]);
|
|
758
|
-
const risks = fieldRecords(data, 'risks', [
|
|
882
|
+
const risks = fieldRecords(data, 'risks', [
|
|
883
|
+
{ risk: 'Overly generic output', mitigation: 'Use named primitives with clear use cases.' },
|
|
884
|
+
]);
|
|
759
885
|
const snippets = fieldRecords(data, 'snippets', []);
|
|
760
|
-
const body = `<section class="two"><div class="panel"><h2>Milestones</h2><div class="timeline">${milestones
|
|
761
|
-
|
|
886
|
+
const body = `<section class="two"><div class="panel"><h2>Milestones</h2><div class="timeline">${milestones
|
|
887
|
+
.map(
|
|
888
|
+
(item, index) => `
|
|
889
|
+
<div class="step"><div class="dot">${index + 1}</div><div class="card"><h3>${escapeHtml(itemTitle(item, `Milestone ${index + 1}`))} ${badge(text(item.status, 'planned'))}</h3><p>${escapeHtml(text(item.detail, 'Add implementation detail.'))}</p></div></div>`,
|
|
890
|
+
)
|
|
891
|
+
.join('')}</div></div>
|
|
762
892
|
<div class="panel"><h2>Data Flow</h2><div class="flow">${flow.map((item, index) => `<div class="flow-node"><h3>${escapeHtml(text(item.from, `Step ${index + 1}`))}</h3><p>${escapeHtml(text(item.label, 'flows to'))}</p><strong>${escapeHtml(text(item.to, 'Next'))}</strong></div>${index < flow.length - 1 ? '<div class="arrow">-></div>' : ''}`).join('')}</div><h2 style="margin-top:18px">Risks</h2>${risks.map((item) => `<div class="card"><strong>${escapeHtml(text(item.risk, 'Risk'))}</strong><p>${escapeHtml(text(item.mitigation, 'Mitigation'))}</p></div>`).join('')}</div></section>
|
|
763
893
|
${snippets.length > 0 ? `<section class="panel" style="margin-top:14px"><h2>Code Checkpoints</h2>${snippets.map((item) => `<h3>${escapeHtml(itemTitle(item, 'Snippet'))}</h3>${codeBlock(item.code)}`).join('')}</section>` : ''}`;
|
|
764
894
|
return page({ title, kind: 'plan-timeline', summary: descriptor.description, data, body });
|
|
@@ -766,19 +896,31 @@ function renderPlanTimeline({ title, data, descriptor }: Parameters<PrimitiveRen
|
|
|
766
896
|
|
|
767
897
|
function renderReviewSheet({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
768
898
|
const findings = fieldRecords(data, 'findings', [
|
|
769
|
-
{
|
|
899
|
+
{
|
|
900
|
+
severity: 'warning',
|
|
901
|
+
title: 'Review focus',
|
|
902
|
+
file: 'src/example.ts',
|
|
903
|
+
line: 1,
|
|
904
|
+
detail: 'Explain the risky behavior and what to inspect.',
|
|
905
|
+
},
|
|
770
906
|
]);
|
|
771
907
|
const files = fieldRecords(data, 'files', []);
|
|
772
908
|
const diff = text(data.diff);
|
|
773
|
-
const body = `<section class="two"><div class="panel"><h2>Findings</h2>${findings
|
|
774
|
-
|
|
909
|
+
const body = `<section class="two"><div class="panel"><h2>Findings</h2>${findings
|
|
910
|
+
.map(
|
|
911
|
+
(item) => `
|
|
912
|
+
<article class="card"><h3>${badge(text(item.severity, 'info'))} ${escapeHtml(itemTitle(item, 'Finding'))}</h3><p class="small">${escapeHtml([text(item.file), text(item.line)].filter(Boolean).join(':'))}</p><p>${escapeHtml(text(item.detail, 'Add review note.'))}</p></article>`,
|
|
913
|
+
)
|
|
914
|
+
.join('')}</div>
|
|
775
915
|
<div class="panel"><h2>Review Tour</h2>${files.length > 0 ? files.map((item) => `<article class="card"><h3>${escapeHtml(text(item.path, 'File'))}</h3><p>${escapeHtml(text(item.why, 'Why this file matters.'))}</p></article>`).join('') : '<p class="muted">Add files with path and why fields for a guided review.</p>'}</div></section>
|
|
776
916
|
${diff ? `<section class="panel" style="margin-top:14px"><h2>Diff Excerpt</h2>${codeBlock(diff)}</section>` : ''}`;
|
|
777
917
|
return page({ title, kind: 'review-sheet', summary: descriptor.description, data, body });
|
|
778
918
|
}
|
|
779
919
|
|
|
780
920
|
function renderPrWriteup({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
781
|
-
const files = fieldRecords(data, 'files', [
|
|
921
|
+
const files = fieldRecords(data, 'files', [
|
|
922
|
+
{ path: 'src/example.ts', why: 'Core behavior changed here.', focus: 'Review edge cases and tests.' },
|
|
923
|
+
]);
|
|
782
924
|
const body = `<section class="panel"><h2>Summary</h2><p>${escapeHtml(text(data.summary, 'Summarize the change in one reviewer-friendly paragraph.'))}</p><p>${escapeHtml(text(data.why, 'Explain why this change matters now.'))}</p></section>
|
|
783
925
|
<section class="two" style="margin-top:14px"><div class="card"><h2>Before</h2>${list(fieldStrings(data, 'before', ['Current behavior or pain point.']))}</div><div class="card emphasis"><h2>After</h2>${list(fieldStrings(data, 'after', ['New behavior or reviewer-visible outcome.']))}</div></section>
|
|
784
926
|
<section class="three" style="margin-top:14px"><div class="panel"><h2>File Tour</h2>${files.map((file) => `<details open><summary><strong>${escapeHtml(text(file.path, 'File'))}</strong></summary><p>${escapeHtml(text(file.why, 'Why this file matters.'))}</p><p class="small">Focus: ${escapeHtml(text(file.focus, 'Review behavior and tests.'))}</p></details>`).join('')}</div>
|
|
@@ -831,7 +973,9 @@ function renderCodeWalkthrough({ title, data, descriptor }: Parameters<Primitive
|
|
|
831
973
|
{ id: 'core', title: 'Core Logic', detail: 'State change or main computation.', role: 'core' },
|
|
832
974
|
{ id: 'view', title: 'Renderer', detail: 'User-visible result.', role: 'view' },
|
|
833
975
|
]);
|
|
834
|
-
const steps = fieldRecords(data, 'steps', [
|
|
976
|
+
const steps = fieldRecords(data, 'steps', [
|
|
977
|
+
{ title: 'Trace the path', file: 'src/example.ts', detail: 'Explain the first important hop.', code: '' },
|
|
978
|
+
]);
|
|
835
979
|
const keyFiles = fieldRecords(data, 'keyFiles', []);
|
|
836
980
|
const edges = fieldRecords(data, 'edges', []);
|
|
837
981
|
const body = `<section class="panel"><h2>Path Summary</h2><p>${escapeHtml(text(data.summary, 'Explain the code path this walkthrough covers.'))}</p><div class="flow" style="margin-top:12px">${modules.map((module) => `<div class="flow-node"><div class="small">${escapeHtml(text(module.role, text(module.id, 'module')))}</div><h3>${escapeHtml(itemTitle(module, 'Module'))}</h3><p>${escapeHtml(text(module.detail, ''))}</p></div>`).join('<div class="arrow">-></div>')}</div>${edges.length > 0 ? `<p class="small" style="margin-top:10px">Edges: ${escapeHtml(edges.map((edge) => `${text(edge.from)} -> ${text(edge.to)}${text(edge.label) ? ` (${text(edge.label)})` : ''}`).join(', '))}</p>` : ''}</section>
|
|
@@ -843,14 +987,26 @@ function renderCodeWalkthrough({ title, data, descriptor }: Parameters<Primitive
|
|
|
843
987
|
|
|
844
988
|
function renderDesignSheet({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
845
989
|
const directions = fieldRecords(data, 'directions', [
|
|
846
|
-
{
|
|
847
|
-
|
|
990
|
+
{
|
|
991
|
+
title: 'Editorial dense',
|
|
992
|
+
tone: 'serious, information-rich',
|
|
993
|
+
palette: ['#f4efe5', '#17120f', '#c84f2f'],
|
|
994
|
+
rationale: 'Good when humans need to compare many details quickly.',
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
title: 'Control-room dark',
|
|
998
|
+
tone: 'operational, high contrast',
|
|
999
|
+
palette: ['#07111d', '#dce8f2', '#4a9eff'],
|
|
1000
|
+
rationale: 'Good for dashboards and incident views.',
|
|
1001
|
+
},
|
|
848
1002
|
]);
|
|
849
1003
|
const tokens = fieldRecords(data, 'tokens', []);
|
|
850
|
-
const body = `<section class="grid">${directions
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1004
|
+
const body = `<section class="grid">${directions
|
|
1005
|
+
.map((item) => {
|
|
1006
|
+
const palette = strings(item.palette);
|
|
1007
|
+
return `<article class="card"><h2>${escapeHtml(itemTitle(item, 'Direction'))}</h2><p>${escapeHtml(text(item.tone, 'Tone'))}</p><div class="swatches">${palette.map((color) => `<span class="swatch" title="${escapeHtml(color)}" style="background:${safeCssColor(color)}"></span>`).join('')}</div><p>${escapeHtml(text(item.rationale, 'Rationale'))}</p></article>`;
|
|
1008
|
+
})
|
|
1009
|
+
.join('')}</section>
|
|
854
1010
|
${tokens.length > 0 ? `<section class="panel" style="margin-top:14px"><h2>Tokens</h2><div class="grid">${tokens.map((item) => `<div class="card"><strong>${escapeHtml(itemTitle(item, 'Token'))}</strong><p>${escapeHtml(text(item.value, ''))}</p></div>`).join('')}</div></section>` : ''}`;
|
|
855
1011
|
return page({ title, kind: 'design-sheet', summary: descriptor.description, data, body });
|
|
856
1012
|
}
|
|
@@ -860,7 +1016,13 @@ function renderComponentGallery({ title, data, descriptor }: Parameters<Primitiv
|
|
|
860
1016
|
const variants = fieldRecords(data, 'variants', [
|
|
861
1017
|
{ label: 'Primary', state: 'default', intent: 'Main action', example: 'Continue', note: 'High emphasis.' },
|
|
862
1018
|
{ label: 'Secondary', state: 'hover', intent: 'Alternative action', example: 'Back', note: 'Lower emphasis.' },
|
|
863
|
-
{
|
|
1019
|
+
{
|
|
1020
|
+
label: 'Destructive',
|
|
1021
|
+
state: 'disabled',
|
|
1022
|
+
intent: 'Danger zone',
|
|
1023
|
+
example: 'Delete',
|
|
1024
|
+
note: 'Requires confirmation.',
|
|
1025
|
+
},
|
|
864
1026
|
]);
|
|
865
1027
|
const body = `<section class="panel"><h2>${escapeHtml(component)}</h2><p>Variant contact sheet for fast visual review.</p></section>
|
|
866
1028
|
<section class="grid" style="margin-top:14px">${variants.map((item) => `<article class="card"><div class="small">${escapeHtml(text(item.state, 'state'))} / ${escapeHtml(text(item.intent, 'intent'))}</div><h2>${escapeHtml(itemTitle(item, 'Variant'))}</h2><button type="button">${escapeHtml(text(item.example, itemTitle(item, 'Example')))}</button><p>${escapeHtml(text(item.note, ''))}</p></article>`).join('')}</section>`;
|
|
@@ -868,13 +1030,17 @@ function renderComponentGallery({ title, data, descriptor }: Parameters<Primitiv
|
|
|
868
1030
|
}
|
|
869
1031
|
|
|
870
1032
|
function renderInteractionPrototype({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
871
|
-
const controls = fieldRecords(data, 'controls', [
|
|
1033
|
+
const controls = fieldRecords(data, 'controls', [
|
|
1034
|
+
{ key: 'duration', label: 'Duration', value: 280, min: 100, max: 900, unit: 'ms' },
|
|
1035
|
+
]);
|
|
872
1036
|
const screens = fieldRecords(data, 'screens', [
|
|
873
1037
|
{ title: 'Start', detail: 'Initial state before interaction.' },
|
|
874
1038
|
{ title: 'Active', detail: 'The user has started the interaction.' },
|
|
875
1039
|
{ title: 'Done', detail: 'Final state after completion.' },
|
|
876
1040
|
]);
|
|
877
|
-
const annotations = fieldRecords(data, 'annotations', [
|
|
1041
|
+
const annotations = fieldRecords(data, 'annotations', [
|
|
1042
|
+
{ title: 'Decision', detail: 'Tune the values until this feels right.' },
|
|
1043
|
+
]);
|
|
878
1044
|
const body = `<section class="three"><div class="panel"><h2>Stage</h2><p>${escapeHtml(text(data.scenario, 'Describe the interaction this prototype evaluates.'))}</p><div class="flow" style="margin-top:16px">${screens.map((screen, index) => `<button class="flow-node" type="button" data-screen="${index}"><h3>${escapeHtml(itemTitle(screen, 'Screen'))}</h3><p>${escapeHtml(text(screen.detail, ''))}</p></button>`).join('<div class="arrow">-></div>')}</div><div class="card emphasis" id="prototype-readout" style="margin-top:14px">Select a screen to inspect it.</div></div>
|
|
879
1045
|
<div class="panel"><h2>Controls</h2>${controls.map((control, index) => `<label class="card"><span class="small">${escapeHtml(text(control.key, `control${index}`))}</span><h3>${escapeHtml(text(control.label, 'Control'))}: <span data-control-value="${index}">${escapeHtml(text(control.value, '0'))}</span>${escapeHtml(text(control.unit, ''))}</h3><input type="range" data-control="${index}" min="${number(control.min, 0)}" max="${number(control.max, 1000)}" value="${number(control.value, 0)}"></label>`).join('')}</div>
|
|
880
1046
|
<div class="panel sticky"><h2>Notes</h2>${annotations.map((item) => `<article class="card"><h3>${escapeHtml(itemTitle(item, 'Note'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p></article>`).join('')}<h2 style="margin-top:18px">Questions</h2>${list(fieldStrings(data, 'questions', ['Does the timing feel responsive?', 'What should persist after completion?']))}${codeBlock(data.snippet)}</div></section>`;
|
|
@@ -924,11 +1090,23 @@ document.querySelectorAll('[data-step]').forEach((button) => button.addEventList
|
|
|
924
1090
|
}
|
|
925
1091
|
|
|
926
1092
|
function renderIllustrationSet({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
927
|
-
const figures = fieldRecords(data, 'figures', [
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1093
|
+
const figures = fieldRecords(data, 'figures', [
|
|
1094
|
+
{
|
|
1095
|
+
title: 'System Loop',
|
|
1096
|
+
caption: 'A small editable SVG-style figure.',
|
|
1097
|
+
shapes: [
|
|
1098
|
+
{ type: 'rect', x: 40, y: 60, width: 160, height: 72, text: 'Agent', color: '#4a9eff' },
|
|
1099
|
+
{ type: 'arrow', x1: 210, y1: 96, x2: 340, y2: 96, color: '#eab308' },
|
|
1100
|
+
{ type: 'circle', cx: 420, cy: 96, r: 42, text: 'Human', color: '#22c55e' },
|
|
1101
|
+
],
|
|
1102
|
+
},
|
|
1103
|
+
]);
|
|
1104
|
+
const body = `<section class="grid">${figures
|
|
1105
|
+
.map((figure, index) => {
|
|
1106
|
+
const shapes = records(figure.shapes);
|
|
1107
|
+
return `<article class="card figure"><h2>${escapeHtml(itemTitle(figure, `Figure ${index + 1}`))}</h2><svg viewBox="0 0 560 260" role="img" aria-label="${escapeHtml(itemTitle(figure, `Figure ${index + 1}`))}" data-figure="${index}"><defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="currentColor"></path></marker></defs>${shapes.map(inlineSvgShape).join('')}</svg><p>${escapeHtml(text(figure.caption, ''))}</p><button type="button" data-copy-svg="${index}">Copy SVG</button></article>`;
|
|
1108
|
+
})
|
|
1109
|
+
.join('')}</section>`;
|
|
932
1110
|
return page({
|
|
933
1111
|
title,
|
|
934
1112
|
kind: 'illustration-set',
|
|
@@ -973,9 +1151,10 @@ function renderPresentation({ title, data, descriptor }: Parameters<PrimitiveRen
|
|
|
973
1151
|
const theme = presentationTheme(data);
|
|
974
1152
|
const accentOverride = safeCssColor(text(data.accent, ''));
|
|
975
1153
|
const accent = accentOverride === 'transparent' ? theme.accent : accentOverride;
|
|
976
|
-
const slideMarkup = slides
|
|
977
|
-
|
|
978
|
-
|
|
1154
|
+
const slideMarkup = slides
|
|
1155
|
+
.map((item, index) => {
|
|
1156
|
+
const metrics = records(item.metrics);
|
|
1157
|
+
return `<article class="slide ${index === 0 ? 'active' : ''}" data-slide="${index}">
|
|
979
1158
|
<div class="slide-grid ${metrics.length > 0 ? 'with-metrics' : 'without-metrics'}">
|
|
980
1159
|
<div class="slide-copy">
|
|
981
1160
|
<div class="kicker">${escapeHtml(text(item.kicker, `Slide ${index + 1}`))}</div>
|
|
@@ -987,7 +1166,8 @@ function renderPresentation({ title, data, descriptor }: Parameters<PrimitiveRen
|
|
|
987
1166
|
</div>
|
|
988
1167
|
${text(item.note) ? `<aside class="speaker-note"><span>Speaker note</span>${escapeHtml(text(item.note))}</aside>` : ''}
|
|
989
1168
|
</article>`;
|
|
990
|
-
|
|
1169
|
+
})
|
|
1170
|
+
.join('');
|
|
991
1171
|
|
|
992
1172
|
return `<!doctype html>
|
|
993
1173
|
<html lang="en">
|
|
@@ -1088,7 +1268,9 @@ showSlide(0);
|
|
|
1088
1268
|
}
|
|
1089
1269
|
|
|
1090
1270
|
function renderExplainer({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
1091
|
-
const steps = fieldRecords(data, 'steps', [
|
|
1271
|
+
const steps = fieldRecords(data, 'steps', [
|
|
1272
|
+
{ title: 'Start here', detail: 'Add request path, data flow, or concept steps.' },
|
|
1273
|
+
]);
|
|
1092
1274
|
const snippets = fieldRecords(data, 'snippets', []);
|
|
1093
1275
|
const faq = fieldRecords(data, 'faq', []);
|
|
1094
1276
|
const glossary = fieldRecords(data, 'glossary', []);
|
|
@@ -1100,7 +1282,10 @@ function renderExplainer({ title, data, descriptor }: Parameters<PrimitiveRender
|
|
|
1100
1282
|
}
|
|
1101
1283
|
|
|
1102
1284
|
function renderStatusReport({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
1103
|
-
const metrics = fieldRecords(data, 'metrics', [
|
|
1285
|
+
const metrics = fieldRecords(data, 'metrics', [
|
|
1286
|
+
{ label: 'Health', value: 'on track', tone: 'ok' },
|
|
1287
|
+
{ label: 'Risk', value: 'medium', tone: 'warn' },
|
|
1288
|
+
]);
|
|
1104
1289
|
const body = `<section class="grid">${metrics.map((item) => `<article class="card"><div class="small">${escapeHtml(text(item.label, 'Metric'))}</div><h2>${escapeHtml(text(item.value, 'Value'))}</h2>${badge(text(item.tone, 'info'))}</article>`).join('')}</section>
|
|
1105
1290
|
<section class="grid" style="margin-top:14px"><article class="card"><h2>Shipped</h2>${list(fieldStrings(data, 'shipped', ['Add shipped items.']))}</article><article class="card"><h2>Slipped</h2>${list(fieldStrings(data, 'slipped', ['Add slipped items.']))}</article><article class="card"><h2>Risks</h2>${list(fieldStrings(data, 'risks', ['Add risks.']))}</article><article class="card"><h2>Next</h2>${list(fieldStrings(data, 'next', ['Add next actions.']))}</article></section>`;
|
|
1106
1291
|
return page({ title, kind: 'status-report', summary: descriptor.description, data, body });
|
|
@@ -1112,8 +1297,12 @@ function renderIncidentReport({ title, data, descriptor }: Parameters<PrimitiveR
|
|
|
1112
1297
|
{ label: 'Status', value: text(data.status, 'resolved'), tone: 'ok' },
|
|
1113
1298
|
{ label: 'Duration', value: text(data.duration, 'unknown'), tone: 'info' },
|
|
1114
1299
|
]);
|
|
1115
|
-
const timeline = fieldRecords(data, 'timeline', [
|
|
1116
|
-
|
|
1300
|
+
const timeline = fieldRecords(data, 'timeline', [
|
|
1301
|
+
{ time: '00:00', event: 'Incident started', detail: 'Add the first observed signal.', tone: 'warn' },
|
|
1302
|
+
]);
|
|
1303
|
+
const actions = fieldRecords(data, 'actions', [
|
|
1304
|
+
{ done: false, owner: 'Unassigned', description: 'Add follow-up action.', due: 'TBD' },
|
|
1305
|
+
]);
|
|
1117
1306
|
const body = `<section class="grid">${impact.map((item) => `<article class="card metric"><div class="small">${escapeHtml(text(item.label, 'Metric'))}</div><strong>${escapeHtml(text(item.value, 'Value'))}</strong>${badge(text(item.tone, 'info'))}</article>`).join('')}</section>
|
|
1118
1307
|
<section class="panel" style="margin-top:14px"><h2>Executive Summary</h2><p>${escapeHtml(text(data.summary, 'Summarize user impact, detection, and resolution.'))}</p></section>
|
|
1119
1308
|
<section class="three" style="margin-top:14px"><div class="panel"><h2>Timeline</h2><div class="timeline">${timeline.map((item, index) => `<div class="step"><div class="dot">${escapeHtml(text(item.time, String(index + 1)))}</div><div class="card"><h3>${escapeHtml(text(item.event, 'Event'))} ${badge(text(item.tone, 'info'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p></div></div>`).join('')}</div></div>
|
|
@@ -1137,10 +1326,33 @@ document.querySelector('[data-copy-actions]')?.addEventListener('click', () => c
|
|
|
1137
1326
|
function renderTriageBoard({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
1138
1327
|
const columns = fieldStrings(data, 'columns', ['Now', 'Next', 'Later', 'Cut']);
|
|
1139
1328
|
const items = fieldRecords(data, 'items', [
|
|
1140
|
-
{
|
|
1141
|
-
|
|
1329
|
+
{
|
|
1330
|
+
title: 'Clarify requirements',
|
|
1331
|
+
detail: 'Human should decide scope boundary.',
|
|
1332
|
+
column: 'Now',
|
|
1333
|
+
rationale: 'Blocks accurate implementation.',
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
title: 'Polish visuals',
|
|
1337
|
+
detail: 'Improve hierarchy after behavior lands.',
|
|
1338
|
+
column: 'Next',
|
|
1339
|
+
rationale: 'Useful but not blocking.',
|
|
1340
|
+
},
|
|
1142
1341
|
]);
|
|
1143
|
-
const body = `<section class="columns">${columns
|
|
1342
|
+
const body = `<section class="columns">${columns
|
|
1343
|
+
.map(
|
|
1344
|
+
(column) =>
|
|
1345
|
+
`<div class="column" data-column="${escapeHtml(column)}"><h2>${escapeHtml(column)}</h2>${items
|
|
1346
|
+
.filter((item) => text(item.column, columns[0]) === column)
|
|
1347
|
+
.map(
|
|
1348
|
+
(item, index) =>
|
|
1349
|
+
`<article class="card ticket" draggable="true" data-ticket="${index}"><h3>${escapeHtml(itemTitle(item, 'Item'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p><p class="small">${escapeHtml(text(item.rationale, ''))}</p></article>`,
|
|
1350
|
+
)
|
|
1351
|
+
.join('')}</div>`,
|
|
1352
|
+
)
|
|
1353
|
+
.join(
|
|
1354
|
+
'',
|
|
1355
|
+
)}</section><p class="small" style="margin-top:12px">Drag cards between columns, then copy JSON or markdown.</p><button type="button" data-copy-markdown>Copy markdown</button>`;
|
|
1144
1356
|
return page({
|
|
1145
1357
|
title,
|
|
1146
1358
|
kind: 'triage-board',
|
|
@@ -1178,8 +1390,21 @@ document.querySelector('[data-copy-markdown]')?.addEventListener('click', () =>
|
|
|
1178
1390
|
|
|
1179
1391
|
function renderConfigEditor({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
1180
1392
|
const flags = fieldRecords(data, 'flags', [
|
|
1181
|
-
{
|
|
1182
|
-
|
|
1393
|
+
{
|
|
1394
|
+
key: 'paymentsV2',
|
|
1395
|
+
label: 'Payments V2',
|
|
1396
|
+
area: 'Checkout',
|
|
1397
|
+
enabled: true,
|
|
1398
|
+
description: 'Required for the new checkout path.',
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
key: 'newCheckout',
|
|
1402
|
+
label: 'New checkout',
|
|
1403
|
+
area: 'Checkout',
|
|
1404
|
+
enabled: false,
|
|
1405
|
+
requires: ['paymentsV2'],
|
|
1406
|
+
description: 'Routes users through the new flow.',
|
|
1407
|
+
},
|
|
1183
1408
|
]);
|
|
1184
1409
|
const body = `<section class="grid">${flags.map((flag, index) => `<label class="card"><div class="small">${escapeHtml(text(flag.area, 'General'))}</div><h3><input type="checkbox" data-flag="${index}" ${flag.enabled === true ? 'checked' : ''}> ${escapeHtml(text(flag.label, text(flag.key, `Flag ${index + 1}`)))}</h3><p>${escapeHtml(text(flag.description, ''))}</p><p class="small">Key: ${escapeHtml(text(flag.key, 'unknown'))}${strings(flag.requires).length > 0 ? ` / requires: ${escapeHtml(strings(flag.requires).join(', '))}` : ''}</p><p class="small" data-warning="${index}"></p></label>`).join('')}</section><button type="button" data-copy-diff style="margin-top:12px">Copy diff</button>`;
|
|
1185
1410
|
return page({
|
|
@@ -1212,8 +1437,13 @@ refreshWarnings();`,
|
|
|
1212
1437
|
}
|
|
1213
1438
|
|
|
1214
1439
|
function renderPromptTuner({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
|
|
1215
|
-
const template = text(
|
|
1216
|
-
|
|
1440
|
+
const template = text(
|
|
1441
|
+
data.template,
|
|
1442
|
+
'Explain {{feature}} for {{audience}}. Include the tradeoffs and one concrete example.',
|
|
1443
|
+
);
|
|
1444
|
+
const samples = fieldRecords(data, 'samples', [
|
|
1445
|
+
{ name: 'Default', variables: { feature: 'PMX Canvas pins', audience: 'coding agents' } },
|
|
1446
|
+
]);
|
|
1217
1447
|
const body = `<section class="two"><div class="panel"><h2>Template</h2><textarea id="template">${escapeHtml(template)}</textarea><p class="small"><span id="char-count">0</span> characters</p></div><div class="panel"><h2>Live Samples</h2><div id="previews"></div><button type="button" data-copy-template>Copy template</button></div></section>`;
|
|
1218
1448
|
return page({
|
|
1219
1449
|
title,
|
|
@@ -1296,9 +1526,8 @@ export function buildHtmlPrimitive(input: HtmlPrimitiveInput): HtmlPrimitiveBuil
|
|
|
1296
1526
|
const data = enrichPresentationData(input.kind, input.data ?? {});
|
|
1297
1527
|
const renderer = RENDERERS[input.kind];
|
|
1298
1528
|
const slideTitles = strings(data.slideTitles);
|
|
1299
|
-
const summary =
|
|
1300
|
-
? `${descriptor.description} Slides: ${slideTitles.join(', ')}.`
|
|
1301
|
-
: descriptor.description;
|
|
1529
|
+
const summary =
|
|
1530
|
+
slideTitles.length > 0 ? `${descriptor.description} Slides: ${slideTitles.join(', ')}.` : descriptor.description;
|
|
1302
1531
|
return {
|
|
1303
1532
|
kind: input.kind,
|
|
1304
1533
|
title,
|