pmx-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/Readme.md +865 -0
  4. package/dist/canvas/global.css +3173 -0
  5. package/dist/canvas/index.js +183 -0
  6. package/dist/json-render/index.css +2 -0
  7. package/dist/json-render/index.js +389 -0
  8. package/dist/types/cli/agent.d.ts +13 -0
  9. package/dist/types/cli/index.d.ts +2 -0
  10. package/dist/types/cli/watch.d.ts +5 -0
  11. package/dist/types/client/App.d.ts +1 -0
  12. package/dist/types/client/canvas/AttentionHistory.d.ts +1 -0
  13. package/dist/types/client/canvas/AttentionToast.d.ts +1 -0
  14. package/dist/types/client/canvas/CanvasNode.d.ts +8 -0
  15. package/dist/types/client/canvas/CanvasViewport.d.ts +8 -0
  16. package/dist/types/client/canvas/CommandPalette.d.ts +4 -0
  17. package/dist/types/client/canvas/ContextMenu.d.ts +24 -0
  18. package/dist/types/client/canvas/ContextPinBar.d.ts +1 -0
  19. package/dist/types/client/canvas/ContextPinHud.d.ts +1 -0
  20. package/dist/types/client/canvas/DockedNode.d.ts +4 -0
  21. package/dist/types/client/canvas/EdgeLayer.d.ts +8 -0
  22. package/dist/types/client/canvas/ExpandedNodeOverlay.d.ts +1 -0
  23. package/dist/types/client/canvas/FocusFieldLayer.d.ts +1 -0
  24. package/dist/types/client/canvas/Minimap.d.ts +23 -0
  25. package/dist/types/client/canvas/SelectionBar.d.ts +1 -0
  26. package/dist/types/client/canvas/ShortcutOverlay.d.ts +3 -0
  27. package/dist/types/client/canvas/SnapshotPanel.d.ts +7 -0
  28. package/dist/types/client/canvas/snap-guides.d.ts +23 -0
  29. package/dist/types/client/canvas/use-node-drag.d.ts +15 -0
  30. package/dist/types/client/canvas/use-node-resize.d.ts +15 -0
  31. package/dist/types/client/canvas/use-pan-zoom.d.ts +16 -0
  32. package/dist/types/client/ext-app/bridge.d.ts +161 -0
  33. package/dist/types/client/icons.d.ts +70 -0
  34. package/dist/types/client/index.d.ts +1 -0
  35. package/dist/types/client/nodes/ContextNode.d.ts +34 -0
  36. package/dist/types/client/nodes/ExtAppFrame.d.ts +18 -0
  37. package/dist/types/client/nodes/FileNode.d.ts +5 -0
  38. package/dist/types/client/nodes/GroupNode.d.ts +6 -0
  39. package/dist/types/client/nodes/ImageNode.d.ts +10 -0
  40. package/dist/types/client/nodes/InlineFormatBar.d.ts +7 -0
  41. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +14 -0
  42. package/dist/types/client/nodes/LedgerNode.d.ts +4 -0
  43. package/dist/types/client/nodes/MarkdownNode.d.ts +6 -0
  44. package/dist/types/client/nodes/McpAppNode.d.ts +4 -0
  45. package/dist/types/client/nodes/MdFormatBar.d.ts +8 -0
  46. package/dist/types/client/nodes/PromptNode.d.ts +5 -0
  47. package/dist/types/client/nodes/ResponseNode.d.ts +5 -0
  48. package/dist/types/client/nodes/StatusNode.d.ts +4 -0
  49. package/dist/types/client/nodes/StatusSummary.d.ts +4 -0
  50. package/dist/types/client/nodes/TraceNode.d.ts +4 -0
  51. package/dist/types/client/nodes/WebpageNode.d.ts +5 -0
  52. package/dist/types/client/nodes/image-warnings.d.ts +6 -0
  53. package/dist/types/client/nodes/inline-editor-commands.d.ts +11 -0
  54. package/dist/types/client/nodes/md-format.d.ts +25 -0
  55. package/dist/types/client/state/attention-bridge.d.ts +3 -0
  56. package/dist/types/client/state/attention-store.d.ts +25 -0
  57. package/dist/types/client/state/canvas-store.d.ts +74 -0
  58. package/dist/types/client/state/intent-bridge.d.ts +158 -0
  59. package/dist/types/client/state/sse-bridge.d.ts +5 -0
  60. package/dist/types/client/theme/tokens.d.ts +27 -0
  61. package/dist/types/client/types.d.ts +40 -0
  62. package/dist/types/client/utils/ext-app-tool-result.d.ts +1 -0
  63. package/dist/types/client/utils/placement.d.ts +1 -0
  64. package/dist/types/client/utils/platform.d.ts +2 -0
  65. package/dist/types/json-render/catalog.d.ts +815 -0
  66. package/dist/types/json-render/charts/components.d.ts +54 -0
  67. package/dist/types/json-render/charts/definitions.d.ts +103 -0
  68. package/dist/types/json-render/charts/extra-components.d.ts +58 -0
  69. package/dist/types/json-render/charts/extra-definitions.d.ts +181 -0
  70. package/dist/types/json-render/renderer/index.d.ts +16 -0
  71. package/dist/types/json-render/schema.d.ts +46 -0
  72. package/dist/types/json-render/server.d.ts +55 -0
  73. package/dist/types/mcp/server.d.ts +22 -0
  74. package/dist/types/server/agent-context.d.ts +21 -0
  75. package/dist/types/server/artifact-paths.d.ts +3 -0
  76. package/dist/types/server/canvas-operations.d.ts +154 -0
  77. package/dist/types/server/canvas-provenance.d.ts +13 -0
  78. package/dist/types/server/canvas-schema.d.ts +49 -0
  79. package/dist/types/server/canvas-serialization.d.ts +25 -0
  80. package/dist/types/server/canvas-state.d.ts +174 -0
  81. package/dist/types/server/canvas-validation.d.ts +33 -0
  82. package/dist/types/server/chart-template.d.ts +29 -0
  83. package/dist/types/server/code-graph.d.ts +67 -0
  84. package/dist/types/server/context-cards.d.ts +24 -0
  85. package/dist/types/server/diagram-presets.d.ts +28 -0
  86. package/dist/types/server/ext-app-call-registry.d.ts +16 -0
  87. package/dist/types/server/ext-app-tool-result.d.ts +1 -0
  88. package/dist/types/server/file-watcher.d.ts +16 -0
  89. package/dist/types/server/index.d.ts +243 -0
  90. package/dist/types/server/mcp-app-candidate.d.ts +25 -0
  91. package/dist/types/server/mcp-app-host.d.ts +65 -0
  92. package/dist/types/server/mcp-app-runtime.d.ts +47 -0
  93. package/dist/types/server/mutation-history.d.ts +105 -0
  94. package/dist/types/server/placement.d.ts +37 -0
  95. package/dist/types/server/server.d.ts +103 -0
  96. package/dist/types/server/spatial-analysis.d.ts +87 -0
  97. package/dist/types/server/trace-manager.d.ts +48 -0
  98. package/dist/types/server/web-artifacts.d.ts +50 -0
  99. package/dist/types/server/webpage-node.d.ts +25 -0
  100. package/dist/types/shared/auto-arrange.d.ts +29 -0
  101. package/dist/types/shared/ext-app-tool-result.d.ts +9 -0
  102. package/dist/types/shared/placement.d.ts +26 -0
  103. package/dist/types/shared/semantic-attention.d.ts +97 -0
  104. package/package.json +109 -0
  105. package/skills/data-analysis/SKILL.md +324 -0
  106. package/skills/doc-coauthoring/SKILL.md +375 -0
  107. package/skills/frontend-design/SKILL.md +45 -0
  108. package/skills/json-render-codegen/SKILL.md +112 -0
  109. package/skills/json-render-core/SKILL.md +265 -0
  110. package/skills/json-render-ink/SKILL.md +273 -0
  111. package/skills/json-render-mcp/SKILL.md +132 -0
  112. package/skills/json-render-react/SKILL.md +264 -0
  113. package/skills/json-render-shadcn/SKILL.md +159 -0
  114. package/skills/playwright-cli/SKILL.md +67 -0
  115. package/skills/pmx-canvas/SKILL.md +668 -0
  116. package/skills/pmx-canvas/evals/evals.json +186 -0
  117. package/skills/pmx-canvas-testing/SKILL.md +78 -0
  118. package/skills/published-consumer-e2e/SKILL.md +43 -0
  119. package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +241 -0
  120. package/skills/web-artifacts-builder/SKILL.md +80 -0
  121. package/skills/web-artifacts-builder/scripts/bundle-artifact.sh +167 -0
  122. package/skills/web-artifacts-builder/scripts/init-artifact.sh +425 -0
  123. package/skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  124. package/skills/web-design-guidelines/SKILL.md +39 -0
  125. package/src/cli/agent.ts +2144 -0
  126. package/src/cli/index.ts +622 -0
  127. package/src/cli/watch.ts +88 -0
  128. package/src/client/App.tsx +507 -0
  129. package/src/client/canvas/AttentionHistory.tsx +81 -0
  130. package/src/client/canvas/AttentionToast.tsx +19 -0
  131. package/src/client/canvas/CanvasNode.tsx +363 -0
  132. package/src/client/canvas/CanvasViewport.tsx +590 -0
  133. package/src/client/canvas/CommandPalette.tsx +302 -0
  134. package/src/client/canvas/ContextMenu.tsx +601 -0
  135. package/src/client/canvas/ContextPinBar.tsx +25 -0
  136. package/src/client/canvas/ContextPinHud.tsx +22 -0
  137. package/src/client/canvas/DockedNode.tsx +66 -0
  138. package/src/client/canvas/EdgeLayer.tsx +280 -0
  139. package/src/client/canvas/ExpandedNodeOverlay.tsx +260 -0
  140. package/src/client/canvas/FocusFieldLayer.tsx +107 -0
  141. package/src/client/canvas/Minimap.tsx +301 -0
  142. package/src/client/canvas/SelectionBar.tsx +69 -0
  143. package/src/client/canvas/ShortcutOverlay.tsx +69 -0
  144. package/src/client/canvas/SnapshotPanel.tsx +236 -0
  145. package/src/client/canvas/snap-guides.ts +170 -0
  146. package/src/client/canvas/use-node-drag.ts +51 -0
  147. package/src/client/canvas/use-node-resize.ts +59 -0
  148. package/src/client/canvas/use-pan-zoom.ts +191 -0
  149. package/src/client/ext-app/bridge.ts +542 -0
  150. package/src/client/icons.tsx +424 -0
  151. package/src/client/index.tsx +7 -0
  152. package/src/client/nodes/ContextNode.tsx +412 -0
  153. package/src/client/nodes/ExtAppFrame.tsx +509 -0
  154. package/src/client/nodes/FileNode.tsx +256 -0
  155. package/src/client/nodes/GroupNode.tsx +39 -0
  156. package/src/client/nodes/ImageNode.tsx +160 -0
  157. package/src/client/nodes/InlineFormatBar.tsx +169 -0
  158. package/src/client/nodes/InlineMarkdownEditor.tsx +123 -0
  159. package/src/client/nodes/LedgerNode.tsx +37 -0
  160. package/src/client/nodes/MarkdownNode.tsx +359 -0
  161. package/src/client/nodes/McpAppNode.tsx +85 -0
  162. package/src/client/nodes/MdFormatBar.tsx +109 -0
  163. package/src/client/nodes/PromptNode.tsx +597 -0
  164. package/src/client/nodes/ResponseNode.tsx +153 -0
  165. package/src/client/nodes/StatusNode.tsx +84 -0
  166. package/src/client/nodes/StatusSummary.tsx +38 -0
  167. package/src/client/nodes/TraceNode.tsx +120 -0
  168. package/src/client/nodes/WebpageNode.tsx +288 -0
  169. package/src/client/nodes/image-warnings.ts +95 -0
  170. package/src/client/nodes/inline-editor-commands.ts +37 -0
  171. package/src/client/nodes/md-format.ts +206 -0
  172. package/src/client/state/attention-bridge.ts +328 -0
  173. package/src/client/state/attention-store.ts +73 -0
  174. package/src/client/state/canvas-store.ts +631 -0
  175. package/src/client/state/intent-bridge.ts +315 -0
  176. package/src/client/state/sse-bridge.ts +965 -0
  177. package/src/client/theme/global.css +3173 -0
  178. package/src/client/theme/tokens.ts +72 -0
  179. package/src/client/types-shims.d.ts +5 -0
  180. package/src/client/types.ts +81 -0
  181. package/src/client/utils/ext-app-tool-result.ts +4 -0
  182. package/src/client/utils/placement.ts +4 -0
  183. package/src/client/utils/platform.ts +2 -0
  184. package/src/json-render/catalog.ts +256 -0
  185. package/src/json-render/charts/components.tsx +198 -0
  186. package/src/json-render/charts/definitions.ts +81 -0
  187. package/src/json-render/charts/extra-components.tsx +267 -0
  188. package/src/json-render/charts/extra-definitions.ts +145 -0
  189. package/src/json-render/renderer/index.css +174 -0
  190. package/src/json-render/renderer/index.tsx +86 -0
  191. package/src/json-render/schema.ts +62 -0
  192. package/src/json-render/server.ts +597 -0
  193. package/src/mcp/server.ts +1377 -0
  194. package/src/server/agent-context.ts +242 -0
  195. package/src/server/artifact-paths.ts +17 -0
  196. package/src/server/canvas-operations.ts +1279 -0
  197. package/src/server/canvas-provenance.ts +243 -0
  198. package/src/server/canvas-schema.ts +432 -0
  199. package/src/server/canvas-serialization.ts +95 -0
  200. package/src/server/canvas-state.ts +1134 -0
  201. package/src/server/canvas-validation.ts +114 -0
  202. package/src/server/chart-template.ts +449 -0
  203. package/src/server/code-graph.ts +370 -0
  204. package/src/server/context-cards.ts +31 -0
  205. package/src/server/diagram-presets.ts +71 -0
  206. package/src/server/ext-app-call-registry.ts +77 -0
  207. package/src/server/ext-app-tool-result.ts +4 -0
  208. package/src/server/file-watcher.ts +121 -0
  209. package/src/server/index.ts +647 -0
  210. package/src/server/mcp-app-candidate.ts +174 -0
  211. package/src/server/mcp-app-host.ts +814 -0
  212. package/src/server/mcp-app-runtime.ts +459 -0
  213. package/src/server/mutation-history.ts +350 -0
  214. package/src/server/placement.ts +125 -0
  215. package/src/server/server.ts +3846 -0
  216. package/src/server/spatial-analysis.ts +356 -0
  217. package/src/server/trace-manager.ts +333 -0
  218. package/src/server/web-artifacts/scripts/bundle-artifact.sh +167 -0
  219. package/src/server/web-artifacts/scripts/init-artifact.sh +426 -0
  220. package/src/server/web-artifacts/scripts/shadcn-components.tar.gz +0 -0
  221. package/src/server/web-artifacts.ts +442 -0
  222. package/src/server/webpage-node.ts +328 -0
  223. package/src/shared/auto-arrange.ts +439 -0
  224. package/src/shared/ext-app-tool-result.ts +76 -0
  225. package/src/shared/placement.ts +81 -0
  226. package/src/shared/semantic-attention.ts +598 -0
