pmx-canvas 0.4.8 → 0.5.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/.github/extensions/pmx-canvas/extension.mjs +154 -5
- package/.github/extensions/pmx-canvas/steering-delivery.mjs +79 -0
- package/CHANGELOG.md +156 -0
- package/Readme.md +72 -23
- package/dist/canvas/global.css +3071 -796
- package/dist/canvas/index.js +122 -104
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/agent.d.ts +1 -0
- package/dist/types/cli/commands/pump.d.ts +9 -0
- package/dist/types/client/App.d.ts +1 -1
- package/dist/types/client/canvas/AgentPresenceLayer.d.ts +11 -0
- package/dist/types/client/canvas/BarHint.d.ts +22 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +5 -0
- package/dist/types/client/canvas/CanvasViewport.d.ts +4 -4
- package/dist/types/client/canvas/CommandBar.d.ts +10 -0
- package/dist/types/client/canvas/ConnectionBanner.d.ts +13 -0
- package/dist/types/client/canvas/ContextMenu.d.ts +6 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +2 -1
- package/dist/types/client/canvas/EmptyState.d.ts +11 -0
- package/dist/types/client/canvas/ExternalWriters.d.ts +5 -0
- package/dist/types/client/canvas/HumanPresenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/Minimap.d.ts +12 -1
- package/dist/types/client/canvas/ScopeFenceLayer.d.ts +8 -0
- package/dist/types/client/canvas/SelectionBar.d.ts +6 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +1 -0
- package/dist/types/client/canvas/SessionReceipt.d.ts +17 -0
- package/dist/types/client/canvas/TextPrompt.d.ts +25 -0
- package/dist/types/client/canvas/ToolRail.d.ts +21 -0
- package/dist/types/client/canvas/TopBar.d.ts +7 -0
- package/dist/types/client/canvas/canvas-area.d.ts +39 -0
- package/dist/types/client/canvas/create-in-view.d.ts +18 -0
- package/dist/types/client/canvas/import-files.d.ts +8 -0
- package/dist/types/client/canvas/kind-colors.d.ts +3 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +14 -0
- package/dist/types/client/canvas/use-now.d.ts +6 -0
- package/dist/types/client/icons.d.ts +11 -4
- package/dist/types/client/nodes/ExtAppFrame.d.ts +2 -0
- package/dist/types/client/nodes/GroupNode.d.ts +6 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +2 -0
- package/dist/types/client/nodes/inline-editor-commands.d.ts +3 -4
- package/dist/types/client/state/canvas-store.d.ts +49 -5
- package/dist/types/client/state/human-store.d.ts +26 -0
- package/dist/types/client/state/iframe-mode.d.ts +1 -0
- package/dist/types/client/state/intent-bridge.d.ts +37 -5
- package/dist/types/client/state/node-factory.d.ts +0 -1
- package/dist/types/client/state/presence-store.d.ts +78 -0
- package/dist/types/client/state/session-store.d.ts +176 -0
- package/dist/types/client/types.d.ts +2 -1
- package/dist/types/client/utils/platform.d.ts +2 -0
- package/dist/types/server/agent-presence.d.ts +192 -0
- package/dist/types/server/ax-gate-ttl.d.ts +6 -0
- package/dist/types/server/ax-interaction.d.ts +4 -10
- package/dist/types/server/ax-state-manager.d.ts +21 -2
- package/dist/types/server/ax-state.d.ts +49 -3
- package/dist/types/server/ax-wait.d.ts +13 -0
- package/dist/types/server/canvas-db.d.ts +9 -1
- package/dist/types/server/canvas-operations.d.ts +11 -0
- package/dist/types/server/canvas-state.d.ts +27 -13
- package/dist/types/server/canvas-validation.d.ts +0 -14
- package/dist/types/server/html-surface.d.ts +1 -1
- package/dist/types/server/human-presence.d.ts +40 -0
- package/dist/types/server/index.d.ts +29 -3
- package/dist/types/server/intent-registry.d.ts +7 -0
- package/dist/types/server/mutation-history.d.ts +15 -2
- package/dist/types/server/operations/invoker.d.ts +13 -2
- package/dist/types/server/operations/ops/ax-presence.d.ts +2 -0
- package/dist/types/server/operations/ops/human-presence.d.ts +3 -0
- package/dist/types/server/operations/registry.d.ts +11 -0
- package/dist/types/server/operations/types.d.ts +1 -1
- package/dist/types/server/scope-fence.d.ts +62 -0
- package/dist/types/server/server.d.ts +0 -12
- package/dist/types/shared/agent-presence.d.ts +130 -0
- package/dist/types/shared/approval-gates.d.ts +19 -0
- package/dist/types/shared/auto-arrange.d.ts +0 -1
- package/dist/types/shared/ax-kinds.d.ts +11 -0
- package/dist/types/shared/human-presence.d.ts +31 -0
- package/dist/types/shared/iframe-probe.d.ts +8 -0
- package/dist/types/shared/scope-fence.d.ts +21 -0
- package/docs/RELEASE.md +20 -0
- package/docs/ax-host-adapter-contract.md +30 -14
- package/docs/cli.md +37 -1
- package/docs/environment.md +3 -0
- package/docs/http-api.md +222 -5
- package/docs/mcp.md +6 -5
- package/docs/node-types.md +3 -1
- package/docs/plans/plan-009-tech-debt-backlog.md +13 -0
- package/docs/screenshot.png +0 -0
- package/docs/screenshots/multi-agent-live.png +0 -0
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +1 -1
- package/package.json +4 -4
- package/skills/pmx-canvas/SKILL.md +112 -17
- package/skills/pmx-canvas/references/codex-app-adapter.md +28 -0
- package/skills/pmx-canvas/references/full-reference.md +55 -16
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +41 -24
- package/skills/pmx-canvas-orchestration/SKILL.md +56 -2
- package/skills/pmx-canvas-testing/SKILL.md +17 -0
- package/src/cli/agent.ts +1 -0
- package/src/cli/commands/copilot.ts +15 -8
- package/src/cli/commands/nodes.ts +1 -19
- package/src/cli/commands/pump.ts +263 -0
- package/src/cli/index.ts +2 -5
- package/src/cli/shared.ts +1 -1
- package/src/client/App.tsx +209 -585
- package/src/client/canvas/AgentPresenceLayer.tsx +74 -0
- package/src/client/canvas/AttentionHistory.tsx +0 -10
- package/src/client/canvas/BarHint.tsx +49 -0
- package/src/client/canvas/CanvasNode.tsx +488 -136
- package/src/client/canvas/CanvasViewport.tsx +190 -94
- package/src/client/canvas/CommandBar.tsx +140 -0
- package/src/client/canvas/CommandPalette.tsx +133 -58
- package/src/client/canvas/ConnectionBanner.tsx +65 -0
- package/src/client/canvas/ContextMenu.tsx +88 -27
- package/src/client/canvas/ContextPinBar.tsx +2 -2
- package/src/client/canvas/EdgeLayer.tsx +121 -38
- package/src/client/canvas/EmptyState.tsx +93 -0
- package/src/client/canvas/ExpandedNodeOverlay.tsx +64 -118
- package/src/client/canvas/ExternalWriters.tsx +308 -0
- package/src/client/canvas/FocusFieldLayer.tsx +12 -1
- package/src/client/canvas/HumanPresenceLayer.tsx +45 -0
- package/src/client/canvas/IntentLayer.tsx +1 -1
- package/src/client/canvas/Minimap.tsx +117 -190
- package/src/client/canvas/ScopeFenceLayer.tsx +38 -0
- package/src/client/canvas/SelectionBar.tsx +175 -23
- package/src/client/canvas/SessionPanel.tsx +481 -0
- package/src/client/canvas/SessionReceipt.tsx +141 -0
- package/src/client/canvas/ShortcutOverlay.tsx +21 -6
- package/src/client/canvas/SnapshotPanel.tsx +166 -80
- package/src/client/canvas/TextPrompt.tsx +128 -0
- package/src/client/canvas/ToolRail.tsx +447 -0
- package/src/client/canvas/TopBar.tsx +308 -0
- package/src/client/canvas/auto-fit.ts +1 -5
- package/src/client/canvas/canvas-area.ts +74 -0
- package/src/client/canvas/create-in-view.ts +48 -0
- package/src/client/canvas/import-files.ts +64 -0
- package/src/client/canvas/kind-colors.ts +22 -0
- package/src/client/canvas/snap-guides.ts +1 -1
- package/src/client/canvas/use-focus-trap.ts +54 -0
- package/src/client/canvas/use-now.ts +17 -0
- package/src/client/canvas/use-pan-zoom.ts +13 -2
- package/src/client/icons.tsx +57 -18
- package/src/client/nodes/ExtAppFrame.tsx +86 -20
- package/src/client/nodes/GroupNode.tsx +9 -28
- package/src/client/nodes/InlineFormatBar.tsx +4 -3
- package/src/client/nodes/InlineMarkdownEditor.tsx +27 -0
- package/src/client/nodes/MarkdownNode.tsx +59 -24
- package/src/client/nodes/MdFormatBar.tsx +2 -4
- package/src/client/nodes/inline-editor-commands.ts +17 -6
- package/src/client/nodes/use-surface-frame.ts +3 -1
- package/src/client/state/attention-bridge.ts +3 -0
- package/src/client/state/canvas-store.ts +285 -90
- package/src/client/state/human-store.ts +175 -0
- package/src/client/state/iframe-mode.ts +33 -9
- package/src/client/state/intent-bridge.ts +75 -13
- package/src/client/state/node-factory.ts +2 -8
- package/src/client/state/presence-store.ts +168 -0
- package/src/client/state/session-store.ts +419 -0
- package/src/client/state/sse-bridge.ts +93 -30
- package/src/client/theme/global.css +3071 -796
- package/src/client/types.ts +3 -1
- package/src/client/utils/platform.ts +6 -1
- package/src/mcp/canvas-access.ts +2 -2
- package/src/server/agent-presence.ts +755 -0
- package/src/server/ax-context.ts +45 -0
- package/src/server/ax-gate-ttl.ts +61 -0
- package/src/server/ax-interaction.ts +29 -1
- package/src/server/ax-state-manager.ts +78 -6
- package/src/server/ax-state.ts +82 -15
- package/src/server/ax-wait.ts +33 -0
- package/src/server/canvas-db.ts +103 -33
- package/src/server/canvas-operations.ts +58 -14
- package/src/server/canvas-state.ts +131 -58
- package/src/server/canvas-validation.ts +3 -53
- package/src/server/demo-state.json +2 -70
- package/src/server/html-surface.ts +11 -2
- package/src/server/human-presence.ts +160 -0
- package/src/server/index.ts +111 -6
- package/src/server/intent-registry.ts +13 -0
- package/src/server/mutation-history.ts +47 -2
- package/src/server/operations/composites.ts +6 -3
- package/src/server/operations/http.ts +10 -1
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/invoker.ts +33 -6
- package/src/server/operations/ops/ax-presence.ts +62 -0
- package/src/server/operations/ops/ax-read.ts +8 -0
- package/src/server/operations/ops/ax-state.ts +106 -1
- package/src/server/operations/ops/ax-timeline.ts +54 -5
- package/src/server/operations/ops/ax-work.ts +46 -0
- package/src/server/operations/ops/batch.ts +3 -1
- package/src/server/operations/ops/edges.ts +88 -2
- package/src/server/operations/ops/groups.ts +4 -3
- package/src/server/operations/ops/human-presence.ts +35 -0
- package/src/server/operations/ops/json-render.ts +11 -1
- package/src/server/operations/ops/nodes.ts +77 -16
- package/src/server/operations/ops/query.ts +1 -0
- package/src/server/operations/ops/snapshots.ts +7 -4
- package/src/server/operations/registry.ts +196 -1
- package/src/server/operations/types.ts +1 -1
- package/src/server/scope-fence.ts +207 -0
- package/src/server/server.ts +177 -79
- package/src/server/trace-manager.ts +0 -2
- package/src/server/web-artifacts.ts +0 -1
- package/src/shared/agent-presence.ts +162 -0
- package/src/shared/approval-gates.ts +35 -0
- package/src/shared/auto-arrange.ts +121 -13
- package/src/shared/ax-kinds.ts +23 -0
- package/src/shared/human-presence.ts +30 -0
- package/src/shared/iframe-probe.ts +8 -0
- package/src/shared/scope-fence.ts +31 -0
- package/dist/types/client/canvas/DockedNode.d.ts +0 -4
- package/src/client/canvas/DockedNode.tsx +0 -165
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"zIndex": 1,
|
|
20
20
|
"collapsed": false,
|
|
21
21
|
"pinned": false,
|
|
22
|
-
"dockPosition": null,
|
|
23
22
|
"data": {
|
|
24
23
|
"title": "① Start here",
|
|
25
24
|
"content": "What the agent is doing, what context it holds, what it decided, and the tool calls behind it.",
|
|
@@ -47,7 +46,6 @@
|
|
|
47
46
|
"zIndex": 1,
|
|
48
47
|
"collapsed": false,
|
|
49
48
|
"pinned": true,
|
|
50
|
-
"dockPosition": null,
|
|
51
49
|
"data": {
|
|
52
50
|
"title": "PMX Canvas — guided tour",
|
|
53
51
|
"content": "# PMX Canvas\n\nAn infinite spatial canvas that agents drive over **MCP**, **HTTP** or the **Bun SDK** — and that humans steer by pinning nodes.\n\nThis board is a live catalogue. Bands ① to ④ and ⑧ run down the left, ⑤ to ⑦ down the right.\n\n| Band | Shows |\n| --- | --- |\n| ① Start here | status · context · ledger · trace |\n| ② Files & review | file · CSV table · diff · image · webpage |\n| ③ Diagrams & surfaces | mermaid · raw HTML · MCP app |\n| ④ Charts | all 12 graph types |\n| ⑤ Structured UI | json-render specs |\n| ⑥ Grouping | one group frame |\n| ⑦ HTML primitives | all 21 generated surfaces |\n| ⑧ Agent at work | a live AX flow, mid-task |\n\n> Pin a node in the browser and the agent reads it back from `canvas://pinned-context`. That is the whole collaboration loop.",
|
|
@@ -75,7 +73,6 @@
|
|
|
75
73
|
"zIndex": 1,
|
|
76
74
|
"collapsed": false,
|
|
77
75
|
"pinned": false,
|
|
78
|
-
"dockPosition": null,
|
|
79
76
|
"data": {
|
|
80
77
|
"phase": "tooling",
|
|
81
78
|
"detail": "Rebuilding the release checklist",
|
|
@@ -111,7 +108,6 @@
|
|
|
111
108
|
"zIndex": 1,
|
|
112
109
|
"collapsed": false,
|
|
113
110
|
"pinned": false,
|
|
114
|
-
"dockPosition": null,
|
|
115
111
|
"data": {
|
|
116
112
|
"toolName": "canvas_query · search",
|
|
117
113
|
"category": "mcp",
|
|
@@ -143,7 +139,6 @@
|
|
|
143
139
|
"zIndex": 1,
|
|
144
140
|
"collapsed": false,
|
|
145
141
|
"pinned": false,
|
|
146
|
-
"dockPosition": null,
|
|
147
142
|
"data": {
|
|
148
143
|
"toolName": "edit_file · CHANGELOG.md",
|
|
149
144
|
"category": "file",
|
|
@@ -175,7 +170,6 @@
|
|
|
175
170
|
"zIndex": 1,
|
|
176
171
|
"collapsed": false,
|
|
177
172
|
"pinned": true,
|
|
178
|
-
"dockPosition": null,
|
|
179
173
|
"data": {
|
|
180
174
|
"currentTokens": 48200,
|
|
181
175
|
"tokenLimit": 200000,
|
|
@@ -236,7 +230,6 @@
|
|
|
236
230
|
"zIndex": 1,
|
|
237
231
|
"collapsed": false,
|
|
238
232
|
"pinned": false,
|
|
239
|
-
"dockPosition": null,
|
|
240
233
|
"data": {
|
|
241
234
|
"owner": "release-auditor",
|
|
242
235
|
"cycle": "0.4.7",
|
|
@@ -268,7 +261,6 @@
|
|
|
268
261
|
"zIndex": 1,
|
|
269
262
|
"collapsed": false,
|
|
270
263
|
"pinned": false,
|
|
271
|
-
"dockPosition": null,
|
|
272
264
|
"data": {
|
|
273
265
|
"title": "② Files & review",
|
|
274
266
|
"content": "Inline file text, delimited data as a table, unified diffs, images, and cached webpage snapshots.",
|
|
@@ -296,7 +288,6 @@
|
|
|
296
288
|
"zIndex": 1,
|
|
297
289
|
"collapsed": false,
|
|
298
290
|
"pinned": false,
|
|
299
|
-
"dockPosition": null,
|
|
300
291
|
"data": {
|
|
301
292
|
"title": "node-sizes.ts",
|
|
302
293
|
"content": "import { canvasState } from './canvas-state.js';\nimport { nodeMinSize } from '../shared/node-sizes.js';\n\n/** Clamp an agent-supplied frame up to the per-type readability floor. */\nexport function clampNodeFrame(type: string, width: number, height: number) {\n const min = nodeMinSize(type);\n if (!min) return { width, height };\n return {\n width: Math.max(width, min.width),\n height: Math.max(height, min.height),\n };\n}\n\nexport function nodeCountByType(): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const node of canvasState.getLayout().nodes) {\n counts[node.type] = (counts[node.type] ?? 0) + 1;\n }\n return counts;\n}",
|
|
@@ -326,7 +317,6 @@
|
|
|
326
317
|
"zIndex": 1,
|
|
327
318
|
"collapsed": false,
|
|
328
319
|
"pinned": true,
|
|
329
|
-
"dockPosition": null,
|
|
330
320
|
"data": {
|
|
331
321
|
"title": "Readability floor — 2 files",
|
|
332
322
|
"content": "diff --git a/src/shared/node-sizes.ts b/src/shared/node-sizes.ts\n--- a/src/shared/node-sizes.ts\n+++ b/src/shared/node-sizes.ts\n@@ -20,6 +20,8 @@ export const NODE_MIN_SIZES: Record<string, { width: number; height: number }> =\n markdown: { width: 360, height: 180 },\n context: { width: 360, height: 180 },\n file: { width: 360, height: 200 },\n+ diff: { width: 420, height: 240 },\n+ mermaid: { width: 360, height: 240 },\n status: { width: 280, height: 120 },\n\ndiff --git a/src/client/canvas/auto-fit.ts b/src/client/canvas/auto-fit.ts\n--- a/src/client/canvas/auto-fit.ts\n+++ b/src/client/canvas/auto-fit.ts\n@@ -63,7 +63,8 @@ export function computeAutoFitHeight(node: CanvasNodeState, contentHeight: numbe\n const fitted = Math.min(contentHeight + AUTO_FIT_TITLEBAR_HEIGHT, AUTO_FIT_MAX_HEIGHT);\n- return fitted;\n+ const min = nodeMinSize(node.type);\n+ return min ? Math.max(fitted, min.height) : fitted;\n }",
|
|
@@ -354,7 +344,6 @@
|
|
|
354
344
|
"zIndex": 1,
|
|
355
345
|
"collapsed": false,
|
|
356
346
|
"pinned": false,
|
|
357
|
-
"dockPosition": null,
|
|
358
347
|
"data": {
|
|
359
348
|
"title": "weekly-throughput.csv",
|
|
360
349
|
"content": "week,merged_prs,review_hours,p95_ci_minutes,flaky_reruns\n2026-W23,41,18.5,11.2,6\n2026-W24,38,21.0,12.8,9\n2026-W25,52,16.4,10.1,4\n2026-W26,47,15.2,9.6,3\n2026-W27,55,14.8,9.1,2\n2026-W28,61,13.9,8.4,2",
|
|
@@ -404,7 +393,6 @@
|
|
|
404
393
|
"zIndex": 1,
|
|
405
394
|
"collapsed": false,
|
|
406
395
|
"pinned": false,
|
|
407
|
-
"dockPosition": null,
|
|
408
396
|
"data": {
|
|
409
397
|
"caption": "Inline SVG data URI — no asset files, no network.",
|
|
410
398
|
"src": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%20480%20300'%20width%3D'480'%20height%3D'300'%3E%3Crect%20width%3D'480'%20height%3D'300'%20rx%3D'16'%20fill%3D'%230f1420'%2F%3E%3Ctext%20x%3D'24'%20y%3D'40'%20fill%3D'%23e6ecff'%20font-family%3D'ui-sans-serif%2Csystem-ui'%20font-size%3D'17'%20font-weight%3D'600'%3ECanvas%20data%20flow%3C%2Ftext%3E%3Crect%20x%3D'24'%20y%3D'70'%20width%3D'128'%20height%3D'62'%20rx%3D'10'%20fill%3D'%2316203a'%20stroke%3D'%234a9eff'%2F%3E%3Ctext%20x%3D'88'%20y%3D'106'%20fill%3D'%239ec5ff'%20font-family%3D'ui-sans-serif%2Csystem-ui'%20font-size%3D'13'%20text-anchor%3D'middle'%3EAgent%20(MCP)%3C%2Ftext%3E%3Crect%20x%3D'176'%20y%3D'70'%20width%3D'128'%20height%3D'62'%20rx%3D'10'%20fill%3D'%2316203a'%20stroke%3D'%237c5cff'%2F%3E%3Ctext%20x%3D'240'%20y%3D'100'%20fill%3D'%23c3b4ff'%20font-family%3D'ui-sans-serif%2Csystem-ui'%20font-size%3D'13'%20text-anchor%3D'middle'%3ECanvas%20state%3C%2Ftext%3E%3Ctext%20x%3D'240'%20y%3D'118'%20fill%3D'%237b86a8'%20font-family%3D'ui-sans-serif%2Csystem-ui'%20font-size%3D'11'%20text-anchor%3D'middle'%3Esingle%20source%20of%20truth%3C%2Ftext%3E%3Crect%20x%3D'328'%20y%3D'70'%20width%3D'128'%20height%3D'62'%20rx%3D'10'%20fill%3D'%2316203a'%20stroke%3D'%2322c55e'%2F%3E%3Ctext%20x%3D'392'%20y%3D'106'%20fill%3D'%238ee5ad'%20font-family%3D'ui-sans-serif%2Csystem-ui'%20font-size%3D'13'%20text-anchor%3D'middle'%3EBrowser%3C%2Ftext%3E%3Cpath%20d%3D'M152%20101%20L172%20101'%20stroke%3D'%234a9eff'%20stroke-width%3D'2'%2F%3E%3Cpath%20d%3D'M304%20101%20L324%20101'%20stroke%3D'%2322c55e'%20stroke-width%3D'2'%2F%3E%3Cpath%20d%3D'M392%20140%20L392%20196%20L88%20196%20L88%20140'%20fill%3D'none'%20stroke%3D'%23eab308'%20stroke-width%3D'2'%20stroke-dasharray%3D'5%205'%2F%3E%3Ctext%20x%3D'240'%20y%3D'188'%20fill%3D'%23eab308'%20font-family%3D'ui-sans-serif%2Csystem-ui'%20font-size%3D'12'%20text-anchor%3D'middle'%3Ehuman%20pins%20nodes%20-%26gt%3B%20canvas%3A%2F%2Fpinned-context%3C%2Ftext%3E%3Crect%20x%3D'24'%20y%3D'222'%20width%3D'432'%20height%3D'52'%20rx%3D'10'%20fill%3D'%23111827'%20stroke%3D'%23243049'%2F%3E%3Ctext%20x%3D'240'%20y%3D'253'%20fill%3D'%237b86a8'%20font-family%3D'ui-monospace%2Cmonospace'%20font-size%3D'12'%20text-anchor%3D'middle'%3Estate%20survives%20refresh%20%C2%B7%20SQLite%20at%20.pmx-canvas%2Fcanvas.db%3C%2Ftext%3E%3C%2Fsvg%3E",
|
|
@@ -433,7 +421,6 @@
|
|
|
433
421
|
"zIndex": 1,
|
|
434
422
|
"collapsed": false,
|
|
435
423
|
"pinned": false,
|
|
436
|
-
"dockPosition": null,
|
|
437
424
|
"data": {
|
|
438
425
|
"url": "https://modelcontextprotocol.io/docs/concepts/resources",
|
|
439
426
|
"title": "Model Context Protocol — Resources",
|
|
@@ -496,7 +483,6 @@
|
|
|
496
483
|
"zIndex": 1,
|
|
497
484
|
"collapsed": false,
|
|
498
485
|
"pinned": false,
|
|
499
|
-
"dockPosition": null,
|
|
500
486
|
"data": {
|
|
501
487
|
"title": "③ Diagrams & surfaces",
|
|
502
488
|
"content": "Mermaid renders from plain text; HTML and MCP App nodes are sandboxed iframes the agent authors.",
|
|
@@ -524,7 +510,6 @@
|
|
|
524
510
|
"zIndex": 1,
|
|
525
511
|
"collapsed": false,
|
|
526
512
|
"pinned": false,
|
|
527
|
-
"dockPosition": null,
|
|
528
513
|
"data": {
|
|
529
514
|
"title": "Flowchart — request path",
|
|
530
515
|
"content": "flowchart LR\n A[Agent] -->|MCP tool call| B(Canvas server)\n B --> C{{SSE stream}}\n C --> D[Browser canvas]\n D -->|human pins a node| B\n B --> E[(canvas.db)]",
|
|
@@ -552,7 +537,6 @@
|
|
|
552
537
|
"zIndex": 1,
|
|
553
538
|
"collapsed": false,
|
|
554
539
|
"pinned": false,
|
|
555
|
-
"dockPosition": null,
|
|
556
540
|
"data": {
|
|
557
541
|
"title": "Sequence — the pin loop",
|
|
558
542
|
"content": "sequenceDiagram\n participant H as Human\n participant C as Canvas\n participant A as Agent\n H->>C: pin 3 nodes\n C-->>A: resources/updated\n A->>C: read canvas://pinned-context\n A->>C: canvas_node add (summary)\n C-->>H: SSE layout update",
|
|
@@ -580,7 +564,6 @@
|
|
|
580
564
|
"zIndex": 1,
|
|
581
565
|
"collapsed": false,
|
|
582
566
|
"pinned": false,
|
|
583
|
-
"dockPosition": null,
|
|
584
567
|
"data": {
|
|
585
568
|
"title": "State — approval gate",
|
|
586
569
|
"content": "stateDiagram-v2\n [*] --> pending\n pending --> approved: human approves\n pending --> rejected: human rejects\n pending --> expired: timeout\n approved --> [*]\n rejected --> [*]\n expired --> [*]",
|
|
@@ -608,7 +591,6 @@
|
|
|
608
591
|
"zIndex": 1,
|
|
609
592
|
"collapsed": false,
|
|
610
593
|
"pinned": false,
|
|
611
|
-
"dockPosition": null,
|
|
612
594
|
"data": {
|
|
613
595
|
"html": "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><style>\n :root { color-scheme: dark; }\n body { margin:0; font:14px/1.5 ui-sans-serif,system-ui,-apple-system,sans-serif; background:transparent; color:#e6ecff; padding:18px; }\n h1 { margin:0 0 4px; font-size:16px; letter-spacing:-0.01em; }\n p.sub { margin:0 0 16px; font-size:12px; color:#8994b3; }\n ul { list-style:none; margin:0; padding:0; display:grid; gap:8px; }\n li { display:flex; align-items:center; gap:10px; padding:9px 12px; border:1px solid #243049; border-radius:9px; background:#131a2b; }\n .dot { width:8px; height:8px; border-radius:99px; flex:0 0 auto; }\n .ok .dot { background:#22c55e; } .warn .dot { background:#eab308; } .run .dot { background:#4a9eff; }\n .name { flex:1; } .val { font:12px ui-monospace,monospace; color:#8994b3; }\n</style></head><body>\n <h1>Release gates — 0.4.7</h1>\n <p class=\"sub\">Raw HTML in a sandboxed iframe. No scripts, no network, no build step.</p>\n <ul>\n <li class=\"ok\"><span class=\"dot\"></span><span class=\"name\">typecheck</span><span class=\"val\">passing</span></li>\n <li class=\"ok\"><span class=\"dot\"></span><span class=\"name\">unit tests</span><span class=\"val\">612 passed</span></li>\n <li class=\"run\"><span class=\"dot\"></span><span class=\"name\">web-canvas e2e</span><span class=\"val\">running</span></li>\n <li class=\"warn\"><span class=\"dot\"></span><span class=\"name\">bundle size</span><span class=\"val\">+4.1% vs 0.4.6</span></li>\n <li class=\"ok\"><span class=\"dot\"></span><span class=\"name\">pack dry-run</span><span class=\"val\">clean</span></li>\n </ul>\n</body></html>",
|
|
614
596
|
"summary": "Five release gates with pass/running/warning status for 0.4.7.",
|
|
@@ -639,7 +621,6 @@
|
|
|
639
621
|
"zIndex": 1,
|
|
640
622
|
"collapsed": false,
|
|
641
623
|
"pinned": false,
|
|
642
|
-
"dockPosition": null,
|
|
643
624
|
"data": {
|
|
644
625
|
"mode": "ext-app",
|
|
645
626
|
"html": "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><style>\n :root { color-scheme: dark; }\n body { margin:0; font:14px/1.55 ui-sans-serif,system-ui,-apple-system,sans-serif; background:#0d1220; color:#e6ecff; }\n header { padding:14px 18px; border-bottom:1px solid #1e2740; display:flex; align-items:center; gap:10px; }\n .badge { font:11px ui-monospace,monospace; color:#9ec5ff; background:#16203a; border:1px solid #2b3a5e; padding:2px 7px; border-radius:6px; }\n h1 { margin:0; font-size:14px; font-weight:600; }\n main { padding:18px; display:grid; gap:14px; }\n .row { display:grid; grid-template-columns:1fr auto; align-items:center; gap:12px; padding:11px 13px; border:1px solid #243049; border-radius:10px; background:#131a2b; }\n .row b { font-weight:600; font-size:13px; } .row small { display:block; color:#8994b3; font-size:11.5px; }\n .pill { font:11px ui-monospace,monospace; padding:3px 9px; border-radius:99px; background:#16203a; color:#9ec5ff; border:1px solid #2b3a5e; }\n footer { padding:12px 18px; border-top:1px solid #1e2740; color:#6f7a99; font-size:11.5px; }\n</style></head><body>\n <header><span class=\"badge\">ext-app</span><h1>Board inspector</h1></header>\n <main>\n <div class=\"row\"><span><b>Nodes on board</b><small>across 8 spatial bands</small></span><span class=\"pill\">69</span></div>\n <div class=\"row\"><span><b>Edges</b><small>flow · depends-on · relation · references</small></span><span class=\"pill\">6</span></div>\n <div class=\"row\"><span><b>Context pins</b><small>read via canvas://pinned-context</small></span><span class=\"pill\">3</span></div>\n <div class=\"row\"><span><b>Groups</b><small>one example frame</small></span><span class=\"pill\">1</span></div>\n </main>\n <footer>An MCP App surface rendered through the app bridge from inline HTML — no live server session required.</footer>\n</body></html>",
|
|
@@ -691,7 +672,6 @@
|
|
|
691
672
|
"zIndex": 1,
|
|
692
673
|
"collapsed": false,
|
|
693
674
|
"pinned": false,
|
|
694
|
-
"dockPosition": null,
|
|
695
675
|
"data": {
|
|
696
676
|
"title": "④ Charts",
|
|
697
677
|
"content": "Every canonical graph type, then the Tufte primitives in the compact bottom row.",
|
|
@@ -719,7 +699,6 @@
|
|
|
719
699
|
"zIndex": 1,
|
|
720
700
|
"collapsed": false,
|
|
721
701
|
"pinned": false,
|
|
722
|
-
"dockPosition": null,
|
|
723
702
|
"data": {
|
|
724
703
|
"title": "line — p95 latency",
|
|
725
704
|
"spec": {
|
|
@@ -839,7 +818,6 @@
|
|
|
839
818
|
"zIndex": 1,
|
|
840
819
|
"collapsed": false,
|
|
841
820
|
"pinned": false,
|
|
842
|
-
"dockPosition": null,
|
|
843
821
|
"data": {
|
|
844
822
|
"title": "bar — merged PRs per week",
|
|
845
823
|
"spec": {
|
|
@@ -963,7 +941,6 @@
|
|
|
963
941
|
"zIndex": 1,
|
|
964
942
|
"collapsed": false,
|
|
965
943
|
"pinned": false,
|
|
966
|
-
"dockPosition": null,
|
|
967
944
|
"data": {
|
|
968
945
|
"title": "area — canvas nodes created",
|
|
969
946
|
"spec": {
|
|
@@ -1075,7 +1052,6 @@
|
|
|
1075
1052
|
"zIndex": 1,
|
|
1076
1053
|
"collapsed": false,
|
|
1077
1054
|
"pinned": false,
|
|
1078
|
-
"dockPosition": null,
|
|
1079
1055
|
"data": {
|
|
1080
1056
|
"title": "pie — node types on real boards",
|
|
1081
1057
|
"spec": {
|
|
@@ -1181,7 +1157,6 @@
|
|
|
1181
1157
|
"zIndex": 1,
|
|
1182
1158
|
"collapsed": false,
|
|
1183
1159
|
"pinned": false,
|
|
1184
|
-
"dockPosition": null,
|
|
1185
1160
|
"data": {
|
|
1186
1161
|
"title": "stacked-bar — CI minutes by stage",
|
|
1187
1162
|
"spec": {
|
|
@@ -1310,7 +1285,6 @@
|
|
|
1310
1285
|
"zIndex": 1,
|
|
1311
1286
|
"collapsed": false,
|
|
1312
1287
|
"pinned": false,
|
|
1313
|
-
"dockPosition": null,
|
|
1314
1288
|
"data": {
|
|
1315
1289
|
"title": "composed — throughput vs review load",
|
|
1316
1290
|
"spec": {
|
|
@@ -1425,7 +1399,6 @@
|
|
|
1425
1399
|
"zIndex": 1,
|
|
1426
1400
|
"collapsed": false,
|
|
1427
1401
|
"pinned": false,
|
|
1428
|
-
"dockPosition": null,
|
|
1429
1402
|
"data": {
|
|
1430
1403
|
"title": "scatter — PR size vs review time",
|
|
1431
1404
|
"spec": {
|
|
@@ -1559,7 +1532,6 @@
|
|
|
1559
1532
|
"zIndex": 1,
|
|
1560
1533
|
"collapsed": false,
|
|
1561
1534
|
"pinned": false,
|
|
1562
|
-
"dockPosition": null,
|
|
1563
1535
|
"data": {
|
|
1564
1536
|
"title": "radar — surface coverage",
|
|
1565
1537
|
"spec": {
|
|
@@ -1688,7 +1660,6 @@
|
|
|
1688
1660
|
"zIndex": 1,
|
|
1689
1661
|
"collapsed": false,
|
|
1690
1662
|
"pinned": false,
|
|
1691
|
-
"dockPosition": null,
|
|
1692
1663
|
"data": {
|
|
1693
1664
|
"title": "sparkline — build time",
|
|
1694
1665
|
"spec": {
|
|
@@ -1830,7 +1801,6 @@
|
|
|
1830
1801
|
"zIndex": 1,
|
|
1831
1802
|
"collapsed": false,
|
|
1832
1803
|
"pinned": false,
|
|
1833
|
-
"dockPosition": null,
|
|
1834
1804
|
"data": {
|
|
1835
1805
|
"title": "dot-plot — test time by suite",
|
|
1836
1806
|
"spec": {
|
|
@@ -1936,7 +1906,6 @@
|
|
|
1936
1906
|
"zIndex": 1,
|
|
1937
1907
|
"collapsed": false,
|
|
1938
1908
|
"pinned": false,
|
|
1939
|
-
"dockPosition": null,
|
|
1940
1909
|
"data": {
|
|
1941
1910
|
"title": "bullet — release KPIs vs target",
|
|
1942
1911
|
"spec": {
|
|
@@ -2072,7 +2041,6 @@
|
|
|
2072
2041
|
"zIndex": 1,
|
|
2073
2042
|
"collapsed": false,
|
|
2074
2043
|
"pinned": false,
|
|
2075
|
-
"dockPosition": null,
|
|
2076
2044
|
"data": {
|
|
2077
2045
|
"title": "slopegraph — coverage 0.4.6 → 0.4.7",
|
|
2078
2046
|
"spec": {
|
|
@@ -2192,7 +2160,6 @@
|
|
|
2192
2160
|
"zIndex": 1,
|
|
2193
2161
|
"collapsed": false,
|
|
2194
2162
|
"pinned": false,
|
|
2195
|
-
"dockPosition": null,
|
|
2196
2163
|
"data": {
|
|
2197
2164
|
"title": "⑤ Structured UI",
|
|
2198
2165
|
"content": "json-render nodes: an agent posts a validated component spec and the canvas renders real UI.",
|
|
@@ -2220,7 +2187,6 @@
|
|
|
2220
2187
|
"zIndex": 1,
|
|
2221
2188
|
"collapsed": false,
|
|
2222
2189
|
"pinned": false,
|
|
2223
|
-
"dockPosition": null,
|
|
2224
2190
|
"data": {
|
|
2225
2191
|
"title": "Release dashboard",
|
|
2226
2192
|
"spec": {
|
|
@@ -2420,7 +2386,6 @@
|
|
|
2420
2386
|
"zIndex": 1,
|
|
2421
2387
|
"collapsed": false,
|
|
2422
2388
|
"pinned": false,
|
|
2423
|
-
"dockPosition": null,
|
|
2424
2389
|
"data": {
|
|
2425
2390
|
"title": "Blocker intake form",
|
|
2426
2391
|
"spec": {
|
|
@@ -2597,7 +2562,6 @@
|
|
|
2597
2562
|
"zIndex": 1,
|
|
2598
2563
|
"collapsed": false,
|
|
2599
2564
|
"pinned": false,
|
|
2600
|
-
"dockPosition": null,
|
|
2601
2565
|
"data": {
|
|
2602
2566
|
"title": "Usage panel",
|
|
2603
2567
|
"spec": {
|
|
@@ -2757,7 +2721,6 @@
|
|
|
2757
2721
|
"zIndex": 1,
|
|
2758
2722
|
"collapsed": false,
|
|
2759
2723
|
"pinned": false,
|
|
2760
|
-
"dockPosition": null,
|
|
2761
2724
|
"data": {
|
|
2762
2725
|
"title": "⑥ Grouping",
|
|
2763
2726
|
"content": "A group says \"these belong together\". One frame is enough — bands carry a board’s structure.",
|
|
@@ -2785,7 +2748,6 @@
|
|
|
2785
2748
|
"zIndex": 1,
|
|
2786
2749
|
"collapsed": false,
|
|
2787
2750
|
"pinned": false,
|
|
2788
|
-
"dockPosition": null,
|
|
2789
2751
|
"data": {
|
|
2790
2752
|
"title": "Sprint 41 — canvas polish",
|
|
2791
2753
|
"content": "Three nodes, one frame. Moving the frame moves the children; ungrouping leaves them exactly where they are.\n\n- Group children keep their own positions by default\n- `childLayout` opts into auto-packing\n- The frame is stored as `data.children` on the group",
|
|
@@ -2814,7 +2776,6 @@
|
|
|
2814
2776
|
"zIndex": 1,
|
|
2815
2777
|
"collapsed": false,
|
|
2816
2778
|
"pinned": false,
|
|
2817
|
-
"dockPosition": null,
|
|
2818
2779
|
"data": {
|
|
2819
2780
|
"phase": "review",
|
|
2820
2781
|
"detail": "6 of 7 items merged",
|
|
@@ -2846,7 +2807,6 @@
|
|
|
2846
2807
|
"zIndex": 1,
|
|
2847
2808
|
"collapsed": false,
|
|
2848
2809
|
"pinned": false,
|
|
2849
|
-
"dockPosition": null,
|
|
2850
2810
|
"data": {
|
|
2851
2811
|
"toolName": "canvas_group · create",
|
|
2852
2812
|
"category": "mcp",
|
|
@@ -2879,7 +2839,6 @@
|
|
|
2879
2839
|
"zIndex": 0,
|
|
2880
2840
|
"collapsed": false,
|
|
2881
2841
|
"pinned": false,
|
|
2882
|
-
"dockPosition": null,
|
|
2883
2842
|
"data": {
|
|
2884
2843
|
"title": "Sprint 41",
|
|
2885
2844
|
"children": [
|
|
@@ -2912,7 +2871,6 @@
|
|
|
2912
2871
|
"zIndex": 1,
|
|
2913
2872
|
"collapsed": false,
|
|
2914
2873
|
"pinned": false,
|
|
2915
|
-
"dockPosition": null,
|
|
2916
2874
|
"data": {
|
|
2917
2875
|
"title": "⑦ HTML primitives",
|
|
2918
2876
|
"content": "All 21 generated communication surfaces — self-contained sandboxed documents, no build step.",
|
|
@@ -2940,7 +2898,6 @@
|
|
|
2940
2898
|
"zIndex": 1,
|
|
2941
2899
|
"collapsed": false,
|
|
2942
2900
|
"pinned": false,
|
|
2943
|
-
"dockPosition": null,
|
|
2944
2901
|
"data": {
|
|
2945
2902
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>How should the demo board be generated?</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / choice-grid</div><h1>How should the demo board be generated?</h1><p>Side-by-side options with tradeoffs, pros, cons, and code or evidence snippets.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"grid\">\n <article class=\"card emphasis\">\n <div class=\"small\">Choice 1</div>\n <h2>Hand-written fixture</h2>\n <p>Edit demo-state.json directly.</p>\n <p><strong>Tradeoff:</strong> Fastest to start, impossible to keep correct.</p>\n <div class=\"two\"><div><h3>Pros</h3><ul><li>No tooling</li><li>Full control of every byte</li></ul></div><div><h3>Cons</h3><ul><li>Data shapes drift from the server</li><li>Generated HTML surfaces cannot be authored by hand</li></ul></div></div>\n <pre><code>// 1100 lines of JSON nobody dares touch</code></pre>\n </article>\n <article class=\"card \">\n <div class=\"small\">Choice 2</div>\n <h2>Generate through the HTTP API</h2>\n <p>A committed script replays the real create path.</p>\n <p><strong>Tradeoff:</strong> Needs a throwaway server during generation.</p>\n <div class=\"two\"><div><h3>Pros</h3><ul><li>Shapes correct by construction</li><li>Regenerates after schema changes</li><li>Asserts its own invariants</li></ul></div><div><h3>Cons</h3><ul><li>One more script to maintain</li></ul></div></div>\n <pre><code>await api("POST", "/api/canvas/node", { type: "mermaid", ... })</code></pre>\n </article>\n <article class=\"card \">\n <div class=\"small\">Choice 3</div>\n <h2>Snapshot a live board</h2>\n <p>Curate by hand in the browser, then export.</p>\n <p><strong>Tradeoff:</strong> Pleasant to author, unreproducible.</p>\n <div class=\"two\"><div><h3>Pros</h3><ul><li>Visual authoring</li><li>No layout maths</li></ul></div><div><h3>Cons</h3><ul><li>Not reviewable in a diff</li><li>Cannot be regenerated</li></ul></div></div>\n \n </article></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"items\": [\n {\n \"title\": \"Hand-written fixture\",\n \"summary\": \"Edit demo-state.json directly.\",\n \"tradeoff\": \"Fastest to start, impossible to keep correct.\",\n \"pros\": [\n \"No tooling\",\n \"Full control of every byte\"\n ],\n \"cons\": [\n \"Data shapes drift from the server\",\n \"Generated HTML surfaces cannot be authored by hand\"\n ],\n \"code\": \"// 1100 lines of JSON nobody dares touch\"\n },\n {\n \"title\": \"Generate through the HTTP API\",\n \"summary\": \"A committed script replays the real create path.\",\n \"tradeoff\": \"Needs a throwaway server during generation.\",\n \"pros\": [\n \"Shapes correct by construction\",\n \"Regenerates after schema changes\",\n \"Asserts its own invariants\"\n ],\n \"cons\": [\n \"One more script to maintain\"\n ],\n \"code\": \"await api(\\\"POST\\\", \\\"/api/canvas/node\\\", { type: \\\"mermaid\\\", ... })\"\n },\n {\n \"title\": \"Snapshot a live board\",\n \"summary\": \"Curate by hand in the browser, then export.\",\n \"tradeoff\": \"Pleasant to author, unreproducible.\",\n \"pros\": [\n \"Visual authoring\",\n \"No layout maths\"\n ],\n \"cons\": [\n \"Not reviewable in a diff\",\n \"Cannot be regenerated\"\n ]\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\n</script>\n</body>\n</html>",
|
|
2946
2903
|
"htmlPrimitive": "choice-grid",
|
|
@@ -3017,7 +2974,6 @@
|
|
|
3017
2974
|
"zIndex": 1,
|
|
3018
2975
|
"collapsed": false,
|
|
3019
2976
|
"pinned": false,
|
|
3020
|
-
"dockPosition": null,
|
|
3021
2977
|
"data": {
|
|
3022
2978
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Demo board rebuild</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / plan-timeline</div><h1>Demo board rebuild</h1><p>Implementation plan with milestones, data flow, code checkpoints, and risks.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"two\"><div class=\"panel\"><h2>Milestones</h2><div class=\"timeline\">\n <div class=\"step\"><div class=\"dot\">1</div><div class=\"card\"><h3>Inventory the surfaces <span class=\"badge ok\">done</span></h3><p>Every node type, graph type and primitive kind.</p></div></div>\n <div class=\"step\"><div class=\"dot\">2</div><div class=\"card\"><h3>Write the generator <span class=\"badge ok\">done</span></h3><p>Create everything through the real HTTP API.</p></div></div>\n <div class=\"step\"><div class=\"dot\">3</div><div class=\"card\"><h3>Lay out the bands <span class=\"badge info\">active</span></h3><p>Spatial bands with section labels, generous gutters.</p></div></div>\n <div class=\"step\"><div class=\"dot\">4</div><div class=\"card\"><h3>Rewrite the fixture test <span class=\"badge info\">todo</span></h3><p>Keep the structural invariants, add coverage assertions.</p></div></div>\n <div class=\"step\"><div class=\"dot\">5</div><div class=\"card\"><h3>Visual pass <span class=\"badge info\">todo</span></h3><p>Screenshot each band and fix anything blank or clipped.</p></div></div></div></div>\n <div class=\"panel\"><h2>Data Flow</h2><div class=\"flow\"><div class=\"flow-node\"><h3>generator</h3><p>HTTP create</p><strong>temp server</strong></div><div class=\"arrow\">-></div><div class=\"flow-node\"><h3>temp server</h3><p>export + stable ids</p><strong>demo-state.json</strong></div><div class=\"arrow\">-></div><div class=\"flow-node\"><h3>demo-state.json</h3><p>--demo</p><strong>seedDemoCanvas</strong></div></div><h2 style=\"margin-top:18px\">Risks</h2><div class=\"card\"><strong>Browser auto-fit reflows the board on first open</strong><p>Create every node with strictSize.</p></div><div class=\"card\"><strong>Fixture depends on files or network at render time</strong><p>Inline file text, data-URI image, cached webpage snapshot.</p></div><div class=\"card\"><strong>Server ids churn on every regeneration</strong><p>Rewrite ids to stable demo-* keys during export.</p></div></div></section>\n <section class=\"panel\" style=\"margin-top:14px\"><h2>Code Checkpoints</h2><h3>Regenerate</h3><pre><code>bun run scripts/generate-demo-board.ts</code></pre><h3>Verify</h3><pre><code>bun test tests/unit/demo.test.ts</code></pre></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"milestones\": [\n {\n \"title\": \"Inventory the surfaces\",\n \"detail\": \"Every node type, graph type and primitive kind.\",\n \"status\": \"done\"\n },\n {\n \"title\": \"Write the generator\",\n \"detail\": \"Create everything through the real HTTP API.\",\n \"status\": \"done\"\n },\n {\n \"title\": \"Lay out the bands\",\n \"detail\": \"Spatial bands with section labels, generous gutters.\",\n \"status\": \"active\"\n },\n {\n \"title\": \"Rewrite the fixture test\",\n \"detail\": \"Keep the structural invariants, add coverage assertions.\",\n \"status\": \"todo\"\n },\n {\n \"title\": \"Visual pass\",\n \"detail\": \"Screenshot each band and fix anything blank or clipped.\",\n \"status\": \"todo\"\n }\n ],\n \"flow\": [\n {\n \"from\": \"generator\",\n \"to\": \"temp server\",\n \"label\": \"HTTP create\"\n },\n {\n \"from\": \"temp server\",\n \"to\": \"demo-state.json\",\n \"label\": \"export + stable ids\"\n },\n {\n \"from\": \"demo-state.json\",\n \"to\": \"seedDemoCanvas\",\n \"label\": \"--demo\"\n }\n ],\n \"risks\": [\n {\n \"risk\": \"Browser auto-fit reflows the board on first open\",\n \"mitigation\": \"Create every node with strictSize.\"\n },\n {\n \"risk\": \"Fixture depends on files or network at render time\",\n \"mitigation\": \"Inline file text, data-URI image, cached webpage snapshot.\"\n },\n {\n \"risk\": \"Server ids churn on every regeneration\",\n \"mitigation\": \"Rewrite ids to stable demo-* keys during export.\"\n }\n ],\n \"snippets\": [\n {\n \"label\": \"Regenerate\",\n \"code\": \"bun run scripts/generate-demo-board.ts\"\n },\n {\n \"label\": \"Verify\",\n \"code\": \"bun test tests/unit/demo.test.ts\"\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\n</script>\n</body>\n</html>",
|
|
3023
2979
|
"htmlPrimitive": "plan-timeline",
|
|
@@ -3119,7 +3075,6 @@
|
|
|
3119
3075
|
"zIndex": 1,
|
|
3120
3076
|
"collapsed": false,
|
|
3121
3077
|
"pinned": false,
|
|
3122
|
-
"dockPosition": null,
|
|
3123
3078
|
"data": {
|
|
3124
3079
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Review — auto-fit floor</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / review-sheet</div><h1>Review — auto-fit floor</h1><p>Annotated PR/code review sheet with severity-colored findings and diff excerpts.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"two\"><div class=\"panel\"><h2>Findings</h2>\n <article class=\"card\"><h3><span class=\"badge warn\">warning</span> Auto-fit undid the creation clamp</h3><p class=\"small\">src/client/canvas/auto-fit.ts:63</p><p>The server clamped 200x100 up to 360x180, then the DOM measure persisted 360x132.</p></article>\n <article class=\"card\"><h3><span class=\"badge info\">info</span> Floor now lives in shared/</h3><p class=\"small\">src/shared/node-sizes.ts:20</p><p>Both sides import the same table — a floor only one side knows about is not a floor.</p></article>\n <article class=\"card\"><h3><span class=\"badge ok\">ok</span> strictSize is still the escape hatch</h3><p class=\"small\">src/shared/node-sizes.ts:48</p><p>Deliberately small fixed frames opt out of both the clamp and the auto-fit floor.</p></article></div>\n <div class=\"panel\"><h2>Review Tour</h2><article class=\"card\"><h3>src/shared/node-sizes.ts</h3><p>Owns the per-type readability floor.</p></article><article class=\"card\"><h3>src/client/canvas/auto-fit.ts</h3><p>Applies the floor to the measured height.</p></article></div></section>\n <section class=\"panel\" style=\"margin-top:14px\"><h2>Diff Excerpt</h2><pre><code>- return fitted;\n+ const min = nodeMinSize(node.type);\n+ return min ? Math.max(fitted, min.height) : fitted;</code></pre></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"findings\": [\n {\n \"severity\": \"warning\",\n \"title\": \"Auto-fit undid the creation clamp\",\n \"file\": \"src/client/canvas/auto-fit.ts\",\n \"line\": 63,\n \"detail\": \"The server clamped 200x100 up to 360x180, then the DOM measure persisted 360x132.\"\n },\n {\n \"severity\": \"info\",\n \"title\": \"Floor now lives in shared/\",\n \"file\": \"src/shared/node-sizes.ts\",\n \"line\": 20,\n \"detail\": \"Both sides import the same table — a floor only one side knows about is not a floor.\"\n },\n {\n \"severity\": \"ok\",\n \"title\": \"strictSize is still the escape hatch\",\n \"file\": \"src/shared/node-sizes.ts\",\n \"line\": 48,\n \"detail\": \"Deliberately small fixed frames opt out of both the clamp and the auto-fit floor.\"\n }\n ],\n \"files\": [\n {\n \"path\": \"src/shared/node-sizes.ts\",\n \"why\": \"Owns the per-type readability floor.\"\n },\n {\n \"path\": \"src/client/canvas/auto-fit.ts\",\n \"why\": \"Applies the floor to the measured height.\"\n }\n ],\n \"diff\": \"- return fitted;\\n+ const min = nodeMinSize(node.type);\\n+ return min ? Math.max(fitted, min.height) : fitted;\"\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\n</script>\n</body>\n</html>",
|
|
3125
3080
|
"htmlPrimitive": "review-sheet",
|
|
@@ -3190,7 +3145,6 @@
|
|
|
3190
3145
|
"zIndex": 1,
|
|
3191
3146
|
"collapsed": false,
|
|
3192
3147
|
"pinned": false,
|
|
3193
|
-
"dockPosition": null,
|
|
3194
3148
|
"data": {
|
|
3195
3149
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>PR — replace the demo board</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / pr-writeup</div><h1>PR — replace the demo board</h1><p>Reviewer-ready PR narrative with motivation, before/after, file tour, test plan, and rollout notes.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"panel\"><h2>Summary</h2><p>Replaces the OKR demo board with a generated catalogue of every public canvas surface.</p><p>The old board used three node types and was dominated by group frames, so it showed almost nothing the product does.</p></section>\n <section class=\"two\" style=\"margin-top:14px\"><div class=\"card\"><h2>Before</h2><ul><li>19 nodes</li><li>3 node types</li><li>3 large group frames</li><li>hand-edited JSON</li></ul></div><div class=\"card emphasis\"><h2>After</h2><ul><li>69 nodes across 8 labelled bands</li><li>all 15 public node types</li><li>all 12 graph types and all 21 HTML primitives</li><li>generated by a committed script</li></ul></div></section>\n <section class=\"three\" style=\"margin-top:14px\"><div class=\"panel\"><h2>File Tour</h2><details open><summary><strong>scripts/generate-demo-board.ts</strong></summary><p>The generator, and the only place the board is authored.</p><p class=\"small\">Focus: Band layout and offline-safety rules.</p></details><details open><summary><strong>src/server/demo-state.json</strong></summary><p>Generated output — never hand-edited.</p><p class=\"small\">Focus: Stable demo-* ids.</p></details><details open><summary><strong>tests/unit/demo.test.ts</strong></summary><p>Structural invariants plus coverage assertions.</p><p class=\"small\">Focus: Type/graph/primitive coverage checks.</p></details></div>\n <div class=\"panel\"><h2>Review Focus</h2><ul><li>No node overlaps</li><li>Group containment inset</li><li>Nothing that needs disk or network at render time</li></ul><h2 style=\"margin-top:18px\">Tests</h2><ul><li>bun test tests/unit/demo.test.ts</li><li>bun run typecheck</li><li>Visual pass over each band at overview zoom</li></ul></div>\n <div class=\"panel sticky\"><h2>Rollout</h2><ul><li>No migration — the fixture is replaced wholesale</li><li>--demo still refuses to clobber a non-empty canvas</li></ul><button type=\"button\" data-copy-markdown style=\"margin-top:12px\">Copy PR markdown</button></div></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"summary\": \"Replaces the OKR demo board with a generated catalogue of every public canvas surface.\",\n \"why\": \"The old board used three node types and was dominated by group frames, so it showed almost nothing the product does.\",\n \"before\": [\n \"19 nodes\",\n \"3 node types\",\n \"3 large group frames\",\n \"hand-edited JSON\"\n ],\n \"after\": [\n \"69 nodes across 8 labelled bands\",\n \"all 15 public node types\",\n \"all 12 graph types and all 21 HTML primitives\",\n \"generated by a committed script\"\n ],\n \"files\": [\n {\n \"path\": \"scripts/generate-demo-board.ts\",\n \"why\": \"The generator, and the only place the board is authored.\",\n \"focus\": \"Band layout and offline-safety rules.\"\n },\n {\n \"path\": \"src/server/demo-state.json\",\n \"why\": \"Generated output — never hand-edited.\",\n \"focus\": \"Stable demo-* ids.\"\n },\n {\n \"path\": \"tests/unit/demo.test.ts\",\n \"why\": \"Structural invariants plus coverage assertions.\",\n \"focus\": \"Type/graph/primitive coverage checks.\"\n }\n ],\n \"reviewFocus\": [\n \"No node overlaps\",\n \"Group containment inset\",\n \"Nothing that needs disk or network at render time\"\n ],\n \"tests\": [\n \"bun test tests/unit/demo.test.ts\",\n \"bun run typecheck\",\n \"Visual pass over each band at overview zoom\"\n ],\n \"rollout\": [\n \"No migration — the fixture is replaced wholesale\",\n \"--demo still refuses to clobber a non-empty canvas\"\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nfunction prMarkdown() {\n const d = PMX_DATA;\n const lines = ['## Summary', d.summary || '', '', '## Why', d.why || '', '', '## Before / After'];\n (d.before || []).forEach((item) => lines.push('- Before: ' + item));\n (d.after || []).forEach((item) => lines.push('- After: ' + item));\n lines.push('', '## Review Focus');\n (d.reviewFocus || []).forEach((item) => lines.push('- ' + item));\n lines.push('', '## Tests');\n (d.tests || []).forEach((item) => lines.push('- ' + item));\n return lines.join('\\n');\n}\ndocument.querySelector('[data-copy-markdown]')?.addEventListener('click', () => copyText(prMarkdown()));\n</script>\n</body>\n</html>",
|
|
3196
3150
|
"htmlPrimitive": "pr-writeup",
|
|
@@ -3270,7 +3224,6 @@
|
|
|
3270
3224
|
"zIndex": 1,
|
|
3271
3225
|
"collapsed": false,
|
|
3272
3226
|
"pinned": false,
|
|
3273
|
-
"dockPosition": null,
|
|
3274
3227
|
"data": {
|
|
3275
3228
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Canvas server map</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / system-map</div><h1>Canvas server map</h1><p>Module or architecture map with boxes, relationships, entry points, and hot paths.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"panel\"><h2>Entry Points</h2><div class=\"swatches\"><span class=\"badge info\">src/mcp/server.ts</span><span class=\"badge info\">src/server/server.ts</span><span class=\"badge info\">src/cli/index.ts</span></div></section>\n <section class=\"grid\" style=\"margin-top:14px\"><article class=\"card\"><div class=\"small\">entry</div><h2>MCP server</h2><p>stdio transport, 22 tools and 14 resources.</p></article><article class=\"card\"><div class=\"small\">entry</div><h2>HTTP API</h2><p>REST + SSE on Bun.serve.</p></article><article class=\"card\"><div class=\"small\">core</div><h2>Operation registry</h2><p>One definition drives HTTP, MCP and the SDK.</p></article><article class=\"card\"><div class=\"small\">core</div><h2>CanvasStateManager</h2><p>Singleton source of truth; every mutation goes through it.</p></article><article class=\"card\"><div class=\"small\">store</div><h2>SQLite</h2><p>.pmx-canvas/canvas.db — nodes, pins, snapshots, blobs.</p></article><article class=\"card\"><div class=\"small\">view</div><h2>Preact client</h2><p>Renders layout; state survives refresh.</p></article></section>\n <section class=\"panel\" style=\"margin-top:14px\"><h2>Relationships</h2><div class=\"flow\"><div class=\"flow-node\"><strong>mcp</strong><p>tool call</p><strong>ops</strong></div><div class=\"arrow\">+</div><div class=\"flow-node\"><strong>http</strong><p>route</p><strong>ops</strong></div><div class=\"arrow\">+</div><div class=\"flow-node\"><strong>ops</strong><p>mutate</p><strong>state</strong></div><div class=\"arrow\">+</div><div class=\"flow-node\"><strong>state</strong><p>debounced save</p><strong>db</strong></div><div class=\"arrow\">+</div><div class=\"flow-node\"><strong>state</strong><p>SSE</p><strong>client</strong></div></div></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"modules\": [\n {\n \"id\": \"mcp\",\n \"title\": \"MCP server\",\n \"detail\": \"stdio transport, 22 tools and 14 resources.\",\n \"role\": \"entry\"\n },\n {\n \"id\": \"http\",\n \"title\": \"HTTP API\",\n \"detail\": \"REST + SSE on Bun.serve.\",\n \"role\": \"entry\"\n },\n {\n \"id\": \"ops\",\n \"title\": \"Operation registry\",\n \"detail\": \"One definition drives HTTP, MCP and the SDK.\",\n \"role\": \"core\"\n },\n {\n \"id\": \"state\",\n \"title\": \"CanvasStateManager\",\n \"detail\": \"Singleton source of truth; every mutation goes through it.\",\n \"role\": \"core\"\n },\n {\n \"id\": \"db\",\n \"title\": \"SQLite\",\n \"detail\": \".pmx-canvas/canvas.db — nodes, pins, snapshots, blobs.\",\n \"role\": \"store\"\n },\n {\n \"id\": \"client\",\n \"title\": \"Preact client\",\n \"detail\": \"Renders layout; state survives refresh.\",\n \"role\": \"view\"\n }\n ],\n \"edges\": [\n {\n \"from\": \"mcp\",\n \"to\": \"ops\",\n \"label\": \"tool call\"\n },\n {\n \"from\": \"http\",\n \"to\": \"ops\",\n \"label\": \"route\"\n },\n {\n \"from\": \"ops\",\n \"to\": \"state\",\n \"label\": \"mutate\"\n },\n {\n \"from\": \"state\",\n \"to\": \"db\",\n \"label\": \"debounced save\"\n },\n {\n \"from\": \"state\",\n \"to\": \"client\",\n \"label\": \"SSE\"\n }\n ],\n \"entryPoints\": [\n \"src/mcp/server.ts\",\n \"src/server/server.ts\",\n \"src/cli/index.ts\"\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\n</script>\n</body>\n</html>",
|
|
3276
3229
|
"htmlPrimitive": "system-map",
|
|
@@ -3374,7 +3327,6 @@
|
|
|
3374
3327
|
"zIndex": 1,
|
|
3375
3328
|
"collapsed": false,
|
|
3376
3329
|
"pinned": false,
|
|
3377
|
-
"dockPosition": null,
|
|
3378
3330
|
"data": {
|
|
3379
3331
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Walkthrough — creating a node</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / code-walkthrough</div><h1>Walkthrough — creating a node</h1><p>Guided code-path map with modules, ordered file steps, snippets, key files, and gotchas.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"panel\"><h2>Path Summary</h2><p>One POST becomes a validated, clamped, persisted node and an SSE frame, through a single shared operation.</p><div class=\"flow\" style=\"margin-top:12px\"><div class=\"flow-node\"><div class=\"small\">entry</div><h3>Bun.serve fetch</h3><p>Matches the operation path.</p></div><div class=\"arrow\">-></div><div class=\"flow-node\"><div class=\"small\">core</div><h3>node.add</h3><p>Zod-validates the body, resolves geometry.</p></div><div class=\"arrow\">-></div><div class=\"flow-node\"><div class=\"small\">core</div><h3>createBasicCanvasNode</h3><p>Per-type data builders and the size clamp.</p></div><div class=\"arrow\">-></div><div class=\"flow-node\"><div class=\"small\">store</div><h3>canvasState.addNode</h3><p>Stores, schedules a save, records undo.</p></div></div><p class=\"small\" style=\"margin-top:10px\">Edges: route -> op (dispatch), op -> build (normalize), build -> state (add)</p></section>\n <section class=\"three\" style=\"margin-top:14px\"><div class=\"panel\"><h2>Walkthrough</h2><details open><summary><strong>1. Resolve the type</strong></summary><p class=\"small\">src/server/operations/ops/nodes.ts</p><p>A missing or unknown type is a 400, never a silent markdown node.</p><pre><code>if (!NODE_TYPE_SET.has(type)) throw new OperationError(...)</code></pre></details><details ><summary><strong>2. Build per-type data</strong></summary><p class=\"small\">src/server/canvas-operations.ts</p><p>file / image / webpage / trace each get a dedicated builder.</p><pre><code>if (input.type === "file") return buildFileNodeData(input);</code></pre></details><details ><summary><strong>3. Clamp the frame</strong></summary><p class=\"small\">src/shared/node-sizes.ts</p><p>An undersized explicit size is raised to the readability floor.</p><pre><code>clampCreateNodeSize(type, width, height, strictSize)</code></pre></details><details ><summary><strong>4. Store and broadcast</strong></summary><p class=\"small\">src/server/canvas-state.ts</p><p>addNode persists and notifies; the browser is only a renderer.</p></details></div>\n <div class=\"panel\"><h2>Key Files</h2><article class=\"card\"><h3>src/server/operations/ops/nodes.ts</h3><p>node.add / update / remove and the shared node payloads.</p></article><article class=\"card\"><h3>src/server/canvas-state.ts</h3><p>The singleton store, undo ring buffer and persistence.</p></article></div>\n <div class=\"panel sticky\"><h2>Gotchas</h2><ul><li>A new node type must also be added to isCanvasNodeType in the SSE bridge, or it renders as nothing.</li><li>Rebuild the client bundle after touching src/client — the dist bundle is not auto-built.</li></ul></div></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"summary\": \"One POST becomes a validated, clamped, persisted node and an SSE frame, through a single shared operation.\",\n \"modules\": [\n {\n \"id\": \"route\",\n \"title\": \"Bun.serve fetch\",\n \"detail\": \"Matches the operation path.\",\n \"role\": \"entry\"\n },\n {\n \"id\": \"op\",\n \"title\": \"node.add\",\n \"detail\": \"Zod-validates the body, resolves geometry.\",\n \"role\": \"core\"\n },\n {\n \"id\": \"build\",\n \"title\": \"createBasicCanvasNode\",\n \"detail\": \"Per-type data builders and the size clamp.\",\n \"role\": \"core\"\n },\n {\n \"id\": \"state\",\n \"title\": \"canvasState.addNode\",\n \"detail\": \"Stores, schedules a save, records undo.\",\n \"role\": \"store\"\n }\n ],\n \"edges\": [\n {\n \"from\": \"route\",\n \"to\": \"op\",\n \"label\": \"dispatch\"\n },\n {\n \"from\": \"op\",\n \"to\": \"build\",\n \"label\": \"normalize\"\n },\n {\n \"from\": \"build\",\n \"to\": \"state\",\n \"label\": \"add\"\n }\n ],\n \"steps\": [\n {\n \"title\": \"Resolve the type\",\n \"file\": \"src/server/operations/ops/nodes.ts\",\n \"detail\": \"A missing or unknown type is a 400, never a silent markdown node.\",\n \"code\": \"if (!NODE_TYPE_SET.has(type)) throw new OperationError(...)\"\n },\n {\n \"title\": \"Build per-type data\",\n \"file\": \"src/server/canvas-operations.ts\",\n \"detail\": \"file / image / webpage / trace each get a dedicated builder.\",\n \"code\": \"if (input.type === \\\"file\\\") return buildFileNodeData(input);\"\n },\n {\n \"title\": \"Clamp the frame\",\n \"file\": \"src/shared/node-sizes.ts\",\n \"detail\": \"An undersized explicit size is raised to the readability floor.\",\n \"code\": \"clampCreateNodeSize(type, width, height, strictSize)\"\n },\n {\n \"title\": \"Store and broadcast\",\n \"file\": \"src/server/canvas-state.ts\",\n \"detail\": \"addNode persists and notifies; the browser is only a renderer.\"\n }\n ],\n \"keyFiles\": [\n {\n \"path\": \"src/server/operations/ops/nodes.ts\",\n \"description\": \"node.add / update / remove and the shared node payloads.\"\n },\n {\n \"path\": \"src/server/canvas-state.ts\",\n \"description\": \"The singleton store, undo ring buffer and persistence.\"\n }\n ],\n \"gotchas\": [\n \"A new node type must also be added to isCanvasNodeType in the SSE bridge, or it renders as nothing.\",\n \"Rebuild the client bundle after touching src/client — the dist bundle is not auto-built.\"\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\n</script>\n</body>\n</html>",
|
|
3380
3332
|
"htmlPrimitive": "code-walkthrough",
|
|
@@ -3495,7 +3447,6 @@
|
|
|
3495
3447
|
"zIndex": 1,
|
|
3496
3448
|
"collapsed": false,
|
|
3497
3449
|
"pinned": false,
|
|
3498
|
-
"dockPosition": null,
|
|
3499
3450
|
"data": {
|
|
3500
3451
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Board typography directions</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / design-sheet</div><h1>Board typography directions</h1><p>Visual design directions, tokens, swatches, type samples, and rationale.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"grid\"><article class=\"card\"><h2>Instrument</h2><p>dense, technical, high contrast</p><div class=\"swatches\"><span class=\"swatch\" title=\"#0d1220\" style=\"background:#0d1220\"></span><span class=\"swatch\" title=\"#131a2b\" style=\"background:#131a2b\"></span><span class=\"swatch\" title=\"#4a9eff\" style=\"background:#4a9eff\"></span><span class=\"swatch\" title=\"#e6ecff\" style=\"background:#e6ecff\"></span></div><p>Reads like a console. Good for status, trace and ledger nodes where density beats warmth.</p></article><article class=\"card\"><h2>Editorial</h2><p>calm, generous leading</p><div class=\"swatches\"><span class=\"swatch\" title=\"#f8f1e7\" style=\"background:#f8f1e7\"></span><span class=\"swatch\" title=\"#16120f\" style=\"background:#16120f\"></span><span class=\"swatch\" title=\"#d65a31\" style=\"background:#d65a31\"></span><span class=\"swatch\" title=\"#6b5f55\" style=\"background:#6b5f55\"></span></div><p>Better for long markdown and explainers; loses signal on dashboards.</p></article><article class=\"card\"><h2>Blueprint</h2><p>cool, structural, muted</p><div class=\"swatches\"><span class=\"swatch\" title=\"#101826\" style=\"background:#101826\"></span><span class=\"swatch\" title=\"#1b2740\" style=\"background:#1b2740\"></span><span class=\"swatch\" title=\"#7c5cff\" style=\"background:#7c5cff\"></span><span class=\"swatch\" title=\"#8ee5ad\" style=\"background:#8ee5ad\"></span></div><p>Diagram-first. Chart ink stays readable against the panel at low zoom.</p></article></section>\n <section class=\"panel\" style=\"margin-top:14px\"><h2>Tokens</h2><div class=\"grid\"><div class=\"card\"><strong>--c-bg</strong><p>#0d1220</p></div><div class=\"card\"><strong>--c-panel</strong><p>#131a2b</p></div><div class=\"card\"><strong>--c-line</strong><p>#243049</p></div><div class=\"card\"><strong>--c-accent</strong><p>#4a9eff</p></div><div class=\"card\"><strong>--c-ok</strong><p>#22c55e</p></div><div class=\"card\"><strong>--c-warn</strong><p>#eab308</p></div></div></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"directions\": [\n {\n \"title\": \"Instrument\",\n \"tone\": \"dense, technical, high contrast\",\n \"palette\": [\n \"#0d1220\",\n \"#131a2b\",\n \"#4a9eff\",\n \"#e6ecff\"\n ],\n \"rationale\": \"Reads like a console. Good for status, trace and ledger nodes where density beats warmth.\"\n },\n {\n \"title\": \"Editorial\",\n \"tone\": \"calm, generous leading\",\n \"palette\": [\n \"#f8f1e7\",\n \"#16120f\",\n \"#d65a31\",\n \"#6b5f55\"\n ],\n \"rationale\": \"Better for long markdown and explainers; loses signal on dashboards.\"\n },\n {\n \"title\": \"Blueprint\",\n \"tone\": \"cool, structural, muted\",\n \"palette\": [\n \"#101826\",\n \"#1b2740\",\n \"#7c5cff\",\n \"#8ee5ad\"\n ],\n \"rationale\": \"Diagram-first. Chart ink stays readable against the panel at low zoom.\"\n }\n ],\n \"tokens\": [\n {\n \"name\": \"--c-bg\",\n \"value\": \"#0d1220\"\n },\n {\n \"name\": \"--c-panel\",\n \"value\": \"#131a2b\"\n },\n {\n \"name\": \"--c-line\",\n \"value\": \"#243049\"\n },\n {\n \"name\": \"--c-accent\",\n \"value\": \"#4a9eff\"\n },\n {\n \"name\": \"--c-ok\",\n \"value\": \"#22c55e\"\n },\n {\n \"name\": \"--c-warn\",\n \"value\": \"#eab308\"\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\n</script>\n</body>\n</html>",
|
|
3501
3452
|
"htmlPrimitive": "design-sheet",
|
|
@@ -3590,7 +3541,6 @@
|
|
|
3590
3541
|
"zIndex": 1,
|
|
3591
3542
|
"collapsed": false,
|
|
3592
3543
|
"pinned": false,
|
|
3593
|
-
"dockPosition": null,
|
|
3594
3544
|
"data": {
|
|
3595
3545
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Node titlebar states</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / component-gallery</div><h1>Node titlebar states</h1><p>Contact sheet for component variants, states, sizes, and accessibility notes.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"panel\"><h2>Node titlebar</h2><p>Variant contact sheet for fast visual review.</p></section>\n <section class=\"grid\" style=\"margin-top:14px\"><article class=\"card\"><div class=\"small\">rest / idle node</div><h2>Default</h2><button type=\"button\">markdown · Release notes</button><p>Type chip, title, collapse and expand affordances.</p></article><article class=\"card\"><div class=\"small\">context-pinned / included in agent context</div><h2>Pinned</h2><button type=\"button\">📌 markdown · Release notes</button><p>Accent ring; appears in canvas://pinned-context.</p></article><article class=\"card\"><div class=\"small\">match / result of canvas_query search</div><h2>Search match</h2><button type=\"button\">file · node-sizes.ts</button><p>Non-matches dim rather than hide.</p></article><article class=\"card\"><div class=\"small\">running / tool call in flight</div><h2>Running trace</h2><button type=\"button\">trace · edit_file</button><p>Pulsing status dot, no layout shift on completion.</p></article><article class=\"card\"><div class=\"small\">focus / agent asked for the human’s eyes</div><h2>Attention</h2><button type=\"button\">diff · Readability floor</button><p>Pulse decays; never steals the viewport.</p></article></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"component\": \"Node titlebar\",\n \"variants\": [\n {\n \"label\": \"Default\",\n \"state\": \"rest\",\n \"intent\": \"idle node\",\n \"example\": \"markdown · Release notes\",\n \"note\": \"Type chip, title, collapse and expand affordances.\"\n },\n {\n \"label\": \"Pinned\",\n \"state\": \"context-pinned\",\n \"intent\": \"included in agent context\",\n \"example\": \"📌 markdown · Release notes\",\n \"note\": \"Accent ring; appears in canvas://pinned-context.\"\n },\n {\n \"label\": \"Search match\",\n \"state\": \"match\",\n \"intent\": \"result of canvas_query search\",\n \"example\": \"file · node-sizes.ts\",\n \"note\": \"Non-matches dim rather than hide.\"\n },\n {\n \"label\": \"Running trace\",\n \"state\": \"running\",\n \"intent\": \"tool call in flight\",\n \"example\": \"trace · edit_file\",\n \"note\": \"Pulsing status dot, no layout shift on completion.\"\n },\n {\n \"label\": \"Attention\",\n \"state\": \"focus\",\n \"intent\": \"agent asked for the human’s eyes\",\n \"example\": \"diff · Readability floor\",\n \"note\": \"Pulse decays; never steals the viewport.\"\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\n</script>\n</body>\n</html>",
|
|
3596
3546
|
"htmlPrimitive": "component-gallery",
|
|
@@ -3665,7 +3615,6 @@
|
|
|
3665
3615
|
"zIndex": 1,
|
|
3666
3616
|
"collapsed": false,
|
|
3667
3617
|
"pinned": false,
|
|
3668
|
-
"dockPosition": null,
|
|
3669
3618
|
"data": {
|
|
3670
3619
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Node expand transition</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / interaction-prototype</div><h1>Node expand transition</h1><p>Throwaway interaction or motion sandbox with a live stage, controls, annotations, and copyable config.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"three\"><div class=\"panel\"><h2>Stage</h2><p>Tune the expand-to-overlay transition before wiring it into CanvasNode.</p><div class=\"flow\" style=\"margin-top:16px\"><button class=\"flow-node\" type=\"button\" data-screen=\"0\"><h3>Rest</h3><p>Node sits in the band at canvas scale.</p></button><div class=\"arrow\">-></div><button class=\"flow-node\" type=\"button\" data-screen=\"1\"><h3>Lift</h3><p>Node scales toward the overlay frame; siblings dim.</p></button><div class=\"arrow\">-></div><button class=\"flow-node\" type=\"button\" data-screen=\"2\"><h3>Overlay</h3><p>Full-size surface with the same renderer, expanded props.</p></button></div><div class=\"card emphasis\" id=\"prototype-readout\" style=\"margin-top:14px\">Select a screen to inspect it.</div></div>\n <div class=\"panel\"><h2>Controls</h2><label class=\"card\"><span class=\"small\">duration</span><h3>Duration: <span data-control-value=\"0\">240</span>ms</h3><input type=\"range\" data-control=\"0\" min=\"80\" max=\"700\" value=\"240\"></label><label class=\"card\"><span class=\"small\">overshoot</span><h3>Overshoot: <span data-control-value=\"1\">6</span>%</h3><input type=\"range\" data-control=\"1\" min=\"0\" max=\"24\" value=\"6\"></label><label class=\"card\"><span class=\"small\">dim</span><h3>Backdrop dim: <span data-control-value=\"2\">62</span>%</h3><input type=\"range\" data-control=\"2\" min=\"0\" max=\"100\" value=\"62\"></label></div>\n <div class=\"panel sticky\"><h2>Notes</h2><article class=\"card\"><h3>Same renderer</h3><p>The overlay must not fork the node component, or surfaces drift.</p></article><article class=\"card\"><h3>No viewport jump</h3><p>Expanding never moves the underlying canvas.</p></article><h2 style=\"margin-top:18px\">Questions</h2><ul><li>Does overshoot help or just feel noisy at 0.4 zoom?</li><li>Should the backdrop dim scale with node count?</li></ul><pre><code>transition: transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1);</code></pre></div></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"scenario\": \"Tune the expand-to-overlay transition before wiring it into CanvasNode.\",\n \"controls\": [\n {\n \"key\": \"duration\",\n \"label\": \"Duration\",\n \"value\": 240,\n \"min\": 80,\n \"max\": 700,\n \"unit\": \"ms\"\n },\n {\n \"key\": \"overshoot\",\n \"label\": \"Overshoot\",\n \"value\": 6,\n \"min\": 0,\n \"max\": 24,\n \"unit\": \"%\"\n },\n {\n \"key\": \"dim\",\n \"label\": \"Backdrop dim\",\n \"value\": 62,\n \"min\": 0,\n \"max\": 100,\n \"unit\": \"%\"\n }\n ],\n \"screens\": [\n {\n \"title\": \"Rest\",\n \"detail\": \"Node sits in the band at canvas scale.\"\n },\n {\n \"title\": \"Lift\",\n \"detail\": \"Node scales toward the overlay frame; siblings dim.\"\n },\n {\n \"title\": \"Overlay\",\n \"detail\": \"Full-size surface with the same renderer, expanded props.\"\n }\n ],\n \"annotations\": [\n {\n \"title\": \"Same renderer\",\n \"detail\": \"The overlay must not fork the node component, or surfaces drift.\"\n },\n {\n \"title\": \"No viewport jump\",\n \"detail\": \"Expanding never moves the underlying canvas.\"\n }\n ],\n \"questions\": [\n \"Does overshoot help or just feel noisy at 0.4 zoom?\",\n \"Should the backdrop dim scale with node count?\"\n ],\n \"snippet\": \"transition: transform 240ms cubic-bezier(0.2, 0.8, 0.2, 1);\"\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nconst screens = PMX_DATA.screens || [];\ndocument.querySelectorAll('[data-screen]').forEach((button) => button.addEventListener('click', () => {\n const screen = screens[Number(button.getAttribute('data-screen'))] || {};\n document.getElementById('prototype-readout').textContent = (screen.title || 'Screen') + ': ' + (screen.detail || '');\n}));\ndocument.querySelectorAll('[data-control]').forEach((input) => input.addEventListener('input', () => {\n document.querySelector('[data-control-value=\"' + input.getAttribute('data-control') + '\"]').textContent = input.value;\n}));\nwindow.__pmxGetCopyJson = () => ({ ...PMX_DATA, controls: Array.from(document.querySelectorAll('[data-control]')).map((input, index) => ({ ...(PMX_DATA.controls || [])[index], value: Number(input.value) })) });\n</script>\n</body>\n</html>",
|
|
3671
3620
|
"htmlPrimitive": "interaction-prototype",
|
|
@@ -3755,7 +3704,6 @@
|
|
|
3755
3704
|
"zIndex": 1,
|
|
3756
3705
|
"collapsed": false,
|
|
3757
3706
|
"pinned": false,
|
|
3758
|
-
"dockPosition": null,
|
|
3759
3707
|
"data": {
|
|
3760
3708
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Release pipeline</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / flowchart</div><h1>Release pipeline</h1><p>Clickable flowchart for pipelines, user journeys, process diagrams, and failure paths.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"flow\"><button class=\"flow-node\" type=\"button\" data-step=\"0\"><h3>Pre-flight</h3><p><span class=\"badge ok\">ok</span> 61s</p></button><div class=\"arrow\">-></div><button class=\"flow-node\" type=\"button\" data-step=\"1\"><h3>Build</h3><p><span class=\"badge ok\">ok</span> 44s</p></button><div class=\"arrow\">-></div><button class=\"flow-node\" type=\"button\" data-step=\"2\"><h3>Version bump</h3><p><span class=\"badge ok\">ok</span> 8s</p></button><div class=\"arrow\">-></div><button class=\"flow-node\" type=\"button\" data-step=\"3\"><h3>Tag and publish</h3><p><span class=\"badge info\">active</span> —</p></button><div class=\"arrow\">-></div><button class=\"flow-node\" type=\"button\" data-step=\"4\"><h3>Smoke</h3><p><span class=\"badge info\">todo</span> —</p></button></section>\n <section class=\"panel\" style=\"margin-top:14px\"><h2 id=\"step-title\">Pre-flight</h2><p id=\"step-detail\">typecheck, lint, unit and client suites.</p></section>\n <section class=\"panel\" style=\"margin-top:14px\"><h2>Failure Paths</h2><article class=\"card\"><h3>Smoke: missing file in package</h3><p>Add it to the files array and publish a patch.</p></article><article class=\"card\"><h3>Tag and publish: registry 403</h3><p>Re-auth, then republish the same tag.</p></article></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"steps\": [\n {\n \"title\": \"Pre-flight\",\n \"detail\": \"typecheck, lint, unit and client suites.\",\n \"status\": \"ok\",\n \"duration\": \"61s\"\n },\n {\n \"title\": \"Build\",\n \"detail\": \"Client bundle, json-render bundle, types.\",\n \"status\": \"ok\",\n \"duration\": \"44s\"\n },\n {\n \"title\": \"Version bump\",\n \"detail\": \"package.json and CHANGELOG entry.\",\n \"status\": \"ok\",\n \"duration\": \"8s\"\n },\n {\n \"title\": \"Tag and publish\",\n \"detail\": \"npm publish from the tagged commit.\",\n \"status\": \"active\",\n \"duration\": \"—\"\n },\n {\n \"title\": \"Smoke\",\n \"detail\": \"Install the published tarball in a clean temp consumer.\",\n \"status\": \"todo\",\n \"duration\": \"—\"\n }\n ],\n \"failurePaths\": [\n {\n \"from\": \"Smoke\",\n \"label\": \"missing file in package\",\n \"detail\": \"Add it to the files array and publish a patch.\"\n },\n {\n \"from\": \"Tag and publish\",\n \"label\": \"registry 403\",\n \"detail\": \"Re-auth, then republish the same tag.\"\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nconst steps = PMX_DATA.steps || [];\ndocument.querySelectorAll('[data-step]').forEach((button) => button.addEventListener('click', () => {\n const step = steps[Number(button.getAttribute('data-step'))] || {};\n document.getElementById('step-title').textContent = step.title || step.label || 'Step';\n document.getElementById('step-detail').textContent = step.detail || step.description || '';\n}));\n</script>\n</body>\n</html>",
|
|
3761
3709
|
"htmlPrimitive": "flowchart",
|
|
@@ -3833,7 +3781,6 @@
|
|
|
3833
3781
|
"zIndex": 1,
|
|
3834
3782
|
"collapsed": false,
|
|
3835
3783
|
"pinned": false,
|
|
3836
|
-
"dockPosition": null,
|
|
3837
3784
|
"data": {
|
|
3838
3785
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Why a spatial canvas</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / deck</div><h1>Why a spatial canvas</h1><p>Arrow-key HTML deck with sections, speaker notes, and copyable JSON payload.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"panel\"><div class=\"small\"><span id=\"slide-count\">1</span> / 3 - use left/right arrows</div><article class=\"slide active\" data-slide=\"0\"><div><div class=\"kicker\">01</div><h2>Chat is a queue, work is a graph</h2><p></p><ul><li>A transcript only remembers order</li><li>Real work has neighbours, not turns</li><li>Space is free context</li></ul><p class=\"small\">Open on the problem, not the product.</p></div></article><article class=\"slide \" data-slide=\"1\"><div><div class=\"kicker\">02</div><h2>The canvas is the agent’s working memory</h2><p></p><ul><li>Nodes persist across sessions</li><li>Position carries meaning</li><li>The agent can read its own board back</li></ul><p class=\"small\"></p></div></article><article class=\"slide \" data-slide=\"2\"><div><div class=\"kicker\">03</div><h2>Pinning is the steering wheel</h2><p></p><ul><li>Human pins what matters</li><li>Agent reads canvas://pinned-context</li><li>No prompt engineering required</li></ul><p class=\"small\">Demo the pin loop live if there is time.</p></div></article></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"slides\": [\n {\n \"title\": \"Chat is a queue, work is a graph\",\n \"kicker\": \"01\",\n \"bullets\": [\n \"A transcript only remembers order\",\n \"Real work has neighbours, not turns\",\n \"Space is free context\"\n ],\n \"note\": \"Open on the problem, not the product.\"\n },\n {\n \"title\": \"The canvas is the agent’s working memory\",\n \"kicker\": \"02\",\n \"bullets\": [\n \"Nodes persist across sessions\",\n \"Position carries meaning\",\n \"The agent can read its own board back\"\n ]\n },\n {\n \"title\": \"Pinning is the steering wheel\",\n \"kicker\": \"03\",\n \"bullets\": [\n \"Human pins what matters\",\n \"Agent reads canvas://pinned-context\",\n \"No prompt engineering required\"\n ],\n \"note\": \"Demo the pin loop live if there is time.\"\n }\n ],\n \"presentation\": true,\n \"slideCount\": 3,\n \"slideTitles\": [\n \"Chat is a queue, work is a graph\",\n \"The canvas is the agent’s working memory\",\n \"Pinning is the steering wheel\"\n ],\n \"speakerNotes\": [\n \"Open on the problem, not the product.\",\n \"Demo the pin loop live if there is time.\"\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nlet currentSlide = 0;\nconst slides = Array.from(document.querySelectorAll('[data-slide]'));\nfunction showSlide(index) {\n currentSlide = Math.max(0, Math.min(slides.length - 1, index));\n slides.forEach((slide, i) => slide.classList.toggle('active', i === currentSlide));\n document.getElementById('slide-count').textContent = String(currentSlide + 1);\n}\ndocument.addEventListener('keydown', (event) => {\n if (event.key === 'ArrowRight' || event.key === 'PageDown' || event.key === ' ') showSlide(currentSlide + 1);\n if (event.key === 'ArrowLeft' || event.key === 'PageUp') showSlide(currentSlide - 1);\n});\n</script>\n</body>\n</html>",
|
|
3839
3786
|
"htmlPrimitive": "deck",
|
|
@@ -3920,7 +3867,6 @@
|
|
|
3920
3867
|
"zIndex": 1,
|
|
3921
3868
|
"collapsed": false,
|
|
3922
3869
|
"pinned": false,
|
|
3923
|
-
"dockPosition": null,
|
|
3924
3870
|
"data": {
|
|
3925
3871
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>PMX Canvas 0.4.7 briefing</title>\n<style>\n :root { color-scheme: dark; --deck-accent: #4BBCFF; --deck-bg: #081524; --deck-panel: #0f1d31; --deck-surface: #10213a; --deck-border: #1b2c44; --deck-text: #e6eef7; --deck-text-secondary: #c7d3ea; --deck-text-muted: #8ea3bd; }\n * { box-sizing: border-box; }\n html, body { width: 100%; height: 100%; overflow: hidden; }\n body { margin: 0; padding: 0; background: var(--deck-bg); color: var(--deck-text); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .deck { height: 100vh; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; background: radial-gradient(circle at 10% 10%, color-mix(in srgb, var(--deck-accent) 32%, transparent), transparent 28rem), linear-gradient(135deg, color-mix(in srgb, var(--deck-panel) 88%, black), var(--deck-bg)); }\n .topbar, .bottombar { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: clamp(12px, 2.5vmin, 24px) clamp(18px, 4vw, 48px); color: var(--deck-text-secondary); }\n .brand { display: grid; gap: 2px; }\n .brand p { margin: 0; }\n .eyebrow { color: var(--deck-accent); font-size: 11px; font-weight: 900; letter-spacing: .18em; text-transform: uppercase; }\n .title { max-width: 70vw; overflow: hidden; color: var(--deck-text); font-size: clamp(16px, 2vw, 24px); font-weight: 850; letter-spacing: -.03em; text-overflow: ellipsis; white-space: nowrap; }\n .controls { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid color-mix(in srgb, var(--deck-border) 82%, white); background: color-mix(in srgb, var(--deck-panel) 82%, transparent); color: var(--deck-text); border-radius: 999px; padding: 8px 12px; font: 700 12px/1 var(--font-sans, ui-sans-serif, system-ui, sans-serif); cursor: pointer; }\n button:hover, button.active { border-color: var(--deck-accent); color: var(--deck-text); background: color-mix(in srgb, var(--deck-accent) 18%, var(--deck-panel)); }\n .slides { min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }\n .slide { display: none; min-height: 100%; align-content: center; gap: clamp(16px, 3vmin, 28px); padding: clamp(24px, 5vmin, 64px) clamp(28px, 6vw, 92px); }\n .slide.active { display: grid; }\n .slide-grid { display: grid; gap: clamp(24px, 5vw, 72px); align-items: center; }\n .slide-grid.with-metrics { grid-template-columns: minmax(0, 1.15fr) minmax(260px, .85fr); }\n .slide-grid.without-metrics { grid-template-columns: minmax(0, 1fr); }\n .slide-copy { max-width: min(1120px, 100%); }\n .kicker { color: var(--deck-accent); font-size: clamp(12px, 1.8vw, 18px); font-weight: 950; letter-spacing: .18em; text-transform: uppercase; }\n h2 { margin: 10px 0 18px; max-width: 16ch; font-size: clamp(40px, 8vmin, 104px); line-height: .9; letter-spacing: -.07em; }\n .lede { max-width: 900px; margin: 0 0 20px; color: var(--deck-text-secondary); font-size: clamp(18px, 3vmin, 32px); line-height: 1.14; letter-spacing: -.03em; }\n ul { display: grid; gap: 12px; max-width: 780px; margin: 0; padding: 0; list-style: none; }\n li { position: relative; padding-left: 30px; color: var(--deck-text-secondary); font-size: clamp(17px, 2.2vmin, 25px); line-height: 1.22; }\n li::before { content: ''; position: absolute; left: 0; top: .42em; width: 12px; height: 12px; border-radius: 50%; background: var(--deck-accent); box-shadow: 0 0 24px color-mix(in srgb, var(--deck-accent) 60%, transparent); }\n .metrics { display: grid; gap: 14px; }\n .metric { border: 1px solid color-mix(in srgb, var(--deck-accent) 42%, var(--deck-border)); border-radius: 28px; padding: 22px; background: color-mix(in srgb, var(--deck-panel) 78%, transparent); box-shadow: 0 24px 70px rgba(0,0,0,.26); }\n .metric span { color: var(--deck-text-muted); font-size: 11px; font-weight: 900; letter-spacing: .14em; text-transform: uppercase; }\n .metric strong { display: block; margin-top: 8px; font-size: clamp(34px, 6vw, 78px); line-height: .9; letter-spacing: -.06em; }\n .metric p { margin: 10px 0 0; color: var(--deck-text-secondary); }\n .speaker-note { max-width: min(1120px, 100%); border-left: 4px solid var(--deck-accent); padding: 10px 14px; color: var(--deck-text-muted); background: color-mix(in srgb, var(--deck-panel) 76%, transparent); border-radius: 14px; }\n .speaker-note span { display: block; margin-bottom: 2px; color: var(--deck-accent); font-size: 10px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; }\n .dots { display: flex; gap: 7px; align-items: center; }\n .dot { width: 30px; height: 7px; border: 0; border-radius: 999px; padding: 0; background: color-mix(in srgb, var(--deck-text) 24%, transparent); }\n .dot.active { width: 54px; background: var(--deck-accent); }\n .hint { font-size: 12px; color: var(--deck-text-muted); }\n html[data-pmx-presentation-mode=\"present\"] .hint { display: none; }\n .progress { height: 3px; width: 180px; overflow: hidden; border-radius: 999px; background: color-mix(in srgb, var(--deck-text) 18%, transparent); }\n .progress span { display: block; height: 100%; width: 0; background: var(--deck-accent); transition: width .2s ease; }\n @media (max-width: 820px) { .slide-grid { grid-template-columns: 1fr; } .metrics { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); } h2 { max-width: none; font-size: clamp(42px, 14vw, 78px); } .lede, li { font-size: 21px; } .topbar { align-items: flex-start; flex-direction: column; } .title { max-width: 100%; } }\n</style>\n</head>\n<body>\n<main class=\"deck\">\n <header class=\"topbar\">\n <div class=\"brand\"><div class=\"eyebrow\">PMX presentation</div><div class=\"title\">PMX Canvas 0.4.7 briefing</div><p>What shipped, what it costs, and what we do next.</p></div>\n </header>\n <section class=\"slides\"><article class=\"slide active\" data-slide=\"0\">\n <div class=\"slide-grid with-metrics\">\n <div class=\"slide-copy\">\n <div class=\"kicker\">01</div>\n <h2>Where we are</h2>\n <p class=\"lede\">The canvas is the shared surface between the agent and the human.</p>\n <ul><li>15 node types</li><li>22 MCP tools</li><li>14 MCP resources</li></ul>\n </div>\n <div class=\"metrics\"><div class=\"metric\"><span>Node types</span><strong>15</strong><p>all catalogued on the demo board</p></div><div class=\"metric\"><span>Graph types</span><strong>12</strong><p>canonical plus Tufte primitives</p></div><div class=\"metric\"><span>HTML primitives</span><strong>21</strong><p>generated, self-contained surfaces</p></div></div>\n </div>\n <aside class=\"speaker-note\"><span>Speaker note</span>Lead with the surface count — it is the clearest proxy for reach.</aside>\n </article><article class=\"slide \" data-slide=\"1\">\n <div class=\"slide-grid without-metrics\">\n <div class=\"slide-copy\">\n <div class=\"kicker\">02</div>\n <h2>What changed in 0.4.7</h2>\n \n <ul><li>The demo board is now a generated catalogue</li><li>Readability floors are shared by server and client</li><li>Delimited files render as tables</li></ul>\n </div>\n \n </div>\n <aside class=\"speaker-note\"><span>Speaker note</span>Keep this slide to three lines; detail lives in the changelog.</aside>\n </article><article class=\"slide \" data-slide=\"2\">\n <div class=\"slide-grid without-metrics\">\n <div class=\"slide-copy\">\n <div class=\"kicker\">03</div>\n <h2>Next</h2>\n <p class=\"lede\">Make the board the first thing a new user sees, not the last.</p>\n <ul><li>Ship --demo in the quickstart</li><li>Record a 60-second tour</li><li>Add a per-band deep link</li></ul>\n </div>\n \n </div>\n \n </article></section>\n <footer class=\"bottombar\">\n <div class=\"dots\"><button class=\"dot active\" type=\"button\" data-dot=\"0\" aria-label=\"Go to slide 1\"></button><button class=\"dot \" type=\"button\" data-dot=\"1\" aria-label=\"Go to slide 2\"></button><button class=\"dot \" type=\"button\" data-dot=\"2\" aria-label=\"Go to slide 3\"></button></div>\n <div class=\"hint\"><span id=\"slide-current\">1</span> / 3 - Arrow keys, Space, Page Up/Down</div>\n <div class=\"progress\" aria-hidden=\"true\"><span id=\"slide-progress\"></span></div>\n </footer>\n</main>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"subtitle\": \"What shipped, what it costs, and what we do next.\",\n \"theme\": \"midnight\",\n \"slides\": [\n {\n \"title\": \"Where we are\",\n \"kicker\": \"01\",\n \"body\": \"The canvas is the shared surface between the agent and the human.\",\n \"bullets\": [\n \"15 node types\",\n \"22 MCP tools\",\n \"14 MCP resources\"\n ],\n \"metrics\": [\n {\n \"label\": \"Node types\",\n \"value\": \"15\",\n \"detail\": \"all catalogued on the demo board\"\n },\n {\n \"label\": \"Graph types\",\n \"value\": \"12\",\n \"detail\": \"canonical plus Tufte primitives\"\n },\n {\n \"label\": \"HTML primitives\",\n \"value\": \"21\",\n \"detail\": \"generated, self-contained surfaces\"\n }\n ],\n \"note\": \"Lead with the surface count — it is the clearest proxy for reach.\"\n },\n {\n \"title\": \"What changed in 0.4.7\",\n \"kicker\": \"02\",\n \"bullets\": [\n \"The demo board is now a generated catalogue\",\n \"Readability floors are shared by server and client\",\n \"Delimited files render as tables\"\n ],\n \"note\": \"Keep this slide to three lines; detail lives in the changelog.\"\n },\n {\n \"title\": \"Next\",\n \"kicker\": \"03\",\n \"body\": \"Make the board the first thing a new user sees, not the last.\",\n \"bullets\": [\n \"Ship --demo in the quickstart\",\n \"Record a 60-second tour\",\n \"Add a per-band deep link\"\n ]\n }\n ],\n \"presentation\": true,\n \"slideCount\": 3,\n \"slideTitles\": [\n \"Where we are\",\n \"What changed in 0.4.7\",\n \"Next\"\n ],\n \"speakerNotes\": [\n \"Lead with the surface count — it is the clearest proxy for reach.\",\n \"Keep this slide to three lines; detail lives in the changelog.\"\n ],\n \"presentationTheme\": \"midnight\"\n}</script>\n<script>\nlet currentSlide = 0;\nconst slides = Array.from(document.querySelectorAll('[data-slide]'));\nconst dots = Array.from(document.querySelectorAll('[data-dot]'));\nfunction showSlide(index) {\n currentSlide = Math.max(0, Math.min(slides.length - 1, index));\n slides.forEach((slide, i) => slide.classList.toggle('active', i === currentSlide));\n dots.forEach((dot, i) => dot.classList.toggle('active', i === currentSlide));\n document.getElementById('slide-current').textContent = String(currentSlide + 1);\n document.getElementById('slide-progress').style.width = String(((currentSlide + 1) / slides.length) * 100) + '%';\n}\ndots.forEach((dot) => dot.addEventListener('click', () => showSlide(Number(dot.getAttribute('data-dot')))));\nfunction handlePresentationKey(key) {\n if (key === 'ArrowRight' || key === 'PageDown' || key === ' ') { showSlide(currentSlide + 1); return true; }\n if (key === 'ArrowLeft' || key === 'PageUp') { showSlide(currentSlide - 1); return true; }\n if (key === 'Home') { showSlide(0); return true; }\n if (key === 'End') { showSlide(slides.length - 1); return true; }\n return false;\n}\nwindow.PMX_CANVAS_PRESENTATION_HANDLE_KEY = handlePresentationKey;\ndocument.addEventListener('pmx-presentation-key', (event) => {\n if (!event.detail || typeof event.detail.key !== 'string') return;\n handlePresentationKey(event.detail.key);\n});\ndocument.addEventListener('keydown', (event) => {\n const tag = event.target && event.target.tagName;\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;\n if (handlePresentationKey(event.key)) event.preventDefault();\n});\nshowSlide(0);\n</script>\n</body>\n</html>",
|
|
3926
3872
|
"htmlPrimitive": "presentation",
|
|
@@ -4030,7 +3976,6 @@
|
|
|
4030
3976
|
"zIndex": 1,
|
|
4031
3977
|
"collapsed": false,
|
|
4032
3978
|
"pinned": false,
|
|
4033
|
-
"dockPosition": null,
|
|
4034
3979
|
"data": {
|
|
4035
3980
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Figures — the collaboration loop</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / illustration-set</div><h1>Figures — the collaboration loop</h1><p>Inline SVG figure sheet with captions and per-figure SVG copy/export controls.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"grid\"><article class=\"card figure\"><h2>The pin loop</h2><svg viewBox=\"0 0 560 260\" role=\"img\" aria-label=\"The pin loop\" data-figure=\"0\"><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><rect x=\"40\" y=\"60\" width=\"150\" height=\"70\" rx=\"14\" fill=\"#22c55e\" opacity=\"0.18\" stroke=\"#22c55e\" stroke-width=\"2\" /><text x=\"52\" y=\"88\" fill=\"var(--color-text, #e8edf2)\" font-size=\"13\" font-weight=\"700\">Human</text><line x1=\"196\" y1=\"95\" x2=\"300\" y2=\"95\" stroke=\"#eab308\" stroke-width=\"2.5\" stroke-linecap=\"round\" marker-end=\"url(#arrow)\"/><rect x=\"306\" y=\"60\" width=\"150\" height=\"70\" rx=\"14\" fill=\"#7c5cff\" opacity=\"0.18\" stroke=\"#7c5cff\" stroke-width=\"2\" /><text x=\"318\" y=\"88\" fill=\"var(--color-text, #e8edf2)\" font-size=\"13\" font-weight=\"700\">Canvas</text><line x1=\"381\" y1=\"136\" x2=\"381\" y2=\"186\" stroke=\"#4a9eff\" stroke-width=\"2.5\" stroke-linecap=\"round\" marker-end=\"url(#arrow)\"/><circle cx=\"381\" cy=\"214\" r=\"30\" fill=\"#4a9eff\" opacity=\"0.22\" stroke=\"#4a9eff\" stroke-width=\"2\" /><text x=\"381\" y=\"218\" text-anchor=\"middle\" fill=\"var(--color-text, #e8edf2)\" font-size=\"13\" font-weight=\"700\">Agent</text><text x=\"40\" y=\"214\" fill=\"var(--color-text, #e8edf2)\" font-size=\"15\" font-weight=\"700\">pins → canvas://pinned-context</text></svg><p>The human curates space; the agent reads that curation as context.</p><button type=\"button\" data-copy-svg=\"0\">Copy SVG</button></article><article class=\"card figure\"><h2>One state, many surfaces</h2><svg viewBox=\"0 0 560 260\" role=\"img\" aria-label=\"One state, many surfaces\" data-figure=\"1\"><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><rect x=\"30\" y=\"40\" width=\"120\" height=\"52\" rx=\"14\" fill=\"#4a9eff\" opacity=\"0.18\" stroke=\"#4a9eff\" stroke-width=\"2\" /><text x=\"42\" y=\"68\" fill=\"var(--color-text, #e8edf2)\" font-size=\"13\" font-weight=\"700\">MCP</text><rect x=\"30\" y=\"108\" width=\"120\" height=\"52\" rx=\"14\" fill=\"#7c5cff\" opacity=\"0.18\" stroke=\"#7c5cff\" stroke-width=\"2\" /><text x=\"42\" y=\"136\" fill=\"var(--color-text, #e8edf2)\" font-size=\"13\" font-weight=\"700\">HTTP</text><rect x=\"30\" y=\"176\" width=\"120\" height=\"52\" rx=\"14\" fill=\"#22c55e\" opacity=\"0.18\" stroke=\"#22c55e\" stroke-width=\"2\" /><text x=\"42\" y=\"204\" fill=\"var(--color-text, #e8edf2)\" font-size=\"13\" font-weight=\"700\">SDK</text><line x1=\"156\" y1=\"66\" x2=\"280\" y2=\"130\" stroke=\"#8994b3\" stroke-width=\"2.5\" stroke-linecap=\"round\" marker-end=\"url(#arrow)\"/><line x1=\"156\" y1=\"134\" x2=\"280\" y2=\"134\" stroke=\"#8994b3\" stroke-width=\"2.5\" stroke-linecap=\"round\" marker-end=\"url(#arrow)\"/><line x1=\"156\" y1=\"202\" x2=\"280\" y2=\"138\" stroke=\"#8994b3\" stroke-width=\"2.5\" stroke-linecap=\"round\" marker-end=\"url(#arrow)\"/><rect x=\"286\" y=\"104\" width=\"200\" height=\"60\" rx=\"14\" fill=\"#eab308\" opacity=\"0.18\" stroke=\"#eab308\" stroke-width=\"2\" /><text x=\"298\" y=\"132\" fill=\"var(--color-text, #e8edf2)\" font-size=\"13\" font-weight=\"700\">Operation registry</text></svg><p>MCP, HTTP and the SDK are three doors into the same operation registry.</p><button type=\"button\" data-copy-svg=\"1\">Copy SVG</button></article></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"figures\": [\n {\n \"title\": \"The pin loop\",\n \"caption\": \"The human curates space; the agent reads that curation as context.\",\n \"shapes\": [\n {\n \"type\": \"rect\",\n \"x\": 40,\n \"y\": 60,\n \"width\": 150,\n \"height\": 70,\n \"text\": \"Human\",\n \"color\": \"#22c55e\"\n },\n {\n \"type\": \"arrow\",\n \"x1\": 196,\n \"y1\": 95,\n \"x2\": 300,\n \"y2\": 95,\n \"color\": \"#eab308\"\n },\n {\n \"type\": \"rect\",\n \"x\": 306,\n \"y\": 60,\n \"width\": 150,\n \"height\": 70,\n \"text\": \"Canvas\",\n \"color\": \"#7c5cff\"\n },\n {\n \"type\": \"arrow\",\n \"x1\": 381,\n \"y1\": 136,\n \"x2\": 381,\n \"y2\": 186,\n \"color\": \"#4a9eff\"\n },\n {\n \"type\": \"circle\",\n \"cx\": 381,\n \"cy\": 214,\n \"r\": 30,\n \"text\": \"Agent\",\n \"color\": \"#4a9eff\"\n },\n {\n \"type\": \"text\",\n \"x\": 40,\n \"y\": 214,\n \"text\": \"pins → canvas://pinned-context\",\n \"color\": \"#8994b3\"\n }\n ]\n },\n {\n \"title\": \"One state, many surfaces\",\n \"caption\": \"MCP, HTTP and the SDK are three doors into the same operation registry.\",\n \"shapes\": [\n {\n \"type\": \"rect\",\n \"x\": 30,\n \"y\": 40,\n \"width\": 120,\n \"height\": 52,\n \"text\": \"MCP\",\n \"color\": \"#4a9eff\"\n },\n {\n \"type\": \"rect\",\n \"x\": 30,\n \"y\": 108,\n \"width\": 120,\n \"height\": 52,\n \"text\": \"HTTP\",\n \"color\": \"#7c5cff\"\n },\n {\n \"type\": \"rect\",\n \"x\": 30,\n \"y\": 176,\n \"width\": 120,\n \"height\": 52,\n \"text\": \"SDK\",\n \"color\": \"#22c55e\"\n },\n {\n \"type\": \"arrow\",\n \"x1\": 156,\n \"y1\": 66,\n \"x2\": 280,\n \"y2\": 130,\n \"color\": \"#8994b3\"\n },\n {\n \"type\": \"arrow\",\n \"x1\": 156,\n \"y1\": 134,\n \"x2\": 280,\n \"y2\": 134,\n \"color\": \"#8994b3\"\n },\n {\n \"type\": \"arrow\",\n \"x1\": 156,\n \"y1\": 202,\n \"x2\": 280,\n \"y2\": 138,\n \"color\": \"#8994b3\"\n },\n {\n \"type\": \"rect\",\n \"x\": 286,\n \"y\": 104,\n \"width\": 200,\n \"height\": 60,\n \"text\": \"Operation registry\",\n \"color\": \"#eab308\"\n }\n ]\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\ndocument.querySelectorAll('[data-copy-svg]').forEach((button) => button.addEventListener('click', () => {\n const svg = document.querySelector('[data-figure=\"' + button.getAttribute('data-copy-svg') + '\"]');\n copyText(svg ? svg.outerHTML : '');\n}));\n</script>\n</body>\n</html>",
|
|
4036
3981
|
"htmlPrimitive": "illustration-set",
|
|
@@ -4187,7 +4132,6 @@
|
|
|
4187
4132
|
"zIndex": 1,
|
|
4188
4133
|
"collapsed": false,
|
|
4189
4134
|
"pinned": false,
|
|
4190
|
-
"dockPosition": null,
|
|
4191
4135
|
"data": {
|
|
4192
4136
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>How context pins work</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / explainer</div><h1>How context pins work</h1><p>Readable explainer with TLDR, collapsible steps, annotated snippets, FAQ, and glossary.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"panel\"><h2>TLDR</h2><p>A pin is a small, durable statement of relevance. The human makes it with a click; the agent reads it as a resource and gets the neighbourhood around it for free.</p></section>\n <section class=\"grid\" style=\"margin-top:14px\"><details open><summary><strong>1. The human pins a node</strong></summary><p>Clicking the pin affordance adds the node id to the canvas pin set (capped at 20).</p></details><details open><summary><strong>2. The server notifies</strong></summary><p>Pin changes emit notifications/resources/updated for canvas://pinned-context.</p></details><details open><summary><strong>3. The agent reads the resource</strong></summary><p>It receives the pinned nodes plus, for each, the nearby unpinned nodes — the implicit context.</p></details><details open><summary><strong>4. Spatial neighbours come along</strong></summary><p>Proximity clustering means the human never has to pin everything, only the anchors.</p></details></section>\n <section class=\"panel\" style=\"margin-top:14px\"><h2>Annotated Snippets</h2><h3>Pin from an agent</h3><p>mode defaults to "set", which replaces the whole list.</p><pre><code>canvas_pin_nodes({ nodeIds: ["node-a", "node-b"], mode: "add" })</code></pre><h3>Read it back</h3><p>Includes the neighbourhood for each pin.</p><pre><code>resources/read canvas://pinned-context</code></pre></section>\n <section class=\"two\" style=\"margin-top:14px\"><div class=\"panel\"><h2>FAQ</h2><details><summary>Does pinning move or lock the node?</summary><p>No. Context pins are separate from arrange-locking; a pinned node still moves.</p></details><details><summary>What happens when a pinned node is deleted?</summary><p>The pin is dropped with it — pins always resolve to live nodes.</p></details><details><summary>Is there a limit?</summary><p>Twenty pins. Past that, curation stops being curation.</p></details></div><div class=\"panel\"><h2>Glossary</h2><div class=\"card\"><strong>Context pin</strong><p>A human-curated marker that puts a node into the agent’s context.</p></div><div class=\"card\"><strong>Neighbourhood</strong><p>The unpinned nodes spatially near a pin, surfaced alongside it.</p></div><div class=\"card\"><strong>Reading order</strong><p>Nodes sorted top-left to bottom-right, the way a human scans a board.</p></div></div></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"summary\": \"A pin is a small, durable statement of relevance. The human makes it with a click; the agent reads it as a resource and gets the neighbourhood around it for free.\",\n \"steps\": [\n {\n \"title\": \"The human pins a node\",\n \"detail\": \"Clicking the pin affordance adds the node id to the canvas pin set (capped at 20).\"\n },\n {\n \"title\": \"The server notifies\",\n \"detail\": \"Pin changes emit notifications/resources/updated for canvas://pinned-context.\"\n },\n {\n \"title\": \"The agent reads the resource\",\n \"detail\": \"It receives the pinned nodes plus, for each, the nearby unpinned nodes — the implicit context.\"\n },\n {\n \"title\": \"Spatial neighbours come along\",\n \"detail\": \"Proximity clustering means the human never has to pin everything, only the anchors.\"\n }\n ],\n \"snippets\": [\n {\n \"label\": \"Pin from an agent\",\n \"code\": \"canvas_pin_nodes({ nodeIds: [\\\"node-a\\\", \\\"node-b\\\"], mode: \\\"add\\\" })\",\n \"note\": \"mode defaults to \\\"set\\\", which replaces the whole list.\"\n },\n {\n \"label\": \"Read it back\",\n \"code\": \"resources/read canvas://pinned-context\",\n \"note\": \"Includes the neighbourhood for each pin.\"\n }\n ],\n \"faq\": [\n {\n \"q\": \"Does pinning move or lock the node?\",\n \"a\": \"No. Context pins are separate from arrange-locking; a pinned node still moves.\"\n },\n {\n \"q\": \"What happens when a pinned node is deleted?\",\n \"a\": \"The pin is dropped with it — pins always resolve to live nodes.\"\n },\n {\n \"q\": \"Is there a limit?\",\n \"a\": \"Twenty pins. Past that, curation stops being curation.\"\n }\n ],\n \"glossary\": [\n {\n \"term\": \"Context pin\",\n \"definition\": \"A human-curated marker that puts a node into the agent’s context.\"\n },\n {\n \"term\": \"Neighbourhood\",\n \"definition\": \"The unpinned nodes spatially near a pin, surfaced alongside it.\"\n },\n {\n \"term\": \"Reading order\",\n \"definition\": \"Nodes sorted top-left to bottom-right, the way a human scans a board.\"\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\n</script>\n</body>\n</html>",
|
|
4193
4137
|
"htmlPrimitive": "explainer",
|
|
@@ -4285,7 +4229,6 @@
|
|
|
4285
4229
|
"zIndex": 1,
|
|
4286
4230
|
"collapsed": false,
|
|
4287
4231
|
"pinned": false,
|
|
4288
|
-
"dockPosition": null,
|
|
4289
4232
|
"data": {
|
|
4290
4233
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Blank ext-app tiles on WebKit</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / incident-report</div><h1>Blank ext-app tiles on WebKit</h1><p>Post-incident report with impact metrics, minute-by-minute timeline, root cause, log excerpts, and action checklist.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"grid\"><article class=\"card metric\"><div class=\"small\">Affected hosts</div><strong>Safari + WKWebView</strong><span class=\"badge warn\">warn</span></article><article class=\"card metric\"><div class=\"small\">Boards affected</div><strong>4</strong><span class=\"badge warn\">warn</span></article><article class=\"card metric\"><div class=\"small\">Data loss</div><strong>none</strong><span class=\"badge ok\">ok</span></article></section>\n <section class=\"panel\" style=\"margin-top:14px\"><h2>Executive Summary</h2><p>Several MCP App nodes rendered as black tiles after a cold reload in WebKit hosts. Content was present in the DOM but never composited.</p></section>\n <section class=\"three\" style=\"margin-top:14px\"><div class=\"panel\"><h2>Timeline</h2><div class=\"timeline\"><div class=\"step\"><div class=\"dot\">09:41</div><div class=\"card\"><h3>Report filed <span class=\"badge warn\">warn</span></h3><p>Screenshot shows four black tiles after reload.</p></div></div><div class=\"step\"><div class=\"dot\">10:05</div><div class=\"card\"><h3>Reproduced <span class=\"badge warn\">warn</span></h3><p>Only when several apps hydrate in the same frame.</p></div></div><div class=\"step\"><div class=\"dot\">10:52</div><div class=\"card\"><h3>Root cause found <span class=\"badge info\">info</span></h3><p>Burst hydration overwhelms the WebKit compositor.</p></div></div><div class=\"step\"><div class=\"dot\">12:10</div><div class=\"card\"><h3>Fix landed <span class=\"badge ok\">ok</span></h3><p>Serialized, boot-aware remount queue; one app at a time.</p></div></div><div class=\"step\"><div class=\"dot\">12:53</div><div class=\"card\"><h3>Verified <span class=\"badge ok\">ok</span></h3><p>Ten consecutive cold reloads, no black tiles.</p></div></div></div></div>\n <div class=\"panel\"><h2>Root Cause</h2><p>A fixed-stagger remount was not boot-aware: each recovery remount reboots the app, so staggered remounts overlapped into a fresh burst and the per-node one-shot flag was already spent.</p><pre><code>[extapp] remount nodeId=mcp-app-1 reason=paint-probe-timeout\n[extapp] boot-wait nodeId=mcp-app-1 elapsed=1840ms\n[extapp] settled nodeId=mcp-app-1</code></pre></div>\n <div class=\"panel sticky\"><h2>Actions</h2><label class=\"card\"><h3><input type=\"checkbox\" data-action checked> Serialize remounts behind a boot-aware queue</h3><p class=\"small\">client / 0.4.6</p></label><label class=\"card\"><h3><input type=\"checkbox\" data-action checked> Mirror the recovery trail to the daemon for hosts without devtools</h3><p class=\"small\">client / 0.4.6</p></label><label class=\"card\"><h3><input type=\"checkbox\" data-action > Add a cold-reload burst case to the e2e suite</h3><p class=\"small\">qa / 0.4.8</p></label><button type=\"button\" data-copy-actions>Copy actions</button></div></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"severity\": \"SEV-3\",\n \"status\": \"resolved\",\n \"duration\": \"3h 12m\",\n \"summary\": \"Several MCP App nodes rendered as black tiles after a cold reload in WebKit hosts. Content was present in the DOM but never composited.\",\n \"impact\": [\n {\n \"label\": \"Affected hosts\",\n \"value\": \"Safari + WKWebView\",\n \"tone\": \"warn\"\n },\n {\n \"label\": \"Boards affected\",\n \"value\": \"4\",\n \"tone\": \"warn\"\n },\n {\n \"label\": \"Data loss\",\n \"value\": \"none\",\n \"tone\": \"ok\"\n }\n ],\n \"timeline\": [\n {\n \"time\": \"09:41\",\n \"event\": \"Report filed\",\n \"detail\": \"Screenshot shows four black tiles after reload.\",\n \"tone\": \"warn\"\n },\n {\n \"time\": \"10:05\",\n \"event\": \"Reproduced\",\n \"detail\": \"Only when several apps hydrate in the same frame.\",\n \"tone\": \"warn\"\n },\n {\n \"time\": \"10:52\",\n \"event\": \"Root cause found\",\n \"detail\": \"Burst hydration overwhelms the WebKit compositor.\",\n \"tone\": \"info\"\n },\n {\n \"time\": \"12:10\",\n \"event\": \"Fix landed\",\n \"detail\": \"Serialized, boot-aware remount queue; one app at a time.\",\n \"tone\": \"ok\"\n },\n {\n \"time\": \"12:53\",\n \"event\": \"Verified\",\n \"detail\": \"Ten consecutive cold reloads, no black tiles.\",\n \"tone\": \"ok\"\n }\n ],\n \"rootCause\": \"A fixed-stagger remount was not boot-aware: each recovery remount reboots the app, so staggered remounts overlapped into a fresh burst and the per-node one-shot flag was already spent.\",\n \"logs\": \"[extapp] remount nodeId=mcp-app-1 reason=paint-probe-timeout\\n[extapp] boot-wait nodeId=mcp-app-1 elapsed=1840ms\\n[extapp] settled nodeId=mcp-app-1\",\n \"actions\": [\n {\n \"done\": true,\n \"owner\": \"client\",\n \"description\": \"Serialize remounts behind a boot-aware queue\",\n \"due\": \"0.4.6\"\n },\n {\n \"done\": true,\n \"owner\": \"client\",\n \"description\": \"Mirror the recovery trail to the daemon for hosts without devtools\",\n \"due\": \"0.4.6\"\n },\n {\n \"done\": false,\n \"owner\": \"qa\",\n \"description\": \"Add a cold-reload burst case to the e2e suite\",\n \"due\": \"0.4.8\"\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nfunction actionState() {\n return Array.from(document.querySelectorAll('[data-action]')).map((input, index) => ({ ...(PMX_DATA.actions || [])[index], done: input.checked }));\n}\nwindow.__pmxGetCopyJson = () => ({ ...PMX_DATA, actions: actionState() });\ndocument.querySelector('[data-copy-actions]')?.addEventListener('click', () => copyText(actionState().map((action) => '- [' + (action.done ? 'x' : ' ') + '] ' + (action.description || 'Action') + ' (' + (action.owner || 'owner') + ', ' + (action.due || 'due') + ')').join('\\n')));\n</script>\n</body>\n</html>",
|
|
4291
4234
|
"htmlPrimitive": "incident-report",
|
|
@@ -4398,7 +4341,6 @@
|
|
|
4398
4341
|
"zIndex": 1,
|
|
4399
4342
|
"collapsed": false,
|
|
4400
4343
|
"pinned": false,
|
|
4401
|
-
"dockPosition": null,
|
|
4402
4344
|
"data": {
|
|
4403
4345
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Weekly canvas status</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / status-report</div><h1>Weekly canvas status</h1><p>Skimmable report with metrics, shipped/slipped lists, blockers, and next actions.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"grid\"><article class=\"card\"><div class=\"small\">Gates</div><h2>4/5 green</h2><span class=\"badge warn\">warn</span></article><article class=\"card\"><div class=\"small\">Unit tests</div><h2>612 passing</h2><span class=\"badge ok\">ok</span></article><article class=\"card\"><div class=\"small\">Coverage</div><h2>71% lines</h2><span class=\"badge ok\">ok</span></article><article class=\"card\"><div class=\"small\">Open bugs</div><h2>7</h2><span class=\"badge warn\">warn</span></article><article class=\"card\"><div class=\"small\">Bundle</div><h2>+4.1%</h2><span class=\"badge warn\">warn</span></article></section>\n <section class=\"grid\" style=\"margin-top:14px\"><article class=\"card\"><h2>Shipped</h2><ul><li>Delimited files render as tables instead of raw text</li><li>Shared readability floor between server and client</li><li>Generated demo board covering every public surface</li></ul></article><article class=\"card\"><h2>Slipped</h2><ul><li>Per-band deep links</li><li>Snapshot diff in the browser</li></ul></article><article class=\"card\"><h2>Risks</h2><ul><li>Bundle growth is trending up three weeks running</li><li>e2e suite is the slowest gate and the flakiest</li></ul></article><article class=\"card\"><h2>Next</h2><ul><li>Cut the bundle back under the 0.4.5 baseline</li><li>Record the 60-second tour off the new demo board</li></ul></article></section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"metrics\": [\n {\n \"label\": \"Gates\",\n \"value\": \"4/5 green\",\n \"tone\": \"warn\"\n },\n {\n \"label\": \"Unit tests\",\n \"value\": \"612 passing\",\n \"tone\": \"ok\"\n },\n {\n \"label\": \"Coverage\",\n \"value\": \"71% lines\",\n \"tone\": \"ok\"\n },\n {\n \"label\": \"Open bugs\",\n \"value\": \"7\",\n \"tone\": \"warn\"\n },\n {\n \"label\": \"Bundle\",\n \"value\": \"+4.1%\",\n \"tone\": \"warn\"\n }\n ],\n \"shipped\": [\n \"Delimited files render as tables instead of raw text\",\n \"Shared readability floor between server and client\",\n \"Generated demo board covering every public surface\"\n ],\n \"slipped\": [\n \"Per-band deep links\",\n \"Snapshot diff in the browser\"\n ],\n \"risks\": [\n \"Bundle growth is trending up three weeks running\",\n \"e2e suite is the slowest gate and the flakiest\"\n ],\n \"next\": [\n \"Cut the bundle back under the 0.4.5 baseline\",\n \"Record the 60-second tour off the new demo board\"\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\n</script>\n</body>\n</html>",
|
|
4404
4346
|
"htmlPrimitive": "status-report",
|
|
@@ -4476,7 +4418,6 @@
|
|
|
4476
4418
|
"zIndex": 1,
|
|
4477
4419
|
"collapsed": false,
|
|
4478
4420
|
"pinned": false,
|
|
4479
|
-
"dockPosition": null,
|
|
4480
4421
|
"data": {
|
|
4481
4422
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Post-0.4.7 triage</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / triage-board</div><h1>Post-0.4.7 triage</h1><p>Draggable Now/Next/Later/Cut board with copy-as-markdown export.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"columns\"><div class=\"column\" data-column=\"Now\"><h2>Now</h2><article class=\"card ticket\" draggable=\"true\" data-ticket=\"0\"><h3>Bundle back under baseline</h3><p>Client bundle is +4.1% over 0.4.6.</p><p class=\"small\">Trending the wrong way three weeks running.</p></article><article class=\"card ticket\" draggable=\"true\" data-ticket=\"1\"><h3>Stabilise the e2e gate</h3><p>Two reruns per week on average.</p><p class=\"small\">Slowest and flakiest gate blocks every release.</p></article></div><div class=\"column\" data-column=\"Next\"><h2>Next</h2><article class=\"card ticket\" draggable=\"true\" data-ticket=\"0\"><h3>Per-band deep links</h3><p>Open the board focused on one band.</p><p class=\"small\">Makes the demo board usable as documentation.</p></article><article class=\"card ticket\" draggable=\"true\" data-ticket=\"1\"><h3>60-second tour recording</h3><p>Screen capture driven off --demo.</p><p class=\"small\">Cheapest onboarding win available.</p></article></div><div class=\"column\" data-column=\"Later\"><h2>Later</h2><article class=\"card ticket\" draggable=\"true\" data-ticket=\"0\"><h3>Snapshot diff in the browser</h3><p>Today it is MCP/HTTP only.</p><p class=\"small\">Useful, not blocking.</p></article></div><div class=\"column\" data-column=\"Cut\"><h2>Cut</h2><article class=\"card ticket\" draggable=\"true\" data-ticket=\"0\"><h3>Theme editor UI</h3><p>Edit CSS variables from the toolbar.</p><p class=\"small\">Nine built-in themes already cover the need.</p></article></div></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>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"columns\": [\n \"Now\",\n \"Next\",\n \"Later\",\n \"Cut\"\n ],\n \"items\": [\n {\n \"title\": \"Bundle back under baseline\",\n \"detail\": \"Client bundle is +4.1% over 0.4.6.\",\n \"column\": \"Now\",\n \"rationale\": \"Trending the wrong way three weeks running.\"\n },\n {\n \"title\": \"Stabilise the e2e gate\",\n \"detail\": \"Two reruns per week on average.\",\n \"column\": \"Now\",\n \"rationale\": \"Slowest and flakiest gate blocks every release.\"\n },\n {\n \"title\": \"Per-band deep links\",\n \"detail\": \"Open the board focused on one band.\",\n \"column\": \"Next\",\n \"rationale\": \"Makes the demo board usable as documentation.\"\n },\n {\n \"title\": \"60-second tour recording\",\n \"detail\": \"Screen capture driven off --demo.\",\n \"column\": \"Next\",\n \"rationale\": \"Cheapest onboarding win available.\"\n },\n {\n \"title\": \"Snapshot diff in the browser\",\n \"detail\": \"Today it is MCP/HTTP only.\",\n \"column\": \"Later\",\n \"rationale\": \"Useful, not blocking.\"\n },\n {\n \"title\": \"Theme editor UI\",\n \"detail\": \"Edit CSS variables from the toolbar.\",\n \"column\": \"Cut\",\n \"rationale\": \"Nine built-in themes already cover the need.\"\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nlet dragged = null;\nfunction boardState() {\n return Array.from(document.querySelectorAll('.column')).map((column) => ({\n column: column.getAttribute('data-column') || 'Column',\n items: Array.from(column.querySelectorAll('.ticket')).map((ticket) => ({\n title: ticket.querySelector('h3')?.textContent || '',\n detail: ticket.querySelector('p')?.textContent || '',\n })),\n }));\n}\ndocument.querySelectorAll('.ticket').forEach((ticket) => {\n ticket.addEventListener('dragstart', () => { dragged = ticket; });\n});\ndocument.querySelectorAll('.column').forEach((column) => {\n column.addEventListener('dragover', (event) => event.preventDefault());\n column.addEventListener('drop', () => { if (dragged) column.appendChild(dragged); });\n});\nfunction boardMarkdown() {\n return boardState().map((column) => {\n const items = column.items.map((item) => '- ' + item.title + ': ' + item.detail);\n return '## ' + column.column + '\\n' + (items.join('\\n') || '- None');\n }).join('\\n\\n');\n}\nwindow.__pmxGetCopyJson = () => ({ ...PMX_DATA, board: boardState() });\ndocument.querySelector('[data-copy-markdown]')?.addEventListener('click', () => copyText(boardMarkdown()));\n</script>\n</body>\n</html>",
|
|
4482
4423
|
"htmlPrimitive": "triage-board",
|
|
@@ -4554,7 +4495,6 @@
|
|
|
4554
4495
|
"zIndex": 1,
|
|
4555
4496
|
"collapsed": false,
|
|
4556
4497
|
"pinned": false,
|
|
4557
|
-
"dockPosition": null,
|
|
4558
4498
|
"data": {
|
|
4559
4499
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Canvas feature flags</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / config-editor</div><h1>Canvas feature flags</h1><p>Form-like editor for flags or structured config with dependency warnings and copy-diff export.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"grid\"><label class=\"card\"><div class=\"small\">Demo</div><h3><input type=\"checkbox\" data-flag=\"0\" checked> strictSize on demo nodes</h3><p>Keeps the generated band layout exactly as authored.</p><p class=\"small\">Key: strictSizeDemo</p><p class=\"small\" data-warning=\"0\"></p></label><label class=\"card\"><div class=\"small\">Layout</div><h3><input type=\"checkbox\" data-flag=\"1\" checked> Iframe content-fit</h3><p>Iframe surfaces grow to their reported content height.</p><p class=\"small\">Key: contentFit</p><p class=\"small\" data-warning=\"1\"></p></label><label class=\"card\"><div class=\"small\">Graph</div><h3><input type=\"checkbox\" data-flag=\"2\" checked> Auto dependency edges</h3><p>Derives depends-on edges between file nodes.</p><p class=\"small\">Key: codeGraph</p><p class=\"small\" data-warning=\"2\"></p></label><label class=\"card\"><div class=\"small\">AX</div><h3><input type=\"checkbox\" data-flag=\"3\" > Node AX interactions</h3><p>Lets eligible node surfaces emit AX operations.</p><p class=\"small\">Key: axInteractions / requires: axCapabilities</p><p class=\"small\" data-warning=\"3\"></p></label><label class=\"card\"><div class=\"small\">AX</div><h3><input type=\"checkbox\" data-flag=\"4\" > AX capability ceiling</h3><p>Per-node-type ceiling a node can narrow but never escalate.</p><p class=\"small\">Key: axCapabilities</p><p class=\"small\" data-warning=\"4\"></p></label><label class=\"card\"><div class=\"small\">Tooling</div><h3><input type=\"checkbox\" data-flag=\"5\" > Bun.WebView automation</h3><p>Headless webview driving for CI screenshots.</p><p class=\"small\">Key: webviewAutomation</p><p class=\"small\" data-warning=\"5\"></p></label></section><button type=\"button\" data-copy-diff style=\"margin-top:12px\">Copy diff</button>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"flags\": [\n {\n \"key\": \"strictSizeDemo\",\n \"label\": \"strictSize on demo nodes\",\n \"area\": \"Demo\",\n \"enabled\": true,\n \"description\": \"Keeps the generated band layout exactly as authored.\"\n },\n {\n \"key\": \"contentFit\",\n \"label\": \"Iframe content-fit\",\n \"area\": \"Layout\",\n \"enabled\": true,\n \"description\": \"Iframe surfaces grow to their reported content height.\"\n },\n {\n \"key\": \"codeGraph\",\n \"label\": \"Auto dependency edges\",\n \"area\": \"Graph\",\n \"enabled\": true,\n \"description\": \"Derives depends-on edges between file nodes.\"\n },\n {\n \"key\": \"axInteractions\",\n \"label\": \"Node AX interactions\",\n \"area\": \"AX\",\n \"enabled\": false,\n \"requires\": [\n \"axCapabilities\"\n ],\n \"description\": \"Lets eligible node surfaces emit AX operations.\"\n },\n {\n \"key\": \"axCapabilities\",\n \"label\": \"AX capability ceiling\",\n \"area\": \"AX\",\n \"enabled\": false,\n \"description\": \"Per-node-type ceiling a node can narrow but never escalate.\"\n },\n {\n \"key\": \"webviewAutomation\",\n \"label\": \"Bun.WebView automation\",\n \"area\": \"Tooling\",\n \"enabled\": false,\n \"description\": \"Headless webview driving for CI screenshots.\"\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nconst originalFlags = PMX_DATA.flags || [];\nfunction flagState() {\n return originalFlags.map((flag, index) => ({ ...flag, enabled: document.querySelector('[data-flag=\"' + index + '\"]').checked }));\n}\nfunction refreshWarnings() {\n const state = flagState();\n const byKey = new Map(state.map((flag) => [flag.key, flag]));\n state.forEach((flag, index) => {\n const missing = (flag.requires || []).filter((key) => flag.enabled && !byKey.get(key)?.enabled);\n document.querySelector('[data-warning=\"' + index + '\"]').textContent = missing.length ? 'Warning: requires ' + missing.join(', ') : '';\n });\n}\ndocument.querySelectorAll('[data-flag]').forEach((input) => input.addEventListener('change', refreshWarnings));\nwindow.__pmxGetCopyJson = () => ({ ...PMX_DATA, flags: flagState() });\ndocument.querySelector('[data-copy-diff]')?.addEventListener('click', () => {\n const diff = flagState().filter((flag, index) => flag.enabled !== originalFlags[index]?.enabled).map((flag) => ({ key: flag.key, enabled: flag.enabled }));\n copyText(JSON.stringify(diff, null, 2));\n});\nrefreshWarnings();\n</script>\n</body>\n</html>",
|
|
4560
4500
|
"htmlPrimitive": "config-editor",
|
|
@@ -4638,7 +4578,6 @@
|
|
|
4638
4578
|
"zIndex": 1,
|
|
4639
4579
|
"collapsed": false,
|
|
4640
4580
|
"pinned": false,
|
|
4641
|
-
"dockPosition": null,
|
|
4642
4581
|
"data": {
|
|
4643
4582
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Board summary prompt</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / prompt-tuner</div><h1>Board summary prompt</h1><p>Side-by-side prompt/template editor with live variable previews and copy export.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <section class=\"two\"><div class=\"panel\"><h2>Template</h2><textarea id=\"template\">You are looking at a PMX Canvas board for {{project}}.\n\nSummarise it for a {{audience}} in {{sentences}} sentences. Lead with what the board is FOR, then what changed most recently. Ignore layout and node ids.\n\nPinned nodes are the human’s curation — weight them above everything else.</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>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"template\": \"You are looking at a PMX Canvas board for {{project}}.\\n\\nSummarise it for a {{audience}} in {{sentences}} sentences. Lead with what the board is FOR, then what changed most recently. Ignore layout and node ids.\\n\\nPinned nodes are the human’s curation — weight them above everything else.\",\n \"samples\": [\n {\n \"name\": \"Standup\",\n \"variables\": {\n \"project\": \"pmx-canvas\",\n \"audience\": \"teammate joining the standup\",\n \"sentences\": \"two\"\n }\n },\n {\n \"name\": \"Exec review\",\n \"variables\": {\n \"project\": \"the 0.4.7 release\",\n \"audience\": \"director who has not seen the tool\",\n \"sentences\": \"three\"\n }\n },\n {\n \"name\": \"Handoff\",\n \"variables\": {\n \"project\": \"the demo board rebuild\",\n \"audience\": \"agent picking the work up cold\",\n \"sentences\": \"four\"\n }\n }\n ]\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nconst templateEl = document.getElementById('template');\nconst previewsEl = document.getElementById('previews');\nconst samples = PMX_DATA.samples || [];\nfunction fill(template, vars) {\n return template.replace(/{{\\s*([\\w.-]+)\\s*}}/g, (_, key) => vars?.[key] ?? '{{' + key + '}}');\n}\nfunction renderPreviews() {\n const value = templateEl.value;\n document.getElementById('char-count').textContent = String(value.length);\n previewsEl.innerHTML = samples.map((sample) => '<div class=\"card\"><h3>' + (sample.name || 'Sample') + '</h3><div class=\"preview\">' + fill(value, sample.variables || {}).replace(/[&<>]/g, (c) => ({'&':'&','<':'<','>':'>'}[c])) + '</div></div>').join('');\n}\ntemplateEl.addEventListener('input', renderPreviews);\nwindow.__pmxGetCopyJson = () => ({ ...PMX_DATA, template: templateEl.value });\ndocument.querySelector('[data-copy-template]')?.addEventListener('click', () => copyText(templateEl.value));\nrenderPreviews();\n</script>\n</body>\n</html>",
|
|
4644
4583
|
"htmlPrimitive": "prompt-tuner",
|
|
@@ -4699,7 +4638,6 @@
|
|
|
4699
4638
|
"zIndex": 1,
|
|
4700
4639
|
"collapsed": false,
|
|
4701
4640
|
"pinned": false,
|
|
4702
|
-
"dockPosition": null,
|
|
4703
4641
|
"data": {
|
|
4704
4642
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Agent board</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / ax-board</div><h1>Agent board</h1><p>Live agent work board: hand the agent tasks, watch status, steer, and run bounded task loops.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <style>\n .ax-field { display: grid; gap: 6px; margin-bottom: 10px; }\n .ax-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n .ax-work-row { margin-bottom: 10px; padding: 12px 14px; }\n .ax-work-head { display: flex; gap: 10px; align-items: baseline; justify-content: space-between; }\n .ax-work-head h3 { margin: 0; }\n .ax-work-foot { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-top: 8px; }\n select { border: 1px solid var(--color-border, #263241); border-radius: 999px; padding: 6px 10px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n input[type=\"number\"] { width: 72px; border: 1px solid var(--color-border, #263241); border-radius: 999px; padding: 6px 10px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n #ax-loop-panel[hidden] { display: none; }\n #ax-offline { border-color: var(--color-danger, #ef4444); margin-bottom: 14px; }\n</style>\n<section class=\"panel\" id=\"ax-offline\" hidden><h2>AX bridge unavailable</h2><p class=\"small\">This board is not running as an AX-enabled canvas node, so its controls are inert.</p></section>\n<section class=\"two\">\n <div class=\"panel\">\n <h2>Give the agent a task</h2>\n <p class=\"small\">Tasks queued here land in the canvas AX work queue, so the agent picks them up without another prompt.</p>\n <div class=\"ax-field\"><input type=\"text\" id=\"ax-task-title\" placeholder=\"What should the agent do?\"></div>\n <div class=\"ax-field\"><textarea id=\"ax-task-detail\" placeholder=\"Optional detail\" style=\"min-height:64px\"></textarea></div>\n <div class=\"ax-row\"><button type=\"button\" id=\"ax-add-task\">Add task</button><span class=\"small\" id=\"ax-task-status\"></span></div>\n <div class=\"ax-row\" style=\"margin-top:10px\">\n <span class=\"small\">or run it as a bounded loop:</span>\n <label class=\"small\">max runs <input type=\"number\" id=\"ax-loop-runs\" min=\"1\" max=\"20\" step=\"1\" value=\"3\"></label>\n <button type=\"button\" id=\"ax-start-loop\">Start loop</button>\n </div>\n </div>\n <div class=\"panel\">\n <h2>Steer</h2>\n <div class=\"ax-field\"><textarea id=\"ax-steer-message\" placeholder=\"Prioritize the auth refactor\" style=\"min-height:92px\"></textarea></div>\n <div class=\"ax-row\"><button type=\"button\" id=\"ax-send-steer\">Queue steering message</button></div>\n <p class=\"small\">Queued for the agent's next turn — steering is recorded on the timeline, it does not wake a running agent.</p>\n <p class=\"small\" id=\"ax-steer-status\"></p>\n </div>\n</section>\n<section class=\"panel\" id=\"ax-loop-panel\" hidden style=\"margin-top:14px\">\n <div class=\"ax-work-head\"><h2 id=\"ax-loop-title\">Loop</h2><span class=\"badge info\" id=\"ax-loop-run\">run 0 of 0</span></div>\n <p class=\"small\">Each run is its own work item. When you mark the current run <strong>done</strong>, the surface opens the next run and steers again — until the cap or Stop.</p>\n <div class=\"ax-row\"><button type=\"button\" id=\"ax-stop-loop\">Stop loop</button><span class=\"small\" id=\"ax-loop-status\"></span></div>\n <p class=\"small\">Loop control lives in this panel, so reloading it stops the loop — the work items stay as the durable record.</p>\n</section>\n<section class=\"panel\" style=\"margin-top:14px\">\n <div class=\"ax-work-head\"><h2>Work</h2><span class=\"small\" id=\"ax-work-counts\"></span></div>\n <div id=\"ax-work-list\"></div>\n <p class=\"small\" id=\"ax-work-status\"></p>\n</section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"note\": \"Tasks queued here land in the canvas AX work queue, so the agent picks them up without another prompt.\",\n \"defaultRuns\": 3\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nconst AX_STATUSES = ['todo', 'in-progress', 'blocked', 'done', 'cancelled'];\nconst AX_MAX_RUNS = 20;\n\nconst el = (id) => document.getElementById(id);\nconst axFlashTimers = {};\nfunction flash(id, message) {\n const target = el(id);\n if (!target) return;\n target.textContent = message;\n if (axFlashTimers[id]) clearTimeout(axFlashTimers[id]);\n axFlashTimers[id] = setTimeout(() => { target.textContent = ''; }, 4000);\n}\nfunction axWorkItems() {\n const state = window.PMX_AX && window.PMX_AX.state;\n return state && Array.isArray(state.workItems) ? state.workItems : [];\n}\nasync function axEmit(type, payload) {\n if (!window.PMX_AX || typeof window.PMX_AX.emit !== 'function') {\n return { ok: false, code: 'ax-bridge-missing', error: 'AX bridge is not available on this surface.' };\n }\n return await window.PMX_AX.emit(type, payload);\n}\nfunction axFailure(result) {\n return (result && (result.error || result.code)) || 'unknown error';\n}\nfunction statusTone(status) {\n if (status === 'done') return 'ok';\n if (status === 'blocked' || status === 'cancelled') return 'danger';\n if (status === 'in-progress') return 'warn';\n return 'info';\n}\nfunction elem(tag, className, textContent) {\n const node = document.createElement(tag);\n if (className) node.className = className;\n if (textContent !== undefined) node.textContent = textContent;\n return node;\n}\nasync function sendSteer() {\n const message = el('ax-steer-message').value.trim();\n if (!message) { flash('ax-steer-status', 'Write a steering message first.'); return; }\n const result = await axEmit('ax.steer', { message });\n if (result && result.ok) {\n el('ax-steer-message').value = '';\n flash('ax-steer-status', 'Queued for the agent\\'s next turn ✓');\n } else {\n flash('ax-steer-status', 'Failed: ' + axFailure(result));\n }\n}\n\n// Loop control is in-memory only (the surface is sandboxed: storage APIs throw).\nlet axLoop = null;\nlet axAdvancing = false;\n\nfunction renderLoop() {\n const panel = el('ax-loop-panel');\n panel.hidden = !axLoop;\n if (!axLoop) return;\n el('ax-loop-title').textContent = axLoop.baseTitle;\n el('ax-loop-run').textContent = 'run ' + axLoop.run + ' of ' + axLoop.maxRuns;\n}\nasync function openRun(session, run) {\n const result = await axEmit('ax.work.create', {\n title: session.baseTitle + ' — run ' + run + '/' + session.maxRuns,\n ...(session.detail ? { detail: session.detail } : {}),\n });\n // The human may have stopped the loop while this was in flight.\n if (axLoop !== session) return false;\n if (!result || !result.ok || !result.primitive || !result.primitive.id) {\n flash('ax-loop-status', 'Could not open run ' + run + ': ' + axFailure(result));\n return false;\n }\n session.run = run;\n session.workItemId = result.primitive.id;\n renderLoop();\n axRenderSurface();\n await axEmit('ax.steer', {\n message:\n 'Loop run ' + run + ' of ' + session.maxRuns + ' for \"' + session.baseTitle +\n '\". Mark work item ' + session.workItemId + ' done when this run is complete.',\n });\n return true;\n}\n/**\n * Start a bounded loop. NEVER auto-started — a human control only. Returns the\n * clamped run count on success (0 on failure) so the surface can sync its inputs;\n * `onStarted` fires the moment run 1 is open, before the loop's own bookkeeping.\n */\nasync function axStartLoop(baseTitle, detail, requested, onStarted) {\n if (axLoop) return 0;\n const maxRuns = Math.min(AX_MAX_RUNS, Math.max(1, Math.round(Number.isFinite(requested) ? requested : 3)));\n const session = { baseTitle: baseTitle, detail: detail, maxRuns: maxRuns, run: 0, workItemId: null };\n axLoop = session;\n renderLoop();\n if (!(await openRun(session, 1))) {\n if (axLoop === session) { axLoop = null; renderLoop(); }\n return 0;\n }\n if (onStarted) onStarted(maxRuns);\n flash('ax-loop-status', requested > AX_MAX_RUNS ? 'Capped at ' + AX_MAX_RUNS + ' runs.' : 'Loop started.');\n await axEmit('ax.event.record', {\n kind: 'steering',\n summary: 'Loop started: ' + baseTitle + ' (max ' + maxRuns + ' runs)',\n });\n return maxRuns;\n}\nasync function stopLoop() {\n const session = axLoop;\n if (!session) return;\n axLoop = null;\n renderLoop();\n axRenderSurface();\n flash('ax-loop-status', 'Loop stopped at run ' + session.run + ' of ' + session.maxRuns + '.');\n await axEmit('ax.event.record', {\n kind: 'steering',\n summary: 'Loop stopped by human: ' + session.baseTitle + ' at run ' + session.run + ' of ' + session.maxRuns,\n });\n}\n/**\n * Advance when the CURRENT run's work item reaches done, observed from live AX\n * state. Double-advance guards: an in-flight flag, and clearing workItemId before\n * awaiting so a repeated state push cannot match the same finished run twice.\n */\nasync function maybeAdvanceLoop() {\n const session = axLoop;\n if (!session || axAdvancing || !session.workItemId) return;\n const current = axWorkItems().find((item) => item.id === session.workItemId);\n if (!current || current.status !== 'done') return;\n if (session.run >= session.maxRuns) {\n axLoop = null;\n renderLoop();\n axRenderSurface();\n flash('ax-loop-status', 'Loop finished — ' + session.maxRuns + ' of ' + session.maxRuns + ' runs done.');\n await axEmit('ax.event.record', {\n kind: 'steering',\n summary: 'Loop complete: ' + session.baseTitle + ' (' + session.maxRuns + ' runs)',\n });\n return;\n }\n axAdvancing = true;\n session.workItemId = null;\n try {\n if (!(await openRun(session, session.run + 1)) && axLoop === session) { axLoop = null; renderLoop(); }\n } finally {\n axAdvancing = false;\n }\n}\n\nfunction applyAxState() {\n axRenderSurface();\n renderLoop();\n void maybeAdvanceLoop();\n}\n\n// ── render (live AX state only — no local mirror of the work list) ──\nfunction renderWork() {\n const items = axWorkItems();\n const list = el('ax-work-list');\n list.textContent = '';\n el('ax-work-counts').textContent = items.length\n ? AX_STATUSES.map((s) => s + ' ' + items.filter((item) => item.status === s).length).join(' · ')\n : '';\n if (items.length === 0) {\n list.appendChild(elem('p', 'muted', 'No work items yet — add a task above and it shows up here.'));\n return;\n }\n for (const item of items) {\n const row = elem('article', 'card ax-work-row');\n row.setAttribute('data-work-id', item.id);\n const head = elem('div', 'ax-work-head');\n head.appendChild(elem('h3', '', item.title));\n head.appendChild(elem('span', 'badge ' + statusTone(item.status), item.status));\n row.appendChild(head);\n if (item.detail) row.appendChild(elem('p', 'small', item.detail));\n const foot = elem('div', 'ax-work-foot');\n const label = elem('label', 'small', 'Status ');\n const select = document.createElement('select');\n select.setAttribute('data-status-for', item.id);\n for (const status of AX_STATUSES) {\n const option = document.createElement('option');\n option.value = status;\n option.textContent = status;\n if (status === item.status) option.selected = true;\n select.appendChild(option);\n }\n select.addEventListener('change', () => setWorkStatus(item.id, select.value));\n label.appendChild(select);\n foot.appendChild(label);\n if (item.agentId) foot.appendChild(elem('span', 'small', 'agent: ' + item.agentId));\n if (axLoop && axLoop.workItemId === item.id) foot.appendChild(elem('span', 'badge info', 'current run'));\n row.appendChild(foot);\n list.appendChild(row);\n }\n}\nfunction axRenderSurface() { renderWork(); }\n\n// ── actions ──\nasync function addTask() {\n const title = el('ax-task-title').value.trim();\n if (!title) { flash('ax-task-status', 'Give the task a title first.'); return; }\n const detail = el('ax-task-detail').value.trim();\n const result = await axEmit('ax.work.create', detail ? { title, detail } : { title });\n if (result && result.ok) {\n el('ax-task-title').value = '';\n el('ax-task-detail').value = '';\n flash('ax-task-status', 'Task queued ✓');\n } else {\n flash('ax-task-status', 'Failed: ' + axFailure(result));\n }\n}\nasync function setWorkStatus(id, status) {\n const result = await axEmit('ax.work.update', { id, status });\n flash('ax-work-status', result && result.ok ? 'Status updated ✓' : 'Update failed: ' + axFailure(result));\n}\nasync function startLoop() {\n if (axLoop) { flash('ax-task-status', 'A loop is already running — stop it first.'); return; }\n const title = el('ax-task-title').value.trim();\n if (!title) { flash('ax-task-status', 'Give the task a title first.'); return; }\n await axStartLoop(title, el('ax-task-detail').value.trim(), Number(el('ax-loop-runs').value), (maxRuns) => {\n el('ax-task-title').value = '';\n el('ax-task-detail').value = '';\n el('ax-loop-runs').value = String(maxRuns);\n });\n}\n\nel('ax-add-task').addEventListener('click', addTask);\nel('ax-start-loop').addEventListener('click', startLoop);\nel('ax-stop-loop').addEventListener('click', stopLoop);\nel('ax-send-steer').addEventListener('click', sendSteer);\nwindow.__pmxGetCopyJson = () => ({\n ...PMX_DATA,\n workItems: axWorkItems(),\n loop: axLoop ? { title: axLoop.baseTitle, run: axLoop.run, maxRuns: axLoop.maxRuns } : null,\n});\nif (!window.PMX_AX || typeof window.PMX_AX.emit !== 'function') el('ax-offline').hidden = false;\n// Seeded snapshot first, then every live push. A loop NEVER auto-starts on load.\nwindow.addEventListener('pmx-ax-update', applyAxState);\napplyAxState();\n</script>\n</body>\n</html>",
|
|
4705
4643
|
"htmlPrimitive": "ax-board",
|
|
@@ -4744,7 +4682,6 @@
|
|
|
4744
4682
|
"zIndex": 1,
|
|
4745
4683
|
"collapsed": false,
|
|
4746
4684
|
"pinned": false,
|
|
4747
|
-
"dockPosition": null,
|
|
4748
4685
|
"data": {
|
|
4749
4686
|
"html": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Release flow</title>\n<style>\n :root { color-scheme: dark light; }\n * { box-sizing: border-box; }\n body { margin: 0; padding: 22px; background: radial-gradient(circle at top left, color-mix(in srgb, var(--color-accent, #4a9eff) 18%, transparent), transparent 34rem), var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }\n .shell { max-width: 1160px; margin: 0 auto; }\n header.hero { display: grid; grid-template-columns: 1fr auto; gap: 16px; align-items: start; margin-bottom: 20px; }\n .kicker { color: var(--color-accent, #4a9eff); text-transform: uppercase; letter-spacing: .16em; font-size: 11px; font-weight: 800; }\n h1 { margin: 5px 0 8px; font-size: clamp(28px, 4vw, 46px); line-height: .95; letter-spacing: -.04em; }\n h2 { margin: 0 0 12px; font-size: 19px; letter-spacing: -.02em; }\n h3 { margin: 0 0 8px; font-size: 15px; }\n p { color: var(--color-text-secondary, #aeb8c2); margin: 0 0 12px; }\n .actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }\n button { border: 1px solid var(--color-border, #263241); background: var(--color-panel, #111821); color: var(--color-text, #e8edf2); border-radius: 999px; padding: 8px 12px; font: inherit; cursor: pointer; }\n button:hover { border-color: var(--color-accent, #4a9eff); transform: translateY(-1px); }\n table { width: 100%; border-collapse: collapse; color: var(--color-text-secondary, #aeb8c2); font-size: 13px; }\n th, td { text-align: left; border-bottom: 1px solid var(--color-border, #263241); padding: 9px 8px; vertical-align: top; }\n th { color: var(--color-text, #e8edf2); font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(235px, 1fr)); gap: 14px; }\n .two { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }\n .three { display: grid; grid-template-columns: 1.1fr 1fr .9fr; gap: 14px; align-items: start; }\n .card, details, .panel { border: 1px solid var(--color-border, #263241); background: color-mix(in srgb, var(--color-panel, #111821) 88%, transparent); border-radius: 18px; padding: 16px; box-shadow: 0 18px 50px rgba(0, 0, 0, .18); }\n .card.emphasis { border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 55%, var(--color-border, #263241)); }\n .sticky { position: sticky; top: 14px; }\n .metric { min-height: 104px; display: grid; align-content: space-between; }\n .metric strong { font-size: clamp(24px, 4vw, 42px); letter-spacing: -.04em; }\n .muted { color: var(--color-text-secondary, #aeb8c2); }\n .small { font-size: 12px; color: var(--color-text-muted, #7e8a97); }\n ul { padding-left: 18px; margin: 8px 0 0; color: var(--color-text-secondary, #aeb8c2); }\n li + li { margin-top: 5px; }\n pre { white-space: pre-wrap; overflow: auto; margin: 10px 0 0; padding: 12px; border-radius: 12px; background: #05070a; border: 1px solid color-mix(in srgb, var(--color-border, #263241) 75%, black); color: #d7eadb; font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px; line-height: 1.45; }\n .badge { display: inline-flex; align-items: center; border-radius: 999px; padding: 3px 8px; font-size: 11px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }\n .badge.ok { color: #062b18; background: var(--color-success, #22c55e); }\n .badge.warn { color: #2b1a02; background: var(--color-warning, #eab308); }\n .badge.danger { color: white; background: var(--color-danger, #ef4444); }\n .badge.info { color: #051a33; background: var(--color-accent, #4a9eff); }\n .timeline { position: relative; display: grid; gap: 12px; }\n .timeline .step { display: grid; grid-template-columns: 34px 1fr; gap: 12px; align-items: start; }\n .dot { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; background: var(--color-accent, #4a9eff); color: #06111f; font-weight: 900; }\n .flow { display: flex; gap: 10px; align-items: stretch; overflow-x: auto; padding-bottom: 4px; }\n .flow-node { min-width: 180px; border-radius: 16px; border: 1px solid var(--color-border, #263241); padding: 14px; background: var(--color-surface, #17202b); }\n .arrow { align-self: center; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .swatches { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; }\n .swatch { width: 44px; height: 44px; border-radius: 13px; border: 1px solid rgba(255,255,255,.22); }\n textarea, input[type=\"text\"] { width: 100%; border: 1px solid var(--color-border, #263241); border-radius: 14px; padding: 12px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n textarea { min-height: 220px; resize: vertical; font-family: var(--font-mono, ui-monospace, monospace); }\n .columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 12px; align-items: start; }\n .column { min-height: 220px; border: 1px dashed var(--color-border, #263241); border-radius: 18px; padding: 12px; background: color-mix(in srgb, var(--color-panel, #111821) 70%, transparent); }\n .ticket { cursor: grab; margin: 10px 0; }\n .ticket:active { cursor: grabbing; }\n .slide { min-height: 390px; display: none; align-content: center; }\n .slide.active { display: grid; }\n .slide h2 { font-size: clamp(30px, 5vw, 58px); line-height: .95; }\n .preview { white-space: pre-wrap; min-height: 220px; }\n .figure svg { width: 100%; min-height: 220px; border-radius: 14px; background: color-mix(in srgb, var(--color-bg, #0b0f14) 86%, white); border: 1px solid var(--color-border, #263241); }\n @media (max-width: 760px) { body { padding: 14px; } header.hero, .two, .three { grid-template-columns: 1fr; } .actions { justify-content: flex-start; } .sticky { position: static; } }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n <header class=\"hero\">\n <div><div class=\"kicker\">PMX HTML primitive / ax-flow</div><h1>Release flow</h1><p>Live task-flow control panel: a step diagram with an explicit loop-back, per-step status, and a one-click materialize onto the board.</p></div>\n <div class=\"actions\"><button type=\"button\" data-copy-json>Copy JSON</button><button type=\"button\" data-copy-prompt>Copy prompt</button></div>\n </header>\n <style>\n .ax-field { display: grid; gap: 6px; margin-bottom: 10px; }\n .ax-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n .ax-work-row { margin-bottom: 10px; padding: 12px 14px; }\n .ax-work-head { display: flex; gap: 10px; align-items: baseline; justify-content: space-between; }\n .ax-work-head h3 { margin: 0; }\n .ax-work-foot { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-top: 8px; }\n select { border: 1px solid var(--color-border, #263241); border-radius: 999px; padding: 6px 10px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n input[type=\"number\"] { width: 72px; border: 1px solid var(--color-border, #263241); border-radius: 999px; padding: 6px 10px; background: var(--color-bg, #0b0f14); color: var(--color-text, #e8edf2); font: inherit; }\n #ax-loop-panel[hidden] { display: none; }\n #ax-offline { border-color: var(--color-danger, #ef4444); margin-bottom: 14px; }\n</style>\n<style>\n /* One left-to-right track that scrolls rather than wrapping: a wrapped row\n leaves a \"→\" pointing at nothing at the end of every row. */\n .ax-flow-scroll { overflow-x: auto; padding: 2px 2px 6px; }\n /* fit-content: the track fills the panel when the steps fit, and grows to the\n steps' minimum width (scrolling) when they don't — so the loop rail always\n spans exactly the flow. */\n .ax-flow-wrap { position: relative; width: fit-content; min-width: 100%; }\n .ax-flow-wrap.looping { padding-bottom: 54px; }\n .ax-flow-diagram { display: flex; align-items: stretch; }\n .ax-flow-step { flex: 1 1 200px; min-width: 200px; max-width: 360px; border: 1px solid var(--color-border, #263241); border-left-width: 4px; border-radius: 16px; padding: 12px 14px; background: var(--color-surface, #17202b); }\n .ax-flow-step h3 { margin: 6px 0 6px; font-size: 14px; }\n .ax-flow-step p { margin: 0 0 10px; }\n .ax-flow-step.tone-ok { border-left-color: var(--color-success, #22c55e); }\n .ax-flow-step.tone-warn { border-left-color: var(--color-warning, #eab308); }\n .ax-flow-step.tone-danger { border-left-color: var(--color-danger, #ef4444); }\n .ax-flow-step.tone-info { border-left-color: var(--color-accent, #4a9eff); }\n .ax-flow-step.tone-idle { border-left-color: var(--color-border, #263241); }\n .ax-flow-num { font-size: 11px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--color-text-muted, #7e8a97); }\n .ax-flow-foot { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }\n .ax-flow-arrow { align-self: center; padding: 0 7px; color: var(--color-accent, #4a9eff); font-weight: 900; }\n .badge.idle { color: var(--color-text-muted, #7e8a97); background: transparent; border: 1px solid var(--color-border, #263241); }\n .ax-flow-rail { position: absolute; left: 22px; right: 22px; bottom: 14px; height: 30px; border: 2px dashed var(--color-accent, #4a9eff); border-top: none; border-radius: 0 0 18px 18px; pointer-events: none; }\n .ax-flow-rail::before { content: '▲'; position: absolute; left: -8px; top: -14px; font-size: 12px; line-height: 1; color: var(--color-accent, #4a9eff); }\n .ax-flow-rail-label { position: absolute; left: 50%; bottom: -9px; transform: translateX(-50%); background: var(--color-bg, #0b0f14); padding: 0 8px; }\n .ax-flow-wrap:not(.looping) .ax-flow-rail { display: none; }\n</style>\n<section class=\"panel\" id=\"ax-offline\" hidden><h2>AX bridge unavailable</h2><p class=\"small\">This flow is not running as an AX-enabled canvas node, so its controls are inert.</p></section>\n<section class=\"panel\">\n <div class=\"ax-work-head\"><h2>Release flow</h2><span class=\"small\" id=\"ax-flow-counts\"></span></div>\n <p class=\"small\">Each step is a canvas AX work item. Materialize drops the same flow onto the board as real nodes.</p>\n <div class=\"ax-flow-scroll\">\n <div class=\"ax-flow-wrap looping\" id=\"ax-flow-wrap\">\n <div class=\"ax-flow-diagram\" id=\"ax-flow-diagram\"></div>\n <div class=\"ax-flow-rail\"><span class=\"badge info ax-flow-rail-label\">loops back to step 1 · max 3 runs</span></div>\n </div>\n </div>\n</section>\n<section class=\"two\" style=\"margin-top:14px\">\n <div class=\"panel\">\n <h2>Run the flow</h2>\n <div class=\"ax-row\"><button type=\"button\" id=\"ax-flow-queue-all\">Queue every step</button><span class=\"small\" id=\"ax-flow-status\"></span></div>\n <p class=\"small\">A step is queued once — after that its status control advances it (todo → in-progress → done).</p>\n <div class=\"ax-row\" style=\"margin-top:10px\">\n <span class=\"small\">or run the whole flow as a bounded loop:</span>\n <label class=\"small\">max runs <input type=\"number\" id=\"ax-loop-runs\" min=\"1\" max=\"20\" step=\"1\" value=\"3\"></label>\n <button type=\"button\" id=\"ax-start-loop\">Start loop</button>\n </div>\n </div>\n <div class=\"panel\">\n <h2>Steer</h2>\n <div class=\"ax-field\"><textarea id=\"ax-steer-message\" placeholder=\"Prioritize the auth refactor\" style=\"min-height:92px\"></textarea></div>\n <div class=\"ax-row\"><button type=\"button\" id=\"ax-send-steer\">Queue steering message</button></div>\n <p class=\"small\">Queued for the agent's next turn — steering is recorded on the timeline, it does not wake a running agent.</p>\n <p class=\"small\" id=\"ax-steer-status\"></p>\n </div>\n</section>\n<section class=\"panel\" id=\"ax-loop-panel\" hidden style=\"margin-top:14px\">\n <div class=\"ax-work-head\"><h2 id=\"ax-loop-title\">Loop</h2><span class=\"badge info\" id=\"ax-loop-run\">run 0 of 0</span></div>\n <p class=\"small\">Each run is its own work item. When you mark the current run <strong>done</strong>, the surface opens the next run and steers again — until the cap or Stop.</p>\n <div class=\"ax-row\"><button type=\"button\" id=\"ax-stop-loop\">Stop loop</button><span class=\"small\" id=\"ax-loop-status\"></span></div>\n <p class=\"small\">Loop control lives in this panel, so reloading it stops the loop — the work items stay as the durable record.</p>\n</section>\n<section class=\"panel\" style=\"margin-top:14px\">\n <h2>Materialize</h2>\n <p class=\"small\">Lays this flow out on the canvas: one markdown node per step, joined in order by flow edges, plus a dashed loop-back edge. Each step node gets its own work item, so its status chip tracks the agent.</p>\n <div class=\"ax-row\"><button type=\"button\" id=\"ax-flow-materialize\">Materialize to board</button><span class=\"small\" id=\"ax-flow-materialize-status\"></span></div>\n <p class=\"small\">Re-materializing REPLACES the nodes this panel put on the board — it never stacks a second copy. The work items it already created stay as the durable record.</p>\n</section>\n</div>\n<script type=\"application/json\" id=\"pmx-data\">{\n \"note\": \"Each step is a canvas AX work item. Materialize drops the same flow onto the board as real nodes.\",\n \"steps\": [\n {\n \"title\": \"Cut the branch\",\n \"detail\": \"Bump the version and write the changelog entry.\"\n },\n {\n \"title\": \"Run the gates\",\n \"detail\": \"typecheck, unit, e2e, lint — all green before anything else.\"\n },\n {\n \"title\": \"Publish\",\n \"detail\": \"Tag, push, npm publish, then smoke the published package.\"\n },\n {\n \"title\": \"Report\",\n \"detail\": \"What shipped, what is still open, what to watch.\"\n }\n ],\n \"loop\": {\n \"enabled\": true,\n \"maxRuns\": 3\n }\n}</script>\n<script>\nconst PMX_DATA = JSON.parse(document.getElementById('pmx-data').textContent);\nfunction fallbackCopy(text) {\n const el = document.createElement('textarea');\n el.value = text;\n el.setAttribute('readonly', '');\n el.style.position = 'fixed';\n el.style.left = '-9999px';\n document.body.appendChild(el);\n el.select();\n document.execCommand('copy');\n el.remove();\n}\nasync function copyText(text) {\n if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText(text);\n else fallbackCopy(text);\n}\ndocument.querySelector('[data-copy-json]')?.addEventListener('click', () => {\n const payload = typeof window.__pmxGetCopyJson === 'function' ? window.__pmxGetCopyJson() : PMX_DATA;\n copyText(JSON.stringify(payload, null, 2));\n});\ndocument.querySelector('[data-copy-prompt]')?.addEventListener('click', () => copyText('Use this PMX Canvas HTML primitive output as context:\\n\\n' + JSON.stringify(PMX_DATA, null, 2)));\n\nconst AX_STATUSES = ['todo', 'in-progress', 'blocked', 'done', 'cancelled'];\nconst AX_MAX_RUNS = 20;\n\nconst el = (id) => document.getElementById(id);\nconst axFlashTimers = {};\nfunction flash(id, message) {\n const target = el(id);\n if (!target) return;\n target.textContent = message;\n if (axFlashTimers[id]) clearTimeout(axFlashTimers[id]);\n axFlashTimers[id] = setTimeout(() => { target.textContent = ''; }, 4000);\n}\nfunction axWorkItems() {\n const state = window.PMX_AX && window.PMX_AX.state;\n return state && Array.isArray(state.workItems) ? state.workItems : [];\n}\nasync function axEmit(type, payload) {\n if (!window.PMX_AX || typeof window.PMX_AX.emit !== 'function') {\n return { ok: false, code: 'ax-bridge-missing', error: 'AX bridge is not available on this surface.' };\n }\n return await window.PMX_AX.emit(type, payload);\n}\nfunction axFailure(result) {\n return (result && (result.error || result.code)) || 'unknown error';\n}\nfunction statusTone(status) {\n if (status === 'done') return 'ok';\n if (status === 'blocked' || status === 'cancelled') return 'danger';\n if (status === 'in-progress') return 'warn';\n return 'info';\n}\nfunction elem(tag, className, textContent) {\n const node = document.createElement(tag);\n if (className) node.className = className;\n if (textContent !== undefined) node.textContent = textContent;\n return node;\n}\nasync function sendSteer() {\n const message = el('ax-steer-message').value.trim();\n if (!message) { flash('ax-steer-status', 'Write a steering message first.'); return; }\n const result = await axEmit('ax.steer', { message });\n if (result && result.ok) {\n el('ax-steer-message').value = '';\n flash('ax-steer-status', 'Queued for the agent\\'s next turn ✓');\n } else {\n flash('ax-steer-status', 'Failed: ' + axFailure(result));\n }\n}\n\n// Loop control is in-memory only (the surface is sandboxed: storage APIs throw).\nlet axLoop = null;\nlet axAdvancing = false;\n\nfunction renderLoop() {\n const panel = el('ax-loop-panel');\n panel.hidden = !axLoop;\n if (!axLoop) return;\n el('ax-loop-title').textContent = axLoop.baseTitle;\n el('ax-loop-run').textContent = 'run ' + axLoop.run + ' of ' + axLoop.maxRuns;\n}\nasync function openRun(session, run) {\n const result = await axEmit('ax.work.create', {\n title: session.baseTitle + ' — run ' + run + '/' + session.maxRuns,\n ...(session.detail ? { detail: session.detail } : {}),\n });\n // The human may have stopped the loop while this was in flight.\n if (axLoop !== session) return false;\n if (!result || !result.ok || !result.primitive || !result.primitive.id) {\n flash('ax-loop-status', 'Could not open run ' + run + ': ' + axFailure(result));\n return false;\n }\n session.run = run;\n session.workItemId = result.primitive.id;\n renderLoop();\n axRenderSurface();\n await axEmit('ax.steer', {\n message:\n 'Loop run ' + run + ' of ' + session.maxRuns + ' for \"' + session.baseTitle +\n '\". Mark work item ' + session.workItemId + ' done when this run is complete.',\n });\n return true;\n}\n/**\n * Start a bounded loop. NEVER auto-started — a human control only. Returns the\n * clamped run count on success (0 on failure) so the surface can sync its inputs;\n * `onStarted` fires the moment run 1 is open, before the loop's own bookkeeping.\n */\nasync function axStartLoop(baseTitle, detail, requested, onStarted) {\n if (axLoop) return 0;\n const maxRuns = Math.min(AX_MAX_RUNS, Math.max(1, Math.round(Number.isFinite(requested) ? requested : 3)));\n const session = { baseTitle: baseTitle, detail: detail, maxRuns: maxRuns, run: 0, workItemId: null };\n axLoop = session;\n renderLoop();\n if (!(await openRun(session, 1))) {\n if (axLoop === session) { axLoop = null; renderLoop(); }\n return 0;\n }\n if (onStarted) onStarted(maxRuns);\n flash('ax-loop-status', requested > AX_MAX_RUNS ? 'Capped at ' + AX_MAX_RUNS + ' runs.' : 'Loop started.');\n await axEmit('ax.event.record', {\n kind: 'steering',\n summary: 'Loop started: ' + baseTitle + ' (max ' + maxRuns + ' runs)',\n });\n return maxRuns;\n}\nasync function stopLoop() {\n const session = axLoop;\n if (!session) return;\n axLoop = null;\n renderLoop();\n axRenderSurface();\n flash('ax-loop-status', 'Loop stopped at run ' + session.run + ' of ' + session.maxRuns + '.');\n await axEmit('ax.event.record', {\n kind: 'steering',\n summary: 'Loop stopped by human: ' + session.baseTitle + ' at run ' + session.run + ' of ' + session.maxRuns,\n });\n}\n/**\n * Advance when the CURRENT run's work item reaches done, observed from live AX\n * state. Double-advance guards: an in-flight flag, and clearing workItemId before\n * awaiting so a repeated state push cannot match the same finished run twice.\n */\nasync function maybeAdvanceLoop() {\n const session = axLoop;\n if (!session || axAdvancing || !session.workItemId) return;\n const current = axWorkItems().find((item) => item.id === session.workItemId);\n if (!current || current.status !== 'done') return;\n if (session.run >= session.maxRuns) {\n axLoop = null;\n renderLoop();\n axRenderSurface();\n flash('ax-loop-status', 'Loop finished — ' + session.maxRuns + ' of ' + session.maxRuns + ' runs done.');\n await axEmit('ax.event.record', {\n kind: 'steering',\n summary: 'Loop complete: ' + session.baseTitle + ' (' + session.maxRuns + ' runs)',\n });\n return;\n }\n axAdvancing = true;\n session.workItemId = null;\n try {\n if (!(await openRun(session, session.run + 1)) && axLoop === session) { axLoop = null; renderLoop(); }\n } finally {\n axAdvancing = false;\n }\n}\n\nfunction applyAxState() {\n axRenderSurface();\n renderLoop();\n void maybeAdvanceLoop();\n}\n\nconst AX_FLOW_TITLE = \"Release flow\";\nconst AX_FLOW_STEPS = Array.isArray(PMX_DATA.steps) ? PMX_DATA.steps : [];\nconst AX_FLOW_LOOP = PMX_DATA.loop || {};\n// Step index → the id of the work item bound to that step (created here, or\n// adopted from a materialize). Matching is by ID ONLY: work-item titles are not\n// unique, so a title match would happily bind the wrong item.\nconst axFlowWorkIds = AX_FLOW_STEPS.map(() => null);\n\nfunction stepWorkItem(index) {\n const id = axFlowWorkIds[index];\n return id ? axWorkItems().find((item) => item.id === id) || null : null;\n}\nfunction stepTone(item) {\n return item ? statusTone(item.status) : 'idle';\n}\n\n// ── render (live AX state only — no local mirror of step status) ──\nfunction renderFlow() {\n const host = el('ax-flow-diagram');\n host.textContent = '';\n let bound = 0;\n let done = 0;\n AX_FLOW_STEPS.forEach((step, index) => {\n if (index > 0) host.appendChild(elem('div', 'ax-flow-arrow', '→'));\n const item = stepWorkItem(index);\n if (item) bound += 1;\n if (item && item.status === 'done') done += 1;\n const card = elem('article', 'ax-flow-step tone-' + stepTone(item));\n card.setAttribute('data-step', String(index + 1));\n const head = elem('div', 'ax-work-head');\n head.appendChild(elem('span', 'ax-flow-num', 'step ' + (index + 1)));\n head.appendChild(elem('span', 'badge ' + stepTone(item), item ? item.status : 'not queued'));\n card.appendChild(head);\n card.appendChild(elem('h3', '', step.title));\n if (step.detail) card.appendChild(elem('p', 'small', step.detail));\n const foot = elem('div', 'ax-flow-foot');\n if (item) {\n const label = elem('label', 'small', 'status ');\n const select = document.createElement('select');\n select.setAttribute('data-status-for', item.id);\n for (const status of AX_STATUSES) {\n const option = document.createElement('option');\n option.value = status;\n option.textContent = status;\n if (status === item.status) option.selected = true;\n select.appendChild(option);\n }\n select.addEventListener('change', () => setStepStatus(index, select.value));\n label.appendChild(select);\n foot.appendChild(label);\n } else {\n const button = elem('button', '', 'Queue step');\n button.type = 'button';\n button.setAttribute('data-queue-step', String(index + 1));\n button.addEventListener('click', () => queueStep(index));\n foot.appendChild(button);\n }\n if (axLoop && item && axLoop.workItemId === item.id) foot.appendChild(elem('span', 'badge info', 'current run'));\n card.appendChild(foot);\n host.appendChild(card);\n });\n el('ax-flow-counts').textContent =\n AX_FLOW_STEPS.length + ' steps · ' + bound + ' queued · ' + done + ' done' +\n (AX_FLOW_LOOP.enabled ? ' · loops' : '');\n}\nfunction axRenderSurface() { renderFlow(); }\n\n// ── actions ──\nasync function queueStep(index) {\n const step = AX_FLOW_STEPS[index];\n if (!step || axFlowWorkIds[index]) return false;\n const result = await axEmit('ax.work.create', {\n title: AX_FLOW_TITLE + ' — ' + (index + 1) + '. ' + step.title,\n ...(step.detail ? { detail: step.detail } : {}),\n });\n if (!result || !result.ok || !result.primitive || !result.primitive.id) {\n flash('ax-flow-status', 'Step ' + (index + 1) + ' failed: ' + axFailure(result));\n return false;\n }\n axFlowWorkIds[index] = result.primitive.id;\n renderFlow();\n flash('ax-flow-status', 'Step ' + (index + 1) + ' queued ✓');\n return true;\n}\nasync function queueAllSteps() {\n for (let index = 0; index < AX_FLOW_STEPS.length; index++) {\n if (!axFlowWorkIds[index] && !(await queueStep(index))) return;\n }\n flash('ax-flow-status', 'Every step is queued ✓');\n}\nasync function setStepStatus(index, status) {\n const id = axFlowWorkIds[index];\n if (!id) return;\n const result = await axEmit('ax.work.update', { id, status });\n flash(\n 'ax-flow-status',\n result && result.ok ? 'Step ' + (index + 1) + ' → ' + status + ' ✓' : 'Update failed: ' + axFailure(result),\n );\n}\nasync function startFlowLoop() {\n if (axLoop) { flash('ax-flow-status', 'A loop is already running — stop it first.'); return; }\n await axStartLoop(AX_FLOW_TITLE, AX_FLOW_STEPS.map((step, i) => (i + 1) + '. ' + step.title).join('\\n'),\n Number(el('ax-loop-runs').value), (maxRuns) => { el('ax-loop-runs').value = String(maxRuns); });\n}\n/**\n * Hand the flow to the server, which owns the whole materialized shape. We send\n * TEXT ONLY — no node type, geometry, or node data — and adopt the work-item ids\n * it links to each step node so this panel and the board show the same status.\n */\nasync function materializeFlow() {\n const result = await axEmit('ax.flow.materialize', {\n title: AX_FLOW_TITLE,\n steps: AX_FLOW_STEPS.map((step) => (step.detail ? { title: step.title, detail: step.detail } : { title: step.title })),\n loop: { enabled: AX_FLOW_LOOP.enabled === true, maxRuns: Number(el('ax-loop-runs').value) || 3 },\n });\n if (!result || !result.ok || !result.primitive) {\n flash('ax-flow-materialize-status', 'Failed: ' + axFailure(result));\n return;\n }\n for (const step of result.primitive.steps) axFlowWorkIds[step.index - 1] = step.workItemId;\n renderFlow();\n flash(\n 'ax-flow-materialize-status',\n result.primitive.steps.length + ' step nodes on the board ✓' +\n (result.primitive.replacedNodeCount ? ' (replaced ' + result.primitive.replacedNodeCount + ')' : ''),\n );\n}\n\nel('ax-flow-queue-all').addEventListener('click', queueAllSteps);\nel('ax-flow-materialize').addEventListener('click', materializeFlow);\nel('ax-start-loop').addEventListener('click', startFlowLoop);\nel('ax-stop-loop').addEventListener('click', stopLoop);\nel('ax-send-steer').addEventListener('click', sendSteer);\nwindow.__pmxGetCopyJson = () => ({\n ...PMX_DATA,\n steps: AX_FLOW_STEPS.map((step, index) => {\n const item = stepWorkItem(index);\n return { ...step, workItemId: axFlowWorkIds[index], status: item ? item.status : null };\n }),\n loop: axLoop ? { title: axLoop.baseTitle, run: axLoop.run, maxRuns: axLoop.maxRuns } : null,\n});\nif (!window.PMX_AX || typeof window.PMX_AX.emit !== 'function') el('ax-offline').hidden = false;\n// Seeded snapshot first, then every live push. A loop NEVER auto-starts on load.\nwindow.addEventListener('pmx-ax-update', applyAxState);\napplyAxState();\n</script>\n</body>\n</html>",
|
|
4750
4687
|
"htmlPrimitive": "ax-flow",
|
|
@@ -4817,7 +4754,6 @@
|
|
|
4817
4754
|
"zIndex": 1,
|
|
4818
4755
|
"collapsed": false,
|
|
4819
4756
|
"pinned": false,
|
|
4820
|
-
"dockPosition": null,
|
|
4821
4757
|
"data": {
|
|
4822
4758
|
"title": "⑧ Agent at work",
|
|
4823
4759
|
"content": "A task caught mid-flight: one work item per step, status chips on the nodes, controls that steer the agent.",
|
|
@@ -4845,7 +4781,6 @@
|
|
|
4845
4781
|
"zIndex": 1,
|
|
4846
4782
|
"collapsed": false,
|
|
4847
4783
|
"pinned": false,
|
|
4848
|
-
"dockPosition": null,
|
|
4849
4784
|
"data": {
|
|
4850
4785
|
"title": "1. Reproduce",
|
|
4851
4786
|
"content": "Ran the publish gate 20× in a loop. Fails 3 in 20, always on the registry probe.",
|
|
@@ -4918,7 +4853,6 @@
|
|
|
4918
4853
|
"zIndex": 1,
|
|
4919
4854
|
"collapsed": false,
|
|
4920
4855
|
"pinned": true,
|
|
4921
|
-
"dockPosition": null,
|
|
4922
4856
|
"data": {
|
|
4923
4857
|
"title": "2. Fix",
|
|
4924
4858
|
"content": "Retry the probe with backoff instead of failing the gate on the first timeout.",
|
|
@@ -4956,7 +4890,6 @@
|
|
|
4956
4890
|
"zIndex": 1,
|
|
4957
4891
|
"collapsed": false,
|
|
4958
4892
|
"pinned": false,
|
|
4959
|
-
"dockPosition": null,
|
|
4960
4893
|
"data": {
|
|
4961
4894
|
"title": "3. Verify",
|
|
4962
4895
|
"content": "Re-run the same 20× loop and require 20 green.",
|
|
@@ -4994,7 +4927,6 @@
|
|
|
4994
4927
|
"zIndex": 1,
|
|
4995
4928
|
"collapsed": false,
|
|
4996
4929
|
"pinned": false,
|
|
4997
|
-
"dockPosition": null,
|
|
4998
4930
|
"data": {
|
|
4999
4931
|
"title": "4. Ship",
|
|
5000
4932
|
"content": "Land the patch, then watch the next three releases.",
|
|
@@ -5032,7 +4964,6 @@
|
|
|
5032
4964
|
"zIndex": 1,
|
|
5033
4965
|
"collapsed": false,
|
|
5034
4966
|
"pinned": false,
|
|
5035
|
-
"dockPosition": null,
|
|
5036
4967
|
"data": {
|
|
5037
4968
|
"title": "How to read this band",
|
|
5038
4969
|
"content": "Each step is a real **AX work item**, so the chip on a step node is the\nagent’s live status — it moves as the agent works.\n\nThe buttons in each step footer steer it: **Start**, **Done** and\n**Blocked** write that step’s work item, which the agent reads back from\n`canvas://ax-work`.\n\nStep 2 is pinned, so `canvas://pinned-context` hands the agent the\nstep it is actually on.",
|
|
@@ -5219,7 +5150,8 @@
|
|
|
5219
5150
|
"prompt": {
|
|
5220
5151
|
"systemAppend": null,
|
|
5221
5152
|
"mode": null
|
|
5222
|
-
}
|
|
5153
|
+
},
|
|
5154
|
+
"scope": null
|
|
5223
5155
|
}
|
|
5224
5156
|
}
|
|
5225
5157
|
}
|