pmx-canvas 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +148 -1
- package/Readme.md +13 -11
- package/dist/canvas/index.js +2 -2
- package/dist/json-render/index.css +1 -1
- package/dist/types/cli/daemon.d.ts +74 -0
- package/dist/types/cli/watch.d.ts +2 -2
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
- package/dist/types/client/canvas/Minimap.d.ts +1 -1
- package/dist/types/client/nodes/FileNode.d.ts +1 -1
- package/dist/types/client/nodes/ImageNode.d.ts +1 -1
- package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
- package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
- package/dist/types/client/nodes/PromptNode.d.ts +1 -1
- package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
- package/dist/types/mcp/canvas-access.d.ts +0 -7
- package/dist/types/server/ax-state-manager.d.ts +3 -2
- package/dist/types/server/bundled-skills.d.ts +3 -3
- package/dist/types/server/canvas-schema.d.ts +1 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/ext-app-lookup.d.ts +1 -3
- package/dist/types/server/html-primitives.d.ts +1 -1
- package/dist/types/server/index.d.ts +4 -4
- package/dist/types/server/operations/composites.d.ts +29 -23
- package/dist/types/server/operations/http.d.ts +2 -1
- package/dist/types/server/operations/index.d.ts +1 -1
- package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
- package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
- package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
- package/dist/types/shared/ax-intent.d.ts +1 -1
- package/docs/RELEASE.md +23 -8
- package/docs/ax-host-adapter-contract.md +7 -7
- package/docs/ax-state-contract.md +7 -6
- package/docs/bun-webview-integration.md +5 -5
- package/docs/cli.md +1 -1
- package/docs/http-api.md +35 -3
- package/docs/mcp.md +121 -100
- package/docs/node-types.md +45 -35
- package/docs/plans/plan-006-mcp-tool-consolidation.md +3 -1
- package/docs/plans/plan-008-registry-finish.md +2 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +51 -0
- package/docs/screenshot.png +0 -0
- package/docs/tech-debt-assessment-2026-06.md +1 -1
- package/docs/tech-debt-assessment-2026-07.md +135 -0
- package/package.json +5 -2
- package/skills/data-analysis/SKILL.md +3 -3
- package/skills/frontend-design/SKILL.md +3 -2
- package/skills/json-render-mcp/SKILL.md +4 -3
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/pmx-canvas/SKILL.md +12 -7
- package/skills/pmx-canvas/evals/evals.json +2 -2
- package/skills/pmx-canvas/references/ax-html-control-surface.md +3 -1
- package/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +2 -2
- package/skills/pmx-canvas/references/full-reference.md +58 -56
- package/skills/pmx-canvas/references/html-primitives.md +3 -3
- package/skills/tufte-viz/SKILL.md +5 -4
- package/skills/web-artifacts-builder/SKILL.md +4 -4
- package/src/cli/agent.ts +1861 -1548
- package/src/cli/daemon.ts +460 -0
- package/src/cli/index.ts +63 -326
- package/src/cli/watch.ts +2 -10
- package/src/client/App.tsx +48 -46
- package/src/client/canvas/AttentionHistory.tsx +11 -1
- package/src/client/canvas/CanvasNode.tsx +41 -29
- package/src/client/canvas/CanvasViewport.tsx +101 -66
- package/src/client/canvas/CommandPalette.tsx +61 -27
- package/src/client/canvas/ContextMenu.tsx +13 -20
- package/src/client/canvas/ContextPinBar.tsx +1 -5
- package/src/client/canvas/ContextPinHud.tsx +1 -6
- package/src/client/canvas/DockedNode.tsx +4 -4
- package/src/client/canvas/EdgeLayer.tsx +37 -36
- package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
- package/src/client/canvas/FocusFieldLayer.tsx +20 -22
- package/src/client/canvas/IntentLayer.tsx +31 -14
- package/src/client/canvas/Minimap.tsx +11 -16
- package/src/client/canvas/SelectionBar.tsx +4 -11
- package/src/client/canvas/ShortcutOverlay.tsx +3 -1
- package/src/client/canvas/SnapshotPanel.tsx +77 -95
- package/src/client/canvas/auto-fit.ts +15 -14
- package/src/client/canvas/snap-guides.ts +12 -12
- package/src/client/canvas/use-node-resize.ts +1 -5
- package/src/client/canvas/use-pan-zoom.ts +25 -26
- package/src/client/ext-app/bridge.ts +3 -12
- package/src/client/icons.tsx +63 -20
- package/src/client/nodes/ContextNode.tsx +14 -25
- package/src/client/nodes/ExtAppFrame.tsx +60 -39
- package/src/client/nodes/FileNode.tsx +74 -62
- package/src/client/nodes/GroupNode.tsx +4 -6
- package/src/client/nodes/HtmlNode.tsx +76 -46
- package/src/client/nodes/ImageNode.tsx +18 -27
- package/src/client/nodes/InlineFormatBar.tsx +4 -21
- package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
- package/src/client/nodes/LedgerNode.tsx +10 -4
- package/src/client/nodes/MarkdownNode.tsx +3 -10
- package/src/client/nodes/McpAppNode.tsx +26 -22
- package/src/client/nodes/MdFormatBar.tsx +10 -7
- package/src/client/nodes/PromptNode.tsx +23 -51
- package/src/client/nodes/ResponseNode.tsx +3 -13
- package/src/client/nodes/StatusNode.tsx +5 -9
- package/src/client/nodes/StatusSummary.tsx +2 -8
- package/src/client/nodes/WebpageNode.tsx +20 -14
- package/src/client/nodes/iframe-document-url.ts +25 -16
- package/src/client/nodes/image-warnings.ts +1 -7
- package/src/client/nodes/md-format.ts +20 -5
- package/src/client/state/attention-bridge.ts +4 -9
- package/src/client/state/attention-store.ts +1 -7
- package/src/client/state/canvas-store.ts +52 -36
- package/src/client/state/intent-bridge.ts +176 -112
- package/src/client/state/intent-store.ts +4 -1
- package/src/client/state/sse-bridge.ts +53 -70
- package/src/json-render/catalog.ts +12 -16
- package/src/json-render/charts/components.tsx +16 -20
- package/src/json-render/charts/extra-components.tsx +8 -16
- package/src/json-render/charts/extra-definitions.ts +1 -2
- package/src/json-render/charts/tufte-components.tsx +37 -20
- package/src/json-render/charts/tufte-definitions.ts +8 -2
- package/src/json-render/renderer/index.tsx +42 -22
- package/src/json-render/schema.ts +6 -3
- package/src/json-render/server.ts +33 -39
- package/src/mcp/canvas-access.ts +40 -121
- package/src/mcp/server.ts +206 -271
- package/src/server/agent-context.ts +63 -36
- package/src/server/ax-context.ts +7 -5
- package/src/server/ax-interaction.ts +176 -43
- package/src/server/ax-state-manager.ts +185 -41
- package/src/server/ax-state.ts +142 -47
- package/src/server/bundled-skills.ts +3 -3
- package/src/server/canvas-db.ts +213 -95
- package/src/server/canvas-operations.ts +177 -120
- package/src/server/canvas-provenance.ts +1 -4
- package/src/server/canvas-schema.ts +500 -102
- package/src/server/canvas-serialization.ts +27 -35
- package/src/server/canvas-state.ts +155 -58
- package/src/server/chart-template.ts +4 -5
- package/src/server/code-graph.ts +20 -7
- package/src/server/diagram-presets.ts +28 -29
- package/src/server/ext-app-lookup.ts +4 -20
- package/src/server/html-node-summary.ts +19 -10
- package/src/server/html-primitives.ts +326 -97
- package/src/server/html-surface.ts +6 -9
- package/src/server/image-source.ts +6 -4
- package/src/server/index.ts +321 -218
- package/src/server/intent-registry.ts +3 -6
- package/src/server/mcp-app-candidate.ts +5 -10
- package/src/server/mcp-app-host.ts +14 -38
- package/src/server/mcp-app-runtime.ts +12 -20
- package/src/server/mutation-history.ts +15 -5
- package/src/server/operations/composites.ts +38 -40
- package/src/server/operations/http.ts +5 -5
- package/src/server/operations/index.ts +7 -1
- package/src/server/operations/invoker.ts +4 -3
- package/src/server/operations/mcp.ts +29 -36
- package/src/server/operations/ops/annotation.ts +122 -10
- package/src/server/operations/ops/app.ts +102 -77
- package/src/server/operations/ops/ax-await.ts +17 -10
- package/src/server/operations/ops/ax-read.ts +347 -0
- package/src/server/operations/ops/ax-shared.ts +2 -7
- package/src/server/operations/ops/ax-state.ts +32 -14
- package/src/server/operations/ops/ax-timeline.ts +32 -19
- package/src/server/operations/ops/ax-work.ts +54 -37
- package/src/server/operations/ops/batch.ts +39 -14
- package/src/server/operations/ops/canvas-wire.ts +91 -0
- package/src/server/operations/ops/edges.ts +37 -25
- package/src/server/operations/ops/ext-app.ts +346 -0
- package/src/server/operations/ops/groups.ts +49 -20
- package/src/server/operations/ops/intent.ts +18 -12
- package/src/server/operations/ops/json-render.ts +239 -98
- package/src/server/operations/ops/nodes.ts +300 -111
- package/src/server/operations/ops/query.ts +62 -33
- package/src/server/operations/ops/snapshots.ts +35 -26
- package/src/server/operations/ops/validate.ts +2 -1
- package/src/server/operations/ops/viewport.ts +60 -16
- package/src/server/operations/ops/webview.ts +44 -18
- package/src/server/operations/registry.ts +2 -3
- package/src/server/operations/types.ts +7 -5
- package/src/server/operations/webview-runner.ts +1 -3
- package/src/server/placement.ts +8 -18
- package/src/server/server.ts +121 -1012
- package/src/server/spatial-analysis.ts +39 -25
- package/src/server/trace-manager.ts +3 -8
- package/src/server/web-artifacts.ts +23 -27
- package/src/server/webpage-node.ts +5 -13
- package/src/shared/auto-arrange.ts +12 -5
- package/src/shared/ax-intent.ts +1 -1
- package/src/shared/content-height-reporter.ts +8 -6
- package/src/shared/ext-app-tool-result.ts +2 -6
- package/src/shared/placement.ts +1 -4
- package/src/shared/semantic-attention.ts +39 -37
- package/src/shared/surface.ts +8 -4
|
@@ -82,8 +82,8 @@ interface EdgePathProps {
|
|
|
82
82
|
edge: CanvasEdge;
|
|
83
83
|
fromNode: CanvasNodeState;
|
|
84
84
|
toNode: CanvasNodeState;
|
|
85
|
-
focused: boolean;
|
|
86
|
-
dimmed: boolean;
|
|
85
|
+
focused: boolean; // connected to the active node
|
|
86
|
+
dimmed: boolean; // active node exists but this edge is NOT connected
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
function EdgePath({ edge, fromNode, toNode, focused, dimmed }: EdgePathProps) {
|
|
@@ -108,22 +108,14 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed }: EdgePathProps) {
|
|
|
108
108
|
const directed = DIRECTED_TYPES.has(edge.type);
|
|
109
109
|
const dash = dashArray(edge);
|
|
110
110
|
|
|
111
|
-
const mid = edge.label
|
|
112
|
-
? bezierMidpoint(start.x, start.y, cx1, cy1, cx2, cy2, end.x, end.y)
|
|
113
|
-
: null;
|
|
111
|
+
const mid = edge.label ? bezierMidpoint(start.x, start.y, cx1, cy1, cx2, cy2, end.x, end.y) : null;
|
|
114
112
|
|
|
115
113
|
const pathId = `edge-path-${edge.id}`;
|
|
116
114
|
|
|
117
115
|
return (
|
|
118
116
|
<g>
|
|
119
117
|
{/* 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
|
-
/>
|
|
118
|
+
<path d={d} fill="none" stroke="transparent" stroke-width="12" style={{ cursor: 'pointer' }} />
|
|
127
119
|
|
|
128
120
|
{/* Glow layer for focused edges */}
|
|
129
121
|
{focused && (
|
|
@@ -171,13 +163,7 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed }: EdgePathProps) {
|
|
|
171
163
|
height="20"
|
|
172
164
|
rx="4"
|
|
173
165
|
/>
|
|
174
|
-
<text
|
|
175
|
-
class="edge-label"
|
|
176
|
-
text-anchor="middle"
|
|
177
|
-
dominant-baseline="central"
|
|
178
|
-
fill="var(--c-text)"
|
|
179
|
-
font-size="11"
|
|
180
|
-
>
|
|
166
|
+
<text class="edge-label" text-anchor="middle" dominant-baseline="central" fill="var(--c-text)" font-size="11">
|
|
181
167
|
{edge.label}
|
|
182
168
|
</text>
|
|
183
169
|
</g>
|
|
@@ -258,23 +244,38 @@ export function EdgeLayer({ nodes, edges }: EdgeLayerProps) {
|
|
|
258
244
|
);
|
|
259
245
|
})}
|
|
260
246
|
{/* Live preview edge while drag-connecting */}
|
|
261
|
-
{draggingEdge.value &&
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
247
|
+
{draggingEdge.value &&
|
|
248
|
+
(() => {
|
|
249
|
+
const de = draggingEdge.value;
|
|
250
|
+
const dx = de.cursorX - de.fromX;
|
|
251
|
+
const dy = de.cursorY - de.fromY;
|
|
252
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
253
|
+
const curve = Math.min(dist * 0.25, 80);
|
|
254
|
+
const nx = dx / (dist || 1);
|
|
255
|
+
const ny = dy / (dist || 1);
|
|
256
|
+
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}`;
|
|
257
|
+
return (
|
|
258
|
+
<g>
|
|
259
|
+
<path
|
|
260
|
+
d={previewD}
|
|
261
|
+
fill="none"
|
|
262
|
+
stroke="var(--c-accent)"
|
|
263
|
+
stroke-width="6"
|
|
264
|
+
opacity="0.1"
|
|
265
|
+
style={{ filter: 'blur(3px)' }}
|
|
266
|
+
/>
|
|
267
|
+
<path
|
|
268
|
+
d={previewD}
|
|
269
|
+
fill="none"
|
|
270
|
+
stroke="var(--c-accent)"
|
|
271
|
+
stroke-width="2"
|
|
272
|
+
stroke-dasharray="6 4"
|
|
273
|
+
opacity="0.8"
|
|
274
|
+
/>
|
|
275
|
+
<circle cx={de.cursorX} cy={de.cursorY} r="5" fill="var(--c-accent)" opacity="0.5" />
|
|
276
|
+
</g>
|
|
277
|
+
);
|
|
278
|
+
})()}
|
|
278
279
|
</svg>
|
|
279
280
|
);
|
|
280
281
|
}
|
|
@@ -83,15 +83,25 @@ function wordCount(text: string): number {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function isPresentationExitMessage(value: unknown, token: string): boolean {
|
|
86
|
-
return
|
|
86
|
+
return (
|
|
87
|
+
value !== null &&
|
|
87
88
|
typeof value === 'object' &&
|
|
88
89
|
(value as { source?: unknown }).source === 'pmx-canvas-html-node' &&
|
|
89
90
|
(value as { type?: unknown }).type === 'presentation-exit' &&
|
|
90
|
-
(value as { token?: unknown }).token === token
|
|
91
|
+
(value as { token?: unknown }).token === token
|
|
92
|
+
);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
function isPresentationNavigationKey(key: string): boolean {
|
|
94
|
-
return
|
|
96
|
+
return (
|
|
97
|
+
key === 'ArrowRight' ||
|
|
98
|
+
key === 'PageDown' ||
|
|
99
|
+
key === ' ' ||
|
|
100
|
+
key === 'ArrowLeft' ||
|
|
101
|
+
key === 'PageUp' ||
|
|
102
|
+
key === 'Home' ||
|
|
103
|
+
key === 'End'
|
|
104
|
+
);
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
function isPresentationExitButtonTarget(target: EventTarget | null): boolean {
|
|
@@ -117,38 +127,49 @@ export function ExpandedNodeOverlay() {
|
|
|
117
127
|
setPresenting(true);
|
|
118
128
|
}, []);
|
|
119
129
|
|
|
120
|
-
const postPresentationMessage = useCallback(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
const postPresentationMessage = useCallback(
|
|
131
|
+
(message: Record<string, unknown>) => {
|
|
132
|
+
const frame = document.querySelector<HTMLIFrameElement>(
|
|
133
|
+
'.html-presentation-overlay iframe.html-node-frame-presentation',
|
|
134
|
+
);
|
|
135
|
+
frame?.contentWindow?.postMessage(
|
|
136
|
+
{
|
|
137
|
+
source: 'pmx-canvas-html-node',
|
|
138
|
+
token: presentationExitToken,
|
|
139
|
+
...message,
|
|
140
|
+
},
|
|
141
|
+
'*',
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
[presentationExitToken],
|
|
145
|
+
);
|
|
128
146
|
|
|
129
147
|
const handleExitPresentation = useCallback(() => {
|
|
130
148
|
setPresenting(false);
|
|
131
149
|
}, []);
|
|
132
150
|
|
|
133
|
-
const handlePresentationKeyDown = useCallback(
|
|
134
|
-
|
|
135
|
-
event.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
const handlePresentationKeyDown = useCallback(
|
|
152
|
+
(event: KeyboardEvent) => {
|
|
153
|
+
if (event.key === 'Escape') {
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
event.stopPropagation();
|
|
156
|
+
setPresenting(false);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (event.key === 'Tab' && !isPresentationExitButtonTarget(event.target)) {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
event.stopPropagation();
|
|
162
|
+
presentationExitButtonRef.current?.focus();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if ((event.key === ' ' || event.key === 'Enter') && isPresentationExitButtonTarget(event.target)) return;
|
|
166
|
+
if (!isPresentationNavigationKey(event.key)) return;
|
|
141
167
|
event.preventDefault();
|
|
142
168
|
event.stopPropagation();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (!isPresentationNavigationKey(event.key)) return;
|
|
148
|
-
event.preventDefault();
|
|
149
|
-
event.stopPropagation();
|
|
150
|
-
postPresentationMessage({ type: 'presentation-key', key: event.key });
|
|
151
|
-
}, [postPresentationMessage]);
|
|
169
|
+
postPresentationMessage({ type: 'presentation-key', key: event.key });
|
|
170
|
+
},
|
|
171
|
+
[postPresentationMessage],
|
|
172
|
+
);
|
|
152
173
|
|
|
153
174
|
const handleBackdropPointerDown = useCallback((e: PointerEvent) => {
|
|
154
175
|
if ((e.target as HTMLElement).classList.contains('expanded-overlay-backdrop')) {
|
|
@@ -190,7 +211,9 @@ export function ExpandedNodeOverlay() {
|
|
|
190
211
|
document.addEventListener('keydown', handlePresentationKeyDown, true);
|
|
191
212
|
window.addEventListener('message', handleMessage);
|
|
192
213
|
return () => {
|
|
193
|
-
focusTimers.forEach((timer) =>
|
|
214
|
+
focusTimers.forEach((timer) => {
|
|
215
|
+
window.clearTimeout(timer);
|
|
216
|
+
});
|
|
194
217
|
document.removeEventListener('keydown', handlePresentationKeyDown, true);
|
|
195
218
|
window.removeEventListener('message', handleMessage);
|
|
196
219
|
};
|
|
@@ -198,16 +221,14 @@ export function ExpandedNodeOverlay() {
|
|
|
198
221
|
|
|
199
222
|
if (!node) return null;
|
|
200
223
|
|
|
201
|
-
const title =
|
|
202
|
-
(node.data.title as string) ||
|
|
203
|
-
(node.data.path as string)?.split('/').pop() ||
|
|
204
|
-
TYPE_LABELS[node.type];
|
|
224
|
+
const title = (node.data.title as string) || (node.data.path as string)?.split('/').pop() || TYPE_LABELS[node.type];
|
|
205
225
|
const textContent = getNodeTextContent(node);
|
|
206
226
|
const words = wordCount(textContent);
|
|
207
227
|
const isCtxPinned = nodeId ? contextPinnedNodeIds.value.has(nodeId) : false;
|
|
208
228
|
const hasText = textContent.length > 0;
|
|
209
229
|
const pendingClose = pendingExpandedNodeCloseId.value === nodeId;
|
|
210
|
-
const isEmbeddedViewer =
|
|
230
|
+
const isEmbeddedViewer =
|
|
231
|
+
node.type === 'mcp-app' || node.type === 'webpage' || node.type === 'json-render' || node.type === 'graph';
|
|
211
232
|
const canPresent = shouldShowPresentationControls(node);
|
|
212
233
|
|
|
213
234
|
return (
|
|
@@ -294,12 +315,7 @@ export function ExpandedNodeOverlay() {
|
|
|
294
315
|
|
|
295
316
|
{/* Copy content */}
|
|
296
317
|
{hasText && (
|
|
297
|
-
<button
|
|
298
|
-
type="button"
|
|
299
|
-
class="expanded-action-btn"
|
|
300
|
-
onClick={handleCopy}
|
|
301
|
-
title="Copy content to clipboard"
|
|
302
|
-
>
|
|
318
|
+
<button type="button" class="expanded-action-btn" onClick={handleCopy} title="Copy content to clipboard">
|
|
303
319
|
{copied ? 'Copied!' : 'Copy'}
|
|
304
320
|
</button>
|
|
305
321
|
)}
|
|
@@ -373,13 +389,21 @@ export function ExpandedNodeOverlay() {
|
|
|
373
389
|
}}
|
|
374
390
|
>
|
|
375
391
|
{isEmbeddedViewer ? (
|
|
376
|
-
<div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
)
|
|
392
|
+
<div style={{ flex: 1, minHeight: 0, display: 'flex' }}>{renderContent(node, true)}</div>
|
|
393
|
+
) : (
|
|
394
|
+
renderContent(node, true)
|
|
395
|
+
)}
|
|
380
396
|
</div>
|
|
381
397
|
{canPresent && presenting && (
|
|
382
|
-
<div
|
|
398
|
+
<div
|
|
399
|
+
ref={presentationOverlayRef}
|
|
400
|
+
class="html-presentation-overlay"
|
|
401
|
+
role="dialog"
|
|
402
|
+
aria-modal="true"
|
|
403
|
+
aria-label={`Present ${title}`}
|
|
404
|
+
tabIndex={-1}
|
|
405
|
+
onKeyDownCapture={handlePresentationKeyDown}
|
|
406
|
+
>
|
|
383
407
|
<button
|
|
384
408
|
ref={presentationExitButtonRef}
|
|
385
409
|
type="button"
|
|
@@ -20,9 +20,7 @@ function getNodeRect(nodeId: string): Rect | null {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function getRegionRect(nodeIds: string[]): Rect | null {
|
|
23
|
-
const rects = nodeIds
|
|
24
|
-
.map((nodeId) => getNodeRect(nodeId))
|
|
25
|
-
.filter((rect): rect is Rect => rect !== null);
|
|
23
|
+
const rects = nodeIds.map((nodeId) => getNodeRect(nodeId)).filter((rect): rect is Rect => rect !== null);
|
|
26
24
|
if (rects.length === 0) return null;
|
|
27
25
|
|
|
28
26
|
const minLeft = Math.min(...rects.map((rect) => rect.left));
|
|
@@ -62,13 +60,7 @@ export function FocusFieldLayer() {
|
|
|
62
60
|
{regions.map((region) => {
|
|
63
61
|
const rect = getRegionRect(region.nodeIds);
|
|
64
62
|
if (!rect) return null;
|
|
65
|
-
return (
|
|
66
|
-
<div
|
|
67
|
-
key={region.id}
|
|
68
|
-
class="attention-field-region"
|
|
69
|
-
style={rectStyle(rect, 42)}
|
|
70
|
-
/>
|
|
71
|
-
);
|
|
63
|
+
return <div key={region.id} class="attention-field-region" style={rectStyle(rect, 42)} />;
|
|
72
64
|
})}
|
|
73
65
|
{secondaryNodeIds.map((nodeId) => {
|
|
74
66
|
const rect = getNodeRect(nodeId);
|
|
@@ -77,12 +69,15 @@ export function FocusFieldLayer() {
|
|
|
77
69
|
<div
|
|
78
70
|
key={`secondary-${nodeId}`}
|
|
79
71
|
class="attention-field-node attention-field-secondary"
|
|
80
|
-
style={rectStyle(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
72
|
+
style={rectStyle(
|
|
73
|
+
{
|
|
74
|
+
left: rect.left - 18,
|
|
75
|
+
top: rect.top - 18,
|
|
76
|
+
width: rect.width + 36,
|
|
77
|
+
height: rect.height + 36,
|
|
78
|
+
},
|
|
79
|
+
28,
|
|
80
|
+
)}
|
|
86
81
|
/>
|
|
87
82
|
);
|
|
88
83
|
})}
|
|
@@ -93,12 +88,15 @@ export function FocusFieldLayer() {
|
|
|
93
88
|
<div
|
|
94
89
|
key={`primary-${nodeId}`}
|
|
95
90
|
class="attention-field-node attention-field-primary"
|
|
96
|
-
style={rectStyle(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
style={rectStyle(
|
|
92
|
+
{
|
|
93
|
+
left: rect.left - 24,
|
|
94
|
+
top: rect.top - 24,
|
|
95
|
+
width: rect.width + 48,
|
|
96
|
+
height: rect.height + 48,
|
|
97
|
+
},
|
|
98
|
+
30,
|
|
99
|
+
)}
|
|
102
100
|
/>
|
|
103
101
|
);
|
|
104
102
|
})}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { useEffect } from 'preact/hooks';
|
|
2
2
|
import { nodes } from '../state/canvas-store';
|
|
3
|
-
import {
|
|
4
|
-
hoveredIntentId,
|
|
5
|
-
intents,
|
|
6
|
-
type ClientIntent,
|
|
7
|
-
} from '../state/intent-store';
|
|
3
|
+
import { hoveredIntentId, intents, type ClientIntent } from '../state/intent-store';
|
|
8
4
|
import { vetoGhostIntent } from '../state/intent-bridge';
|
|
9
5
|
import { getNodeIcon } from '../icons';
|
|
10
6
|
import { TYPE_LABELS } from '../types';
|
|
@@ -75,8 +71,7 @@ function bezierPath(a: { x: number; y: number }, b: { x: number; y: number }): s
|
|
|
75
71
|
function GhostInfo({ intent }: { intent: ClientIntent }) {
|
|
76
72
|
const NodeIcon = isKnownNodeType(intent.nodeType) ? getNodeIcon(intent.nodeType) : null;
|
|
77
73
|
const label = intent.label || intent.kind;
|
|
78
|
-
const confidencePct =
|
|
79
|
-
typeof intent.confidence === 'number' ? `${Math.round(intent.confidence * 100)}%` : null;
|
|
74
|
+
const confidencePct = typeof intent.confidence === 'number' ? `${Math.round(intent.confidence * 100)}%` : null;
|
|
80
75
|
return (
|
|
81
76
|
<div
|
|
82
77
|
class="intent-info"
|
|
@@ -160,9 +155,7 @@ function GhostOverlay({ intent, rect, variant }: { intent: ClientIntent; rect: R
|
|
|
160
155
|
}
|
|
161
156
|
|
|
162
157
|
function renderGhost(intent: ClientIntent) {
|
|
163
|
-
const settledRect = intent.phase === 'settling'
|
|
164
|
-
? getNodeRect(intent.settledNodeId)
|
|
165
|
-
: null;
|
|
158
|
+
const settledRect = intent.phase === 'settling' ? getNodeRect(intent.settledNodeId) : null;
|
|
166
159
|
switch (intent.kind) {
|
|
167
160
|
case 'create': {
|
|
168
161
|
if (!intent.position) return null;
|
|
@@ -174,7 +167,12 @@ function renderGhost(intent: ClientIntent) {
|
|
|
174
167
|
if (!intent.position) return null;
|
|
175
168
|
const source = getNodeRect(intent.nodeId);
|
|
176
169
|
const size = source ?? DEFAULT_GHOST_SIZE;
|
|
177
|
-
const rect: Rect = settledRect ?? {
|
|
170
|
+
const rect: Rect = settledRect ?? {
|
|
171
|
+
left: intent.position.x,
|
|
172
|
+
top: intent.position.y,
|
|
173
|
+
width: size.width,
|
|
174
|
+
height: size.height,
|
|
175
|
+
};
|
|
178
176
|
return <GhostBox key={intent.id} intent={intent} rect={rect} />;
|
|
179
177
|
}
|
|
180
178
|
case 'remove': {
|
|
@@ -192,7 +190,10 @@ function renderGhost(intent: ClientIntent) {
|
|
|
192
190
|
const from = getNodeRect(intent.edge.from);
|
|
193
191
|
const to = getNodeRect(intent.edge.to);
|
|
194
192
|
if (!from || !to) return null;
|
|
195
|
-
const mid = {
|
|
193
|
+
const mid = {
|
|
194
|
+
x: (from.left + from.width / 2 + to.left + to.width / 2) / 2,
|
|
195
|
+
y: (from.top + from.height / 2 + to.top + to.height / 2) / 2,
|
|
196
|
+
};
|
|
196
197
|
const rect: Rect = { left: mid.x - 110, top: mid.y - 18, width: 220, height: 36 };
|
|
197
198
|
// The bezier itself is drawn in the shared SVG layer; here we anchor the info card.
|
|
198
199
|
return (
|
|
@@ -200,7 +201,12 @@ function renderGhost(intent: ClientIntent) {
|
|
|
200
201
|
key={intent.id}
|
|
201
202
|
class={`intent-ghost intent-ghost-connect is-${intent.phase}`}
|
|
202
203
|
data-intent-id={intent.id}
|
|
203
|
-
style={{
|
|
204
|
+
style={{
|
|
205
|
+
left: `${rect.left}px`,
|
|
206
|
+
top: `${rect.top}px`,
|
|
207
|
+
width: `${rect.width}px`,
|
|
208
|
+
opacity: ghostOpacity(intent),
|
|
209
|
+
}}
|
|
204
210
|
>
|
|
205
211
|
<GhostInfo intent={intent} />
|
|
206
212
|
</div>
|
|
@@ -234,7 +240,18 @@ export function IntentLayer() {
|
|
|
234
240
|
|
|
235
241
|
return (
|
|
236
242
|
<div class="intent-layer">
|
|
237
|
-
<svg
|
|
243
|
+
<svg
|
|
244
|
+
class="intent-line-layer"
|
|
245
|
+
style={{
|
|
246
|
+
position: 'absolute',
|
|
247
|
+
top: 0,
|
|
248
|
+
left: 0,
|
|
249
|
+
width: '100%',
|
|
250
|
+
height: '100%',
|
|
251
|
+
overflow: 'visible',
|
|
252
|
+
pointerEvents: 'none',
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
238
255
|
<defs>
|
|
239
256
|
<marker id="intent-arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
|
240
257
|
<path d="M0,0 L6,3 L0,6 Z" class="intent-arrow-head" />
|
|
@@ -108,14 +108,7 @@ interface MinimapProps {
|
|
|
108
108
|
containerHeight: number;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export function Minimap({
|
|
112
|
-
viewport,
|
|
113
|
-
nodes,
|
|
114
|
-
edges,
|
|
115
|
-
onNavigate,
|
|
116
|
-
containerWidth,
|
|
117
|
-
containerHeight,
|
|
118
|
-
}: MinimapProps) {
|
|
111
|
+
export function Minimap({ viewport, nodes, edges, onNavigate, containerWidth, containerHeight }: MinimapProps) {
|
|
119
112
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
120
113
|
const isDragging = useRef(false);
|
|
121
114
|
const frameRef = useRef<MinimapFrame | null>(null);
|
|
@@ -216,12 +209,15 @@ export function Minimap({
|
|
|
216
209
|
scheduleDraw();
|
|
217
210
|
}, [containerWidth, containerHeight, scheduleDraw]);
|
|
218
211
|
|
|
219
|
-
useEffect(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
212
|
+
useEffect(
|
|
213
|
+
() => () => {
|
|
214
|
+
if (drawRafId.current !== null) {
|
|
215
|
+
window.cancelAnimationFrame(drawRafId.current);
|
|
216
|
+
drawRafId.current = null;
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
[],
|
|
220
|
+
);
|
|
225
221
|
|
|
226
222
|
const handleNavigateFromEvent = useCallback(
|
|
227
223
|
(e: MouseEvent | PointerEvent) => {
|
|
@@ -233,8 +229,7 @@ export function Minimap({
|
|
|
233
229
|
const my = e.clientY - rect.top;
|
|
234
230
|
|
|
235
231
|
const frame =
|
|
236
|
-
frameRef.current
|
|
237
|
-
?? computeMinimapFrame(nodes.value, viewport.value, containerWidth, containerHeight);
|
|
232
|
+
frameRef.current ?? computeMinimapFrame(nodes.value, viewport.value, containerWidth, containerHeight);
|
|
238
233
|
frameRef.current = frame;
|
|
239
234
|
const { bounds, scale } = frame;
|
|
240
235
|
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { useCallback } from 'preact/hooks';
|
|
2
|
-
import {
|
|
3
|
-
addContextPins,
|
|
4
|
-
clearSelection,
|
|
5
|
-
selectedNodeIds,
|
|
6
|
-
} from '../state/canvas-store';
|
|
2
|
+
import { addContextPins, clearSelection, selectedNodeIds } from '../state/canvas-store';
|
|
7
3
|
import { createEdgeFromClient, createGroupFromClient } from '../state/intent-bridge';
|
|
8
4
|
|
|
9
5
|
export function SelectionBar() {
|
|
10
6
|
const count = selectedNodeIds.value.size;
|
|
11
|
-
if (count === 0) return null;
|
|
12
7
|
|
|
13
8
|
const handlePinContext = useCallback(() => {
|
|
14
9
|
const ids = Array.from(selectedNodeIds.value);
|
|
@@ -34,16 +29,14 @@ export function SelectionBar() {
|
|
|
34
29
|
clearSelection();
|
|
35
30
|
}, []);
|
|
36
31
|
|
|
32
|
+
if (count === 0) return null;
|
|
33
|
+
|
|
37
34
|
return (
|
|
38
35
|
<div class="selection-bar">
|
|
39
36
|
<span class="selection-bar-count">
|
|
40
37
|
{'\u2726'} {count} node{count !== 1 ? 's' : ''} selected
|
|
41
38
|
</span>
|
|
42
|
-
<button
|
|
43
|
-
type="button"
|
|
44
|
-
class="selection-bar-btn selection-bar-pin-ctx"
|
|
45
|
-
onClick={handlePinContext}
|
|
46
|
-
>
|
|
39
|
+
<button type="button" class="selection-bar-btn selection-bar-pin-ctx" onClick={handlePinContext}>
|
|
47
40
|
Pin as context
|
|
48
41
|
</button>
|
|
49
42
|
{count >= 2 && (
|
|
@@ -48,7 +48,9 @@ export function ShortcutOverlay({ onClose }: { onClose: () => void }) {
|
|
|
48
48
|
<div class="shortcut-overlay" onMouseDown={(e) => e.stopPropagation()}>
|
|
49
49
|
<div class="shortcut-overlay-header">
|
|
50
50
|
<span class="shortcut-overlay-title">Keyboard Shortcuts</span>
|
|
51
|
-
<span class="shortcut-overlay-hint">
|
|
51
|
+
<span class="shortcut-overlay-hint">
|
|
52
|
+
Press <kbd>?</kbd> or <kbd>Esc</kbd> to close
|
|
53
|
+
</span>
|
|
52
54
|
</div>
|
|
53
55
|
<div class="shortcut-overlay-body">
|
|
54
56
|
{GROUPS.map((group) => (
|