@@ -0,0 +1,280 @@
1
+ import type { Signal } from '@preact/signals';
2
+ import { activeNodeId, draggingEdge, searchHighlightIds } from '../state/canvas-store';
3
+ import type { CanvasEdge, CanvasNodeState } from '../types';
4
+
5
+ // ── Edge type visual styles ──────────────────────────────────
6
+ const EDGE_COLORS: Record<CanvasEdge['type'], string> = {
7
+ relation: 'var(--c-muted)',
8
+ 'depends-on': 'var(--c-warn)',
9
+ flow: 'var(--c-accent)',
10
+ references: 'var(--c-dim)',
11
+ };
12
+
13
+ const DIRECTED_TYPES = new Set<CanvasEdge['type']>(['depends-on', 'flow']);
14
+
15
+ function dashArray(edge: CanvasEdge): string | undefined {
16
+ if (edge.style === 'dashed') return '8 4';
17
+ if (edge.style === 'dotted') return '3 3';
18
+ if (edge.type === 'references' && !edge.style) return '8 4';
19
+ return undefined;
20
+ }
21
+
22
+ // ── Anchor computation ───────────────────────────────────────
23
+ interface Anchor {
24
+ x: number;
25
+ y: number;
26
+ }
27
+
28
+ function computeAnchor(node: CanvasNodeState, target: CanvasNodeState): Anchor {
29
+ const cx = node.position.x + node.size.width / 2;
30
+ const cy = node.position.y + node.size.height / 2;
31
+ const tx = target.position.x + target.size.width / 2;
32
+ const ty = target.position.y + target.size.height / 2;
33
+
34
+ const dx = tx - cx;
35
+ const dy = ty - cy;
36
+
37
+ const hw = node.size.width / 2;
38
+ const hh = node.size.height / 2;
39
+
40
+ // Determine which side the edge exits from
41
+ const tanAngle = Math.abs(dy / (dx || 0.001));
42
+ const boxRatio = hh / (hw || 0.001);
43
+
44
+ if (tanAngle > boxRatio) {
45
+ // Top or bottom
46
+ const sign = dy > 0 ? 1 : -1;
47
+ return {
48
+ x: cx + (hh / tanAngle) * (dx > 0 ? 1 : -1),
49
+ y: cy + hh * sign,
50
+ };
51
+ }
52
+
53
+ // Left or right
54
+ const sign = dx > 0 ? 1 : -1;
55
+ return {
56
+ x: cx + hw * sign,
57
+ y: cy + tanAngle * hw * (dy > 0 ? 1 : -1),
58
+ };
59
+ }
60
+
61
+ // ── Bezier midpoint at t=0.5 ─────────────────────────────────
62
+ function bezierMidpoint(
63
+ x1: number,
64
+ y1: number,
65
+ cx1: number,
66
+ cy1: number,
67
+ cx2: number,
68
+ cy2: number,
69
+ x2: number,
70
+ y2: number,
71
+ ): { x: number; y: number } {
72
+ const t = 0.5;
73
+ const mt = 1 - t;
74
+ return {
75
+ x: mt * mt * mt * x1 + 3 * mt * mt * t * cx1 + 3 * mt * t * t * cx2 + t * t * t * x2,
76
+ y: mt * mt * mt * y1 + 3 * mt * mt * t * cy1 + 3 * mt * t * t * cy2 + t * t * t * y2,
77
+ };
78
+ }
79
+
80
+ // ── EdgePath component ───────────────────────────────────────
81
+ interface EdgePathProps {
82
+ edge: CanvasEdge;
83
+ fromNode: CanvasNodeState;
84
+ toNode: CanvasNodeState;
85
+ focused: boolean; // connected to the active node
86
+ dimmed: boolean; // active node exists but this edge is NOT connected
87
+ }
88
+
89
+ function EdgePath({ edge, fromNode, toNode, focused, dimmed }: EdgePathProps) {
90
+ const start = computeAnchor(fromNode, toNode);
91
+ const end = computeAnchor(toNode, fromNode);
92
+
93
+ const dx = end.x - start.x;
94
+ const dy = end.y - start.y;
95
+ const dist = Math.sqrt(dx * dx + dy * dy);
96
+ const curvature = Math.min(dist * 0.25, 80);
97
+
98
+ // Control points: offset perpendicular to direct line
99
+ const nx = dx / (dist || 1);
100
+ const ny = dy / (dist || 1);
101
+ const cx1 = start.x + nx * curvature;
102
+ const cy1 = start.y + ny * curvature;
103
+ const cx2 = end.x - nx * curvature;
104
+ const cy2 = end.y - ny * curvature;
105
+
106
+ const d = `M ${start.x} ${start.y} C ${cx1} ${cy1}, ${cx2} ${cy2}, ${end.x} ${end.y}`;
107
+ const color = EDGE_COLORS[edge.type];
108
+ const directed = DIRECTED_TYPES.has(edge.type);
109
+ const dash = dashArray(edge);
110
+
111
+ const mid = edge.label
112
+ ? bezierMidpoint(start.x, start.y, cx1, cy1, cx2, cy2, end.x, end.y)
113
+ : null;
114
+
115
+ const pathId = `edge-path-${edge.id}`;
116
+
117
+ return (
118
+ <g>
119
+ {/* Invisible wide hitbox for hover/click */}
120
+ <path
121
+ d={d}
122
+ fill="none"
123
+ stroke="transparent"
124
+ stroke-width="12"
125
+ style={{ cursor: 'pointer' }}
126
+ />
127
+
128
+ {/* Glow layer for focused edges */}
129
+ {focused && (
130
+ <path
131
+ d={d}
132
+ fill="none"
133
+ stroke={color}
134
+ stroke-width="6"
135
+ stroke-dasharray={dash}
136
+ opacity="0.15"
137
+ style={{ filter: 'blur(3px)' }}
138
+ />
139
+ )}
140
+
141
+ {/* Visible edge */}
142
+ <path
143
+ id={pathId}
144
+ d={d}
145
+ fill="none"
146
+ stroke={color}
147
+ stroke-width={focused ? 2.5 : 1.5}
148
+ stroke-dasharray={dash}
149
+ marker-end={directed ? 'url(#edge-arrow)' : undefined}
150
+ opacity={dimmed ? 0.2 : focused ? 1 : 0.75}
151
+ style={{ transition: 'opacity 0.2s, stroke-width 0.2s' }}
152
+ />
153
+
154
+ {/* Animated pulse dot */}
155
+ {edge.animated && (
156
+ <circle r="3" fill={color} opacity="0.9">
157
+ <animateMotion dur="2s" repeatCount="indefinite">
158
+ <mpath href={`#${pathId}`} />
159
+ </animateMotion>
160
+ </circle>
161
+ )}
162
+
163
+ {/* Label at midpoint */}
164
+ {mid && edge.label && (
165
+ <g transform={`translate(${mid.x}, ${mid.y})`}>
166
+ <rect
167
+ class="edge-label-bg"
168
+ x={-(edge.label.length * 3.5 + 8)}
169
+ y="-10"
170
+ width={edge.label.length * 7 + 16}
171
+ height="20"
172
+ rx="4"
173
+ />
174
+ <text
175
+ class="edge-label"
176
+ text-anchor="middle"
177
+ dominant-baseline="central"
178
+ fill="var(--c-text)"
179
+ font-size="11"
180
+ >
181
+ {edge.label}
182
+ </text>
183
+ </g>
184
+ )}
185
+ </g>
186
+ );
187
+ }
188
+
189
+ // ── EdgeLayer ────────────────────────────────────────────────
190
+ interface EdgeLayerProps {
191
+ nodes: Signal<Map<string, CanvasNodeState>>;
192
+ edges: Signal<Map<string, CanvasEdge>>;
193
+ }
194
+
195
+ export function EdgeLayer({ nodes, edges }: EdgeLayerProps) {
196
+ const nodeMap = nodes.value;
197
+ const edgeList = Array.from(edges.value.values());
198
+ const focusId = activeNodeId.value;
199
+ const hasFocus = focusId !== null;
200
+ const searchSet = searchHighlightIds.value;
201
+ const hasSearch = searchSet !== null;
202
+
203
+ if (edgeList.length === 0) return null;
204
+
205
+ const PAD = 96;
206
+ const worldNodes = Array.from(nodeMap.values());
207
+ const minX = Math.min(...worldNodes.map((node) => node.position.x)) - PAD;
208
+ const minY = Math.min(...worldNodes.map((node) => node.position.y)) - PAD;
209
+ const maxX = Math.max(...worldNodes.map((node) => node.position.x + node.size.width)) + PAD;
210
+ const maxY = Math.max(...worldNodes.map((node) => node.position.y + node.size.height)) + PAD;
211
+ const width = Math.max(1, maxX - minX);
212
+ const height = Math.max(1, maxY - minY);
213
+
214
+ return (
215
+ <svg
216
+ aria-label="Canvas connections"
217
+ role="img"
218
+ viewBox={`${minX} ${minY} ${width} ${height}`}
219
+ width={width}
220
+ height={height}
221
+ style={{
222
+ position: 'absolute',
223
+ top: `${minY}px`,
224
+ left: `${minX}px`,
225
+ pointerEvents: 'none',
226
+ overflow: 'visible',
227
+ }}
228
+ >
229
+ <title>Canvas connections</title>
230
+ <defs>
231
+ <marker
232
+ id="edge-arrow"
233
+ viewBox="0 0 10 10"
234
+ refX="9"
235
+ refY="5"
236
+ markerWidth="8"
237
+ markerHeight="8"
238
+ orient="auto-start-reverse"
239
+ >
240
+ <path d="M 0 1 L 10 5 L 0 9 z" fill="currentColor" opacity="0.75" />
241
+ </marker>
242
+ </defs>
243
+ {edgeList.map((edge) => {
244
+ const fromNode = nodeMap.get(edge.from);
245
+ const toNode = nodeMap.get(edge.to);
246
+ if (!fromNode || !toNode) return null;
247
+ const isConnected = hasFocus && (edge.from === focusId || edge.to === focusId);
248
+ const searchDimmed = hasSearch && !(searchSet.has(edge.from) || searchSet.has(edge.to));
249
+ return (
250
+ <EdgePath
251
+ key={edge.id}
252
+ edge={edge}
253
+ fromNode={fromNode}
254
+ toNode={toNode}
255
+ focused={isConnected}
256
+ dimmed={(hasFocus && !isConnected) || searchDimmed}
257
+ />
258
+ );
259
+ })}
260
+ {/* Live preview edge while drag-connecting */}
261
+ {draggingEdge.value && (() => {
262
+ const de = draggingEdge.value;
263
+ const dx = de.cursorX - de.fromX;
264
+ const dy = de.cursorY - de.fromY;
265
+ const dist = Math.sqrt(dx * dx + dy * dy);
266
+ const curve = Math.min(dist * 0.25, 80);
267
+ const nx = dx / (dist || 1);
268
+ const ny = dy / (dist || 1);
269
+ const previewD = `M ${de.fromX} ${de.fromY} C ${de.fromX + nx * curve} ${de.fromY + ny * curve}, ${de.cursorX - nx * curve} ${de.cursorY - ny * curve}, ${de.cursorX} ${de.cursorY}`;
270
+ return (
271
+ <g>
272
+ <path d={previewD} fill="none" stroke="var(--c-accent)" stroke-width="6" opacity="0.1" style={{ filter: 'blur(3px)' }} />
273
+ <path d={previewD} fill="none" stroke="var(--c-accent)" stroke-width="2" stroke-dasharray="6 4" opacity="0.8" />
274
+ <circle cx={de.cursorX} cy={de.cursorY} r="5" fill="var(--c-accent)" opacity="0.5" />
275
+ </g>
276
+ );
277
+ })()}
278
+ </svg>
279
+ );
280
+ }
@@ -0,0 +1,260 @@
1
+ import { useCallback, useState } from 'preact/hooks';
2
+ import { ContextNode } from '../nodes/ContextNode';
3
+ import { FileNode } from '../nodes/FileNode';
4
+ import { LedgerNode } from '../nodes/LedgerNode';
5
+ import { MarkdownNode } from '../nodes/MarkdownNode';
6
+ import { McpAppNode } from '../nodes/McpAppNode';
7
+ import { StatusNode } from '../nodes/StatusNode';
8
+ import { ImageNode } from '../nodes/ImageNode';
9
+ import { WebpageNode } from '../nodes/WebpageNode';
10
+ import { PromptNode } from '../nodes/PromptNode';
11
+ import { ResponseNode } from '../nodes/ResponseNode';
12
+ import { TraceNode } from '../nodes/TraceNode';
13
+ import {
14
+ collapseExpandedNode,
15
+ contextPinnedNodeIds,
16
+ expandedNodeId,
17
+ nodes,
18
+ toggleContextPin,
19
+ } from '../state/canvas-store';
20
+ import { TYPE_LABELS } from '../types';
21
+ import type { CanvasNodeState } from '../types';
22
+
23
+ function renderContent(node: CanvasNodeState, expanded: boolean) {
24
+ switch (node.type) {
25
+ case 'markdown':
26
+ return <MarkdownNode node={node} expanded={expanded} />;
27
+ case 'mcp-app':
28
+ return <McpAppNode node={node} />;
29
+ case 'webpage':
30
+ return <WebpageNode node={node} expanded={expanded} />;
31
+ case 'json-render':
32
+ return <McpAppNode node={node} />;
33
+ case 'graph':
34
+ return <McpAppNode node={node} />;
35
+ case 'prompt':
36
+ return <PromptNode node={node} />;
37
+ case 'response':
38
+ return <ResponseNode node={node} expanded={expanded} />;
39
+ case 'status':
40
+ return <StatusNode node={node} />;
41
+ case 'context':
42
+ return <ContextNode node={node} expanded={expanded} />;
43
+ case 'ledger':
44
+ return <LedgerNode node={node} />;
45
+ case 'trace':
46
+ return <TraceNode node={node} />;
47
+ case 'file':
48
+ return <FileNode node={node} expanded={expanded} />;
49
+ case 'image':
50
+ return <ImageNode node={node} expanded={expanded} />;
51
+ default:
52
+ return <div>Unknown node type</div>;
53
+ }
54
+ }
55
+
56
+ /** Extract plain text content from a node for word count / copy. */
57
+ function getNodeTextContent(node: CanvasNodeState): string {
58
+ switch (node.type) {
59
+ case 'markdown':
60
+ return (node.data.content as string) || '';
61
+ case 'file':
62
+ return (node.data.fileContent as string) || '';
63
+ case 'webpage':
64
+ return (node.data.content as string) || '';
65
+ case 'json-render':
66
+ case 'graph':
67
+ return JSON.stringify(node.data.spec ?? node.data.graphConfig ?? {}, null, 2);
68
+ default:
69
+ return '';
70
+ }
71
+ }
72
+
73
+ function wordCount(text: string): number {
74
+ if (!text) return 0;
75
+ return text.split(/\s+/).filter(Boolean).length;
76
+ }
77
+
78
+ export function ExpandedNodeOverlay() {
79
+ const nodeId = expandedNodeId.value;
80
+ const node = nodeId ? nodes.value.get(nodeId) : undefined;
81
+ const [copied, setCopied] = useState(false);
82
+
83
+ const handleClose = useCallback(() => {
84
+ collapseExpandedNode();
85
+ }, []);
86
+
87
+ const handleBackdropPointerDown = useCallback((e: PointerEvent) => {
88
+ if ((e.target as HTMLElement).classList.contains('expanded-overlay-backdrop')) {
89
+ collapseExpandedNode();
90
+ }
91
+ }, []);
92
+
93
+ const handleCopy = useCallback(() => {
94
+ if (!node) return;
95
+ const text = getNodeTextContent(node);
96
+ if (!text) return;
97
+ navigator.clipboard.writeText(text).then(() => {
98
+ setCopied(true);
99
+ setTimeout(() => setCopied(false), 1500);
100
+ });
101
+ }, [node]);
102
+
103
+ const handleToggleCtxPin = useCallback(() => {
104
+ if (!nodeId) return;
105
+ toggleContextPin(nodeId);
106
+ }, [nodeId]);
107
+
108
+ if (!node) return null;
109
+
110
+ const title =
111
+ (node.data.title as string) ||
112
+ (node.data.path as string)?.split('/').pop() ||
113
+ TYPE_LABELS[node.type];
114
+ const textContent = getNodeTextContent(node);
115
+ const words = wordCount(textContent);
116
+ const isCtxPinned = nodeId ? contextPinnedNodeIds.value.has(nodeId) : false;
117
+ const hasText = textContent.length > 0;
118
+
119
+ return (
120
+ <div
121
+ class="expanded-overlay-backdrop"
122
+ onPointerDown={handleBackdropPointerDown}
123
+ style={{
124
+ position: 'fixed',
125
+ inset: 0,
126
+ zIndex: 10001,
127
+ background: 'rgba(10,14,30,0.85)',
128
+ backdropFilter: 'blur(8px)',
129
+ display: 'flex',
130
+ alignItems: 'stretch',
131
+ justifyContent: 'center',
132
+ padding: '32px',
133
+ }}
134
+ >
135
+ <div
136
+ class="expanded-overlay-panel"
137
+ style={{
138
+ flex: 1,
139
+ maxWidth: '1200px',
140
+ display: 'flex',
141
+ flexDirection: 'column',
142
+ background: 'var(--c-panel)',
143
+ border: `1px solid ${isCtxPinned ? 'var(--c-warn)' : 'var(--c-accent)'}`,
144
+ borderRadius: 'var(--radius)',
145
+ boxShadow: `0 0 0 1px ${isCtxPinned ? 'var(--c-warn)' : 'var(--c-accent)'}, 0 24px 80px rgba(0,0,0,0.6)`,
146
+ overflow: 'hidden',
147
+ }}
148
+ >
149
+ {/* Title bar */}
150
+ <div
151
+ style={{
152
+ display: 'flex',
153
+ alignItems: 'center',
154
+ gap: '10px',
155
+ padding: '10px 16px',
156
+ background: 'var(--c-panel-glass)',
157
+ borderBottom: '1px solid var(--c-line)',
158
+ flexShrink: 0,
159
+ }}
160
+ >
161
+ <span
162
+ style={{
163
+ fontSize: '10px',
164
+ padding: '1px 6px',
165
+ borderRadius: '4px',
166
+ background: 'var(--c-accent-12)',
167
+ color: 'var(--c-accent)',
168
+ textTransform: 'uppercase',
169
+ letterSpacing: '0.04em',
170
+ }}
171
+ >
172
+ {TYPE_LABELS[node.type]}
173
+ </span>
174
+ <span
175
+ style={{
176
+ flex: 1,
177
+ fontSize: '13px',
178
+ fontWeight: 600,
179
+ color: 'var(--c-text)',
180
+ whiteSpace: 'nowrap',
181
+ overflow: 'hidden',
182
+ textOverflow: 'ellipsis',
183
+ }}
184
+ >
185
+ {title}
186
+ </span>
187
+
188
+ {/* Action buttons */}
189
+ <div class="expanded-actions">
190
+ {/* Context pin toggle */}
191
+ <button
192
+ type="button"
193
+ class={`expanded-action-btn ${isCtxPinned ? 'expanded-action-active' : ''}`}
194
+ onClick={handleToggleCtxPin}
195
+ title={isCtxPinned ? 'Remove from context' : 'Pin as context'}
196
+ >
197
+ {isCtxPinned ? '\u2726 In context' : '\u2726 Pin as context'}
198
+ </button>
199
+
200
+ {/* Copy content */}
201
+ {hasText && (
202
+ <button
203
+ type="button"
204
+ class="expanded-action-btn"
205
+ onClick={handleCopy}
206
+ title="Copy content to clipboard"
207
+ >
208
+ {copied ? 'Copied!' : 'Copy'}
209
+ </button>
210
+ )}
211
+
212
+ {/* Word count */}
213
+ {words > 0 && (
214
+ <span class="expanded-meta">
215
+ {words.toLocaleString()} word{words !== 1 ? 's' : ''}
216
+ </span>
217
+ )}
218
+ </div>
219
+
220
+ <span style={{ fontSize: '10px', color: 'var(--c-muted)' }}>Esc to close</span>
221
+ <button
222
+ type="button"
223
+ onClick={handleClose}
224
+ style={{
225
+ background: 'none',
226
+ border: 'none',
227
+ color: 'var(--c-muted)',
228
+ cursor: 'pointer',
229
+ padding: '2px 6px',
230
+ fontSize: '16px',
231
+ lineHeight: 1,
232
+ borderRadius: '4px',
233
+ }}
234
+ onMouseEnter={(e) => {
235
+ (e.target as HTMLElement).style.color = 'var(--c-text)';
236
+ }}
237
+ onMouseLeave={(e) => {
238
+ (e.target as HTMLElement).style.color = 'var(--c-muted)';
239
+ }}
240
+ title="Close (Esc)"
241
+ >
242
+ ×
243
+ </button>
244
+ </div>
245
+
246
+ {/* Content area — full height */}
247
+ <div
248
+ style={{
249
+ flex: 1,
250
+ overflow: 'auto',
251
+ padding: '16px',
252
+ minHeight: 0,
253
+ }}
254
+ >
255
+ {renderContent(node, true)}
256
+ </div>
257
+ </div>
258
+ </div>
259
+ );
260
+ }
@@ -0,0 +1,107 @@
1
+ import { attentionPrimaryNodeIds, attentionRegions, attentionSecondaryNodeIds } from '../state/attention-store';
2
+ import { nodes } from '../state/canvas-store';
3
+
4
+ interface Rect {
5
+ left: number;
6
+ top: number;
7
+ width: number;
8
+ height: number;
9
+ }
10
+
11
+ function getNodeRect(nodeId: string): Rect | null {
12
+ const node = nodes.value.get(nodeId);
13
+ if (!node || node.dockPosition !== null) return null;
14
+ return {
15
+ left: node.position.x,
16
+ top: node.position.y,
17
+ width: node.size.width,
18
+ height: node.size.height,
19
+ };
20
+ }
21
+
22
+ function getRegionRect(nodeIds: string[]): Rect | null {
23
+ const rects = nodeIds
24
+ .map((nodeId) => getNodeRect(nodeId))
25
+ .filter((rect): rect is Rect => rect !== null);
26
+ if (rects.length === 0) return null;
27
+
28
+ const minLeft = Math.min(...rects.map((rect) => rect.left));
29
+ const minTop = Math.min(...rects.map((rect) => rect.top));
30
+ const maxRight = Math.max(...rects.map((rect) => rect.left + rect.width));
31
+ const maxBottom = Math.max(...rects.map((rect) => rect.top + rect.height));
32
+ const padX = 54;
33
+ const padY = 46;
34
+
35
+ return {
36
+ left: minLeft - padX,
37
+ top: minTop - padY,
38
+ width: maxRight - minLeft + padX * 2,
39
+ height: maxBottom - minTop + padY * 2,
40
+ };
41
+ }
42
+
43
+ function rectStyle(rect: Rect, radius: number): Record<string, string> {
44
+ return {
45
+ left: `${rect.left}px`,
46
+ top: `${rect.top}px`,
47
+ width: `${rect.width}px`,
48
+ height: `${rect.height}px`,
49
+ borderRadius: `${radius}px`,
50
+ };
51
+ }
52
+
53
+ export function FocusFieldLayer() {
54
+ const primaryNodeIds = Array.from(attentionPrimaryNodeIds.value);
55
+ const secondaryNodeIds = Array.from(attentionSecondaryNodeIds.value);
56
+ const regions = attentionRegions.value;
57
+
58
+ if (primaryNodeIds.length === 0 && secondaryNodeIds.length === 0) return null;
59
+
60
+ return (
61
+ <div class="attention-field-layer" aria-hidden="true">
62
+ {regions.map((region) => {
63
+ const rect = getRegionRect(region.nodeIds);
64
+ if (!rect) return null;
65
+ return (
66
+ <div
67
+ key={region.id}
68
+ class="attention-field-region"
69
+ style={rectStyle(rect, 42)}
70
+ />
71
+ );
72
+ })}
73
+ {secondaryNodeIds.map((nodeId) => {
74
+ const rect = getNodeRect(nodeId);
75
+ if (!rect) return null;
76
+ return (
77
+ <div
78
+ key={`secondary-${nodeId}`}
79
+ class="attention-field-node attention-field-secondary"
80
+ style={rectStyle({
81
+ left: rect.left - 18,
82
+ top: rect.top - 18,
83
+ width: rect.width + 36,
84
+ height: rect.height + 36,
85
+ }, 28)}
86
+ />
87
+ );
88
+ })}
89
+ {primaryNodeIds.map((nodeId) => {
90
+ const rect = getNodeRect(nodeId);
91
+ if (!rect) return null;
92
+ return (
93
+ <div
94
+ key={`primary-${nodeId}`}
95
+ class="attention-field-node attention-field-primary"
96
+ style={rectStyle({
97
+ left: rect.left - 24,
98
+ top: rect.top - 24,
99
+ width: rect.width + 48,
100
+ height: rect.height + 48,
101
+ }, 30)}
102
+ />
103
+ );
104
+ })}
105
+ </div>
106
+ );
107
+ }