pmx-canvas 0.10.0 → 0.11.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.
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +246 -108
- package/CHANGELOG.md +32 -0
- package/dist/canvas/chunks/abnfDiagram-N423BO3Z-p8dtp6va.js +1 -0
- package/dist/canvas/chunks/architecture-TIHT7OUA-9tffvpsv.js +1 -0
- package/dist/canvas/chunks/architectureDiagram-T3A2C74G-5jhmxrzf.js +36 -0
- package/dist/canvas/chunks/blockDiagram-VBNYF7ZC-pw1fa9yb.js +132 -0
- package/dist/canvas/chunks/c4Diagram-5PPSVZJV-x07nq0tt.js +10 -0
- package/dist/canvas/chunks/classDiagram-JCYQIIEL-nc6g5x1r.js +1 -0
- package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-0qnbpwvh.js +1 -0
- package/dist/canvas/chunks/cose-bilkent-JH36ORCC-nm39npq0.js +1 -0
- package/dist/canvas/chunks/cynefin-VYW2F7L2-ys8p46ts.js +1 -0
- package/dist/canvas/chunks/cynefinDiagram-MW4NZA55-vdxrwahz.js +62 -0
- package/dist/canvas/chunks/dagre-VZM6K2ZE-bqdm7wpd.js +4 -0
- package/dist/canvas/chunks/diagram-7IWD3JNH-gs5gpr93.js +30 -0
- package/dist/canvas/chunks/diagram-B4RE2ZJO-cgwjg47s.js +3 -0
- package/dist/canvas/chunks/diagram-LBJQPF4R-w3vahpav.js +24 -0
- package/dist/canvas/chunks/diagram-Q27KOJAE-42837z6h.js +24 -0
- package/dist/canvas/chunks/diagram-UB23O5K3-4fz83q59.js +41 -0
- package/dist/canvas/chunks/ebnfDiagram-BXEA7PRR-zphtgegj.js +1 -0
- package/dist/canvas/chunks/erDiagram-JOGREHBK-zvbbdxp4.js +85 -0
- package/dist/canvas/chunks/eventmodeling-45OFAUF4-v9q6dn4c.js +1 -0
- package/dist/canvas/chunks/flowDiagram-UKHOOZJN-yrr5z8tc.js +1 -0
- package/dist/canvas/chunks/ganttDiagram-PKOTCBZU-282jzycm.js +292 -0
- package/dist/canvas/chunks/gitGraph-TEB2WS4Q-kdwb8m0a.js +1 -0
- package/dist/canvas/chunks/gitGraphDiagram-DS77QQ5N-vqqy6p1b.js +106 -0
- package/dist/canvas/chunks/index-0rk4gp1g.js +2 -0
- package/dist/canvas/chunks/index-194m3hc3.js +2 -0
- package/dist/canvas/chunks/index-1r72hdzb.js +301 -0
- package/dist/canvas/chunks/index-353m7b0p.js +2 -0
- package/dist/canvas/chunks/index-3cy1bzwh.js +2 -0
- package/dist/canvas/chunks/index-5tv8v1hk.js +1 -0
- package/dist/canvas/chunks/index-6ceya556.js +332 -0
- package/dist/canvas/chunks/index-6qmsnxwy.js +4 -0
- package/dist/canvas/chunks/index-d3b3gqe9.js +2 -0
- package/dist/canvas/chunks/index-fjtdggjn.js +2 -0
- package/dist/canvas/chunks/index-g3pwnqeb.js +2 -0
- package/dist/canvas/chunks/index-gf4e0et4.js +2 -0
- package/dist/canvas/chunks/index-hj99nz1f.js +1 -0
- package/dist/canvas/chunks/index-nprm96xp.js +16 -0
- package/dist/canvas/chunks/index-qj8fxwsf.js +2 -0
- package/dist/canvas/chunks/index-r2fyc9w1.js +89 -0
- package/dist/canvas/chunks/index-rft2hcz5.js +2 -0
- package/dist/canvas/chunks/index-rh7nv68n.js +2 -0
- package/dist/canvas/chunks/index-s4j678gj.js +2 -0
- package/dist/canvas/chunks/index-tb1wf525.js +179 -0
- package/dist/canvas/chunks/index-vv56h019.js +2 -0
- package/dist/canvas/chunks/index-w2mra17t.js +207 -0
- package/dist/canvas/chunks/index-wgv7en21.js +232 -0
- package/dist/canvas/chunks/index-yw8h9vha.js +157 -0
- package/dist/canvas/chunks/info-DKCQHKI2-wpzs6sz3.js +1 -0
- package/dist/canvas/chunks/infoDiagram-6WML65LV-v1yvggqj.js +2 -0
- package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-5hnq1a9p.js +70 -0
- package/dist/canvas/chunks/journeyDiagram-NVQOT4AX-zjcffveh.js +139 -0
- package/dist/canvas/chunks/kanban-definition-27J2QSJJ-dakqtfp1.js +89 -0
- package/dist/canvas/chunks/katex-dmg8k4zm.js +257 -0
- package/dist/canvas/chunks/mermaid.core-j2geswrd.js +12 -0
- package/dist/canvas/chunks/mindmap-definition-FAOFIHXS-142xhr3k.js +96 -0
- package/dist/canvas/chunks/packet-7NZHBO7P-xn439f0s.js +1 -0
- package/dist/canvas/chunks/pegDiagram-VL7TDLO6-5wvk7vsd.js +1 -0
- package/dist/canvas/chunks/pie-RZYD4A2V-0kf16rwh.js +1 -0
- package/dist/canvas/chunks/pieDiagram-7S7Q4E2Y-ztdhwycc.js +39 -0
- package/dist/canvas/chunks/quadrantDiagram-CIZ2JOQS-c5p9gmg6.js +7 -0
- package/dist/canvas/chunks/radar-I7S5WNFK-0g2pz36t.js +1 -0
- package/dist/canvas/chunks/railroad-3IZDKUUU-fcaran6j.js +1 -0
- package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-wgdya41q.js +1 -0
- package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-gy07a4ka.js +1 -0
- package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-ewk6wtkh.js +1 -0
- package/dist/canvas/chunks/railroadDiagram-AXF67PYL-xkmqkjwf.js +1 -0
- package/dist/canvas/chunks/requirementDiagram-LRYGKXZP-9wm2dyaw.js +84 -0
- package/dist/canvas/chunks/sankeyDiagram-W5VNT64P-2w19etgb.js +40 -0
- package/dist/canvas/chunks/sequenceDiagram-SI44F4Z6-0jn0vwmz.js +162 -0
- package/dist/canvas/chunks/sizeCapture-X5ZJPWSS-x75epwdq.js +1 -0
- package/dist/canvas/chunks/stateDiagram-OKZ733FA-schjpy6d.js +1 -0
- package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-k8sfp2e7.js +1 -0
- package/dist/canvas/chunks/swimlanes-SLNWSIFB-gkw4xce3.js +1 -0
- package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-nqysw67j.js +8 -0
- package/dist/canvas/chunks/timeline-definition-Z64GVDOM-x7nkcy01.js +120 -0
- package/dist/canvas/chunks/treeView-QDETBFTQ-cesw6gp0.js +1 -0
- package/dist/canvas/chunks/treemap-6X3UGDF4-zdv4knp9.js +1 -0
- package/dist/canvas/chunks/vennDiagram-T6HMQDX7-x8dqn9yf.js +34 -0
- package/dist/canvas/chunks/wardley-OPB4EBWU-44pdddez.js +1 -0
- package/dist/canvas/chunks/wardleyDiagram-T6FBY63Y-t1j52t9r.js +78 -0
- package/dist/canvas/chunks/xychartDiagram-ELKLHX3M-38t16pme.js +7 -0
- package/dist/canvas/global.css +389 -229
- package/dist/canvas/index.js +141 -211
- package/dist/canvas/mcp-app.html +4205 -554
- package/dist/canvas/mermaid-entry.js +338 -338
- package/dist/canvas/surface-theme.css +6 -6
- package/dist/json-render/index.css +1 -1
- package/dist/json-render/index.js +49 -49
- package/dist/types/client/canvas/BarHint.d.ts +5 -3
- package/dist/types/client/canvas/CanvasNode.d.ts +0 -11
- package/dist/types/client/canvas/EdgeLayer.d.ts +1 -2
- package/dist/types/client/canvas/auto-fit.d.ts +4 -5
- package/dist/types/client/mermaid-entry.d.ts +1 -2
- package/dist/types/client/mermaid-render.d.ts +1 -0
- package/dist/types/client/nodes/BoardNode.d.ts +1 -1
- package/dist/types/client/nodes/ExtAppFrame.d.ts +7 -90
- package/dist/types/client/nodes/McpAppNode.d.ts +1 -25
- package/dist/types/client/nodes/MermaidNode.d.ts +1 -8
- package/dist/types/client/nodes/ViewerFrame.d.ts +29 -0
- package/dist/types/client/nodes/ViewerHost.d.ts +6 -0
- package/dist/types/client/nodes/iframe-document-url.d.ts +3 -9
- package/dist/types/client/nodes/primitive-copy.d.ts +5 -0
- package/dist/types/client/nodes/use-iframe-content-height.d.ts +1 -1
- package/dist/types/client/nodes/use-surface-frame.d.ts +4 -6
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +2 -1
- package/dist/types/client/state/canvas-store.d.ts +3 -1
- package/dist/types/client/state/intent-bridge.d.ts +1 -0
- package/dist/types/client/state/presentation.d.ts +17 -6
- package/dist/types/json-render/server.d.ts +2 -1
- package/dist/types/server/canvas-state.d.ts +2 -0
- package/dist/types/server/index.d.ts +10 -5
- package/dist/types/server/operations/ops/boards.d.ts +0 -1
- package/dist/types/server/tour-control.d.ts +13 -3
- package/dist/types/shared/ax-surface-protocol.d.ts +0 -9
- package/dist/types/shared/tour.d.ts +8 -0
- package/docs/design/Chrome.dc.html +32 -6
- package/docs/design/Types1.dc.html +3 -2
- package/docs/design/Types2.dc.html +1 -1
- package/docs/design/Types3.dc.html +1 -1
- package/docs/design/Zoom.dc.html +37 -60
- package/docs/design/canvas.json +1 -1
- package/docs/design.md +169 -4
- package/docs/http-api.md +51 -1
- package/docs/mcp.md +8 -0
- package/docs/node-types.md +38 -3
- package/docs/plans/plan-009-tech-debt-backlog.md +2 -2
- package/docs/plans/plan-019-post-0.9-roadmap.md +283 -11
- package/docs/product-vision-2026-09.md +9 -0
- package/package.json +2 -2
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +1 -1
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/tools.json +246 -108
- package/skills/pmx-canvas/SKILL.md +11 -18
- package/skills/pmx-canvas/references/full-reference.md +9 -4
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +3 -5
- package/src/client/canvas/BarHint.tsx +6 -3
- package/src/client/canvas/CanvasNode.tsx +34 -47
- package/src/client/canvas/CanvasViewport.tsx +6 -7
- package/src/client/canvas/ContextMenu.tsx +63 -3
- package/src/client/canvas/EdgeLayer.tsx +365 -169
- package/src/client/canvas/ExpandedNodeOverlay.tsx +1 -2
- package/src/client/canvas/Presentation.tsx +53 -3
- package/src/client/canvas/TopBar.tsx +24 -9
- package/src/client/canvas/auto-fit.ts +9 -11
- package/src/client/canvas/canvas-area.ts +1 -1
- package/src/client/canvas/use-focus-trap.ts +6 -1
- package/src/client/mermaid-entry.ts +6 -20
- package/src/client/mermaid-render.ts +39 -0
- package/src/client/nodes/AxStepControls.tsx +45 -69
- package/src/client/nodes/BoardNode.tsx +65 -9
- package/src/client/nodes/ContextNode.tsx +88 -396
- package/src/client/nodes/ExtAppFrame.tsx +283 -1462
- package/src/client/nodes/FileNode.tsx +92 -81
- package/src/client/nodes/HtmlNode.tsx +8 -36
- package/src/client/nodes/LedgerNode.tsx +31 -17
- package/src/client/nodes/MarkdownNode.tsx +8 -17
- package/src/client/nodes/McpAppNode.tsx +48 -154
- package/src/client/nodes/MermaidNode.tsx +80 -69
- package/src/client/nodes/PromptNode.tsx +12 -14
- package/src/client/nodes/ResponseNode.tsx +8 -6
- package/src/client/nodes/StatusNode.tsx +22 -7
- package/src/client/nodes/StatusSummary.tsx +5 -3
- package/src/client/nodes/TraceNode.tsx +8 -8
- package/src/client/nodes/ViewerFrame.tsx +262 -0
- package/src/client/nodes/ViewerHost.tsx +41 -0
- package/src/client/nodes/WebpageNode.tsx +22 -24
- package/src/client/nodes/iframe-document-url.ts +20 -27
- package/src/client/nodes/primitive-copy.ts +97 -0
- package/src/client/nodes/use-iframe-content-height.ts +34 -10
- package/src/client/nodes/use-surface-frame.ts +22 -11
- package/src/client/nodes/viewer-runtime-state.ts +41 -35
- package/src/client/state/canvas-store.ts +18 -12
- package/src/client/state/intent-bridge.ts +1 -0
- package/src/client/state/presentation.ts +135 -15
- package/src/client/state/sse-bridge.ts +18 -19
- package/src/client/theme/global.css +389 -229
- package/src/client/theme/surface-theme.css +6 -6
- package/src/json-render/charts/tufte-components.tsx +44 -2
- package/src/json-render/renderer/index.css +62 -2
- package/src/json-render/renderer/index.tsx +31 -7
- package/src/json-render/server.ts +3 -2
- package/src/server/board-export.ts +35 -24
- package/src/server/canvas-geometry.ts +1 -0
- package/src/server/canvas-state.ts +5 -0
- package/src/server/demo-state.json +106 -100
- package/src/server/html-primitives.ts +3 -9
- package/src/server/html-surface.ts +8 -0
- package/src/server/index.ts +19 -9
- package/src/server/operations/mcp-contracts.ts +17 -2
- package/src/server/operations/ops/boards.ts +1 -3
- package/src/server/operations/ops/viewport.ts +11 -9
- package/src/server/server.ts +7 -59
- package/src/server/tour-control.ts +66 -6
- package/src/shared/ax-surface-protocol.ts +0 -11
- package/src/shared/content-height-reporter.ts +4 -3
- package/src/shared/tour.ts +8 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { boardList } from '../state/boards-store';
|
|
1
|
+
import { activeBoard, boardList } from '../state/boards-store';
|
|
2
2
|
import { cardQualification, effectiveLifecycleFilter as lifecycleFilter } from '../state/knowledge-store';
|
|
3
3
|
import { knowledgeLifecycleEnabled } from '../state/workspace-settings';
|
|
4
4
|
import { relationHighlight } from '../state/relation-store';
|
|
@@ -50,7 +50,7 @@ import { AxStepControls } from '../nodes/AxStepControls';
|
|
|
50
50
|
import { getNodeIcon, IconChevronRight, IconClose, IconMore, IconExpand, IconPin } from '../icons';
|
|
51
51
|
import { EXPANDABLE_TYPES, TYPE_LABELS } from '../types';
|
|
52
52
|
import type { CanvasNodeState } from '../types';
|
|
53
|
-
import {
|
|
53
|
+
import { computeAutoFitHeight, shouldAutoFitNode } from './auto-fit';
|
|
54
54
|
import { activeGuides, buildSnapCache, clearSnapCache, snapToGuides } from './snap-guides';
|
|
55
55
|
import { useNodeDrag } from './use-node-drag';
|
|
56
56
|
import { useNodeResize } from './use-node-resize';
|
|
@@ -89,44 +89,6 @@ export function nearestNodeInDirection(
|
|
|
89
89
|
return best;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/** Fraction of a node's height the title bar may occupy before it starts
|
|
93
|
-
* crowding out the content it is labelling. */
|
|
94
|
-
const MAX_TITLEBAR_HEIGHT_RATIO = 0.4;
|
|
95
|
-
/** Fraction of a node's width the fixed title-bar chrome may occupy before it
|
|
96
|
-
* starts eating the node's NAME. */
|
|
97
|
-
const MAX_TITLEBAR_CHROME_WIDTH_RATIO = 0.55;
|
|
98
|
-
/** Unscaled width of everything in the bar that is not the title: type icon,
|
|
99
|
-
* status/type pill, the control cluster, and the gaps between them. */
|
|
100
|
-
const TITLEBAR_CHROME_BASE_WIDTH = 175;
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* How much to enlarge node chrome (title bar, badges, icons) when zoomed out so
|
|
104
|
-
* it stays legible. Full inverse compensation is capped at 2.2x — but the cap
|
|
105
|
-
* alone ignored the node it is drawn on, so on SHORT nodes the growing bar ate
|
|
106
|
-
* the body: a 116px-tall section label at 46% zoom gave the title bar 72% of the
|
|
107
|
-
* node and left 14px for text that needed 41, so the markdown was simply cut off.
|
|
108
|
-
* The scale is therefore also bounded by the node's own height, keeping the bar
|
|
109
|
-
* under MAX_TITLEBAR_HEIGHT_RATIO of it. Tall nodes are unaffected (their height
|
|
110
|
-
* never binds); pass height 0 for collapsed/auto-height nodes to skip the bound.
|
|
111
|
-
*/
|
|
112
|
-
export function nodeChromeScale(viewportScale: number, nodeHeight: number, nodeWidth = 0): number {
|
|
113
|
-
if (!Number.isFinite(viewportScale) || viewportScale >= 1) return 1;
|
|
114
|
-
const zoomScale = Math.min(2.2, 1 / Math.max(viewportScale, 0.01));
|
|
115
|
-
let bounded = zoomScale;
|
|
116
|
-
if (Number.isFinite(nodeHeight) && nodeHeight > 0) {
|
|
117
|
-
bounded = Math.min(bounded, (nodeHeight * MAX_TITLEBAR_HEIGHT_RATIO) / AUTO_FIT_TITLEBAR_HEIGHT);
|
|
118
|
-
}
|
|
119
|
-
// Width matters as much as height: the icon, pill and control cluster are all
|
|
120
|
-
// flex-shrink: 0, so the NAME is the only thing that gives way. Unbounded, a
|
|
121
|
-
// 360px step node at 52% zoom spent 100 of 187 bar pixels on controls alone and
|
|
122
|
-
// the title collapsed to nothing. Bound the scale so the fixed chrome can never
|
|
123
|
-
// take more than MAX_TITLEBAR_CHROME_WIDTH_RATIO of the node.
|
|
124
|
-
if (Number.isFinite(nodeWidth) && nodeWidth > 0) {
|
|
125
|
-
bounded = Math.min(bounded, (nodeWidth * MAX_TITLEBAR_CHROME_WIDTH_RATIO) / TITLEBAR_CHROME_BASE_WIDTH);
|
|
126
|
-
}
|
|
127
|
-
return Math.max(1, bounded);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
92
|
/** A header title keeps at least this much room, or all of it when shorter (HeaderMarks.dc.html). */
|
|
131
93
|
const TITLE_MIN_WIDTH = 140;
|
|
132
94
|
|
|
@@ -217,7 +179,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
217
179
|
document.removeEventListener('keydown', escListener.current);
|
|
218
180
|
escListener.current = null;
|
|
219
181
|
}
|
|
220
|
-
persistLayout();
|
|
182
|
+
persistLayout({ raisedNodeId: node.id });
|
|
221
183
|
// Membership changes ONLY here, and only while the pill was showing.
|
|
222
184
|
const target = dragDropTarget.value;
|
|
223
185
|
endDropTracking();
|
|
@@ -308,7 +270,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
308
270
|
toggleSelected(node.id);
|
|
309
271
|
return;
|
|
310
272
|
}
|
|
311
|
-
bringToFront(node.id);
|
|
273
|
+
bringToFront(node.id, false);
|
|
312
274
|
// User wins (phase 8): hold the node for the drag; an agent mid-edit yields.
|
|
313
275
|
reportHumanGrab(node.id);
|
|
314
276
|
noteNearAtGrab(node.id);
|
|
@@ -359,6 +321,15 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
359
321
|
(e: KeyboardEvent) => {
|
|
360
322
|
const target = e.target as HTMLElement | null;
|
|
361
323
|
if (target && target !== e.currentTarget && target.closest('input, textarea, [contenteditable="true"]')) return;
|
|
324
|
+
if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) {
|
|
325
|
+
const more = shellRef.current?.querySelector<HTMLButtonElement>('.node-hover-control[aria-haspopup="menu"]');
|
|
326
|
+
if (more) {
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
e.stopPropagation();
|
|
329
|
+
more.click();
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
362
333
|
const dir = ARROW_DIRS[e.key];
|
|
363
334
|
if (dir) {
|
|
364
335
|
e.preventDefault();
|
|
@@ -445,10 +416,14 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
445
416
|
const isTrace = node.type === 'trace';
|
|
446
417
|
const isTraceRunning = isTrace && node.data.status === 'running';
|
|
447
418
|
const isGroup = node.type === 'group';
|
|
419
|
+
const isHeading = node.type === 'markdown' && node.data.display === 'heading';
|
|
420
|
+
const graphType = (node.data.graphConfig as { graphType?: string } | undefined)?.graphType;
|
|
448
421
|
const isStrictSize = node.data.strictSize === true;
|
|
449
422
|
const viewportScale = Math.max(viewport.value.scale, 0.01);
|
|
450
|
-
|
|
451
|
-
|
|
423
|
+
// Text alone may grow within the fixed header line. Enlarging the whole bar
|
|
424
|
+
// resizes embedded viewports and reflows their content while zooming.
|
|
425
|
+
const titleScale = Math.min(1.2, Math.max(1, 1 / viewportScale));
|
|
426
|
+
const chipFold = useHeaderFold(titleRef, headerChip(node, isContextPinned), title, node.size.width, titleScale);
|
|
452
427
|
|
|
453
428
|
const groupColor = isGroup && node.data.color ? (node.data.color as string) : undefined;
|
|
454
429
|
const nodeStyle: Record<string, string | number> = {
|
|
@@ -457,7 +432,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
457
432
|
width: `${node.size.width}px`,
|
|
458
433
|
height: node.collapsed ? 'auto' : `${node.size.height}px`,
|
|
459
434
|
zIndex: node.zIndex,
|
|
460
|
-
'--node-
|
|
435
|
+
'--node-title-scale': titleScale.toFixed(3),
|
|
461
436
|
'--node-port-scale': (1 / viewportScale).toFixed(3),
|
|
462
437
|
...(groupColor ? { '--group-color': groupColor } : {}),
|
|
463
438
|
};
|
|
@@ -477,6 +452,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
477
452
|
isTrace ? 'trace-node' : '',
|
|
478
453
|
isTraceRunning ? 'trace-running' : '',
|
|
479
454
|
isGroup ? 'group-node' : '',
|
|
455
|
+
isHeading ? 'heading-node' : '',
|
|
480
456
|
isStrictSize ? 'strict-size' : '',
|
|
481
457
|
isAgentMutating ? 'agent-mutating' : '',
|
|
482
458
|
sessionChromeActive.value && sessionEditedIds.value.has(node.id) ? 'session-edited' : '',
|
|
@@ -623,7 +599,9 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
623
599
|
) : (
|
|
624
600
|
<span class="group-name">{title}</span>
|
|
625
601
|
)}
|
|
626
|
-
<span class="group-count">
|
|
602
|
+
<span class="group-count">
|
|
603
|
+
{groupChildren.length} {groupChildren.length === 1 ? 'node' : 'nodes'}
|
|
604
|
+
</span>
|
|
627
605
|
</span>
|
|
628
606
|
<span class="group-edge-spacer" />
|
|
629
607
|
<span class="group-actions" role="toolbar" aria-label={`${title} actions`}>
|
|
@@ -740,7 +718,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
740
718
|
<NodeTypeIcon node={node} pinned={isContextPinned} title={title} fold={chipFold}>
|
|
741
719
|
{(() => {
|
|
742
720
|
const NodeIcon = getNodeIcon(node.type);
|
|
743
|
-
return <NodeIcon size={
|
|
721
|
+
return <NodeIcon size={14} />;
|
|
744
722
|
})()}
|
|
745
723
|
</NodeTypeIcon>
|
|
746
724
|
{/* The kind-colored icon says what type this is (rail-chrome-v2 card
|
|
@@ -766,6 +744,14 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
766
744
|
{title}
|
|
767
745
|
</span>
|
|
768
746
|
)}
|
|
747
|
+
{activeBoard()?.readmeNodeId === node.id && <span class="node-readme-mark">README</span>}
|
|
748
|
+
{node.type === 'graph' && graphType && <span class="node-header-meta">{graphType}</span>}
|
|
749
|
+
{node.type === 'file' && <div class="node-file-toolbar" data-file-toolbar={node.id} />}
|
|
750
|
+
{node.type === 'status' && typeof node.data.updatedAt === 'string' && (
|
|
751
|
+
<time class="node-header-meta" dateTime={node.data.updatedAt}>
|
|
752
|
+
{new Date(node.data.updatedAt).toLocaleTimeString()}
|
|
753
|
+
</time>
|
|
754
|
+
)}
|
|
769
755
|
<NearPinMark node={node} pinned={isContextPinned} fold={chipFold} />
|
|
770
756
|
<NodeContextMark node={node} pinned={isContextPinned} fold={chipFold} />
|
|
771
757
|
<div class="node-controls">
|
|
@@ -827,6 +813,7 @@ export function CanvasNode({ node, children, onContextMenu }: CanvasNodeProps) {
|
|
|
827
813
|
onContextMenu?.(e, node.id);
|
|
828
814
|
}}
|
|
829
815
|
aria-label="More actions"
|
|
816
|
+
aria-haspopup="menu"
|
|
830
817
|
>
|
|
831
818
|
<IconMore size={14} />
|
|
832
819
|
</button>
|
|
@@ -17,6 +17,7 @@ import { HtmlNode } from '../nodes/HtmlNode';
|
|
|
17
17
|
import { PromptNode } from '../nodes/PromptNode';
|
|
18
18
|
import { ResponseNode } from '../nodes/ResponseNode';
|
|
19
19
|
import { TraceNode } from '../nodes/TraceNode';
|
|
20
|
+
import { ViewerHost } from '../nodes/ViewerHost';
|
|
20
21
|
import {
|
|
21
22
|
activeNodeId,
|
|
22
23
|
cancelViewportAnimation,
|
|
@@ -111,9 +112,10 @@ class WorldNode extends Component<{
|
|
|
111
112
|
|
|
112
113
|
render() {
|
|
113
114
|
const { node, onContextMenu } = this.props;
|
|
115
|
+
const viewer = ['html', 'mcp-app', 'json-render', 'graph', 'webpage', 'mermaid'].includes(node.type);
|
|
114
116
|
return (
|
|
115
117
|
<CanvasNode node={node} onContextMenu={onContextMenu}>
|
|
116
|
-
{renderNodeContent(node)}
|
|
118
|
+
{viewer ? <ViewerHost node={node}>{renderNodeContent(node)}</ViewerHost> : renderNodeContent(node)}
|
|
117
119
|
</CanvasNode>
|
|
118
120
|
);
|
|
119
121
|
}
|
|
@@ -873,9 +875,8 @@ export function CanvasViewport({
|
|
|
873
875
|
: 'default',
|
|
874
876
|
}}
|
|
875
877
|
>
|
|
876
|
-
{/*
|
|
877
|
-
|
|
878
|
-
the scale pivot is the top-left corner of the world layer. */}
|
|
878
|
+
{/* Scale the complete spatial world proportionally, including iframe
|
|
879
|
+
contents. CSS zoom changes embedded viewport semantics in WebKit. */}
|
|
879
880
|
<div
|
|
880
881
|
ref={worldRef}
|
|
881
882
|
class="canvas-world"
|
|
@@ -894,9 +895,7 @@ export function CanvasViewport({
|
|
|
894
895
|
<IntentLayer />
|
|
895
896
|
<AgentPresenceLayer />
|
|
896
897
|
<HumanPresenceLayer />
|
|
897
|
-
<EdgeLayer nodes={nodes} edges={edges}
|
|
898
|
-
<EdgeLayer nodes={nodes} edges={edges} part="pills" />
|
|
899
|
-
<EdgeLayer nodes={nodes} edges={edges} part="pill-hits" />
|
|
898
|
+
<EdgeLayer nodes={nodes} edges={edges} />
|
|
900
899
|
{draggingEdge.value && (
|
|
901
900
|
<div
|
|
902
901
|
class="edge-hint-pill"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentChildren } from 'preact';
|
|
2
2
|
import { knowledgeLifecycleEnabled } from '../state/workspace-settings';
|
|
3
|
-
import { useCallback, useLayoutEffect, useState } from 'preact/hooks';
|
|
3
|
+
import { useCallback, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
4
4
|
import {
|
|
5
5
|
contextPinnedNodeIds,
|
|
6
6
|
edges,
|
|
@@ -28,8 +28,9 @@ import {
|
|
|
28
28
|
updateNodeFromClient,
|
|
29
29
|
} from '../state/intent-bridge';
|
|
30
30
|
import { canOpenAsSite, openNodeAsSite } from '../nodes/surface-url';
|
|
31
|
+
import { canCopyPrimitiveJson, copyPrimitiveJson } from '../nodes/primitive-copy';
|
|
31
32
|
import { askText } from './TextPrompt';
|
|
32
|
-
import { activeBoard, boardIncarnation } from '../state/boards-store';
|
|
33
|
+
import { activeBoard, boardIncarnation, setBoardReadme } from '../state/boards-store';
|
|
33
34
|
import { openLifecycle } from '../state/knowledge-store';
|
|
34
35
|
import { RELATION_KINDS, relationKind } from '../../shared/relations.js';
|
|
35
36
|
import { EXPANDABLE_TYPES } from '../types';
|
|
@@ -62,7 +63,9 @@ export function useContextMenu() {
|
|
|
62
63
|
const openNodeMenu = useCallback((e: MouseEvent, nodeId: string) => {
|
|
63
64
|
e.preventDefault();
|
|
64
65
|
e.stopPropagation();
|
|
65
|
-
|
|
66
|
+
const trigger = e.detail === 0 && e.target instanceof Element ? e.target.closest('button') : null;
|
|
67
|
+
const bounds = trigger?.getBoundingClientRect();
|
|
68
|
+
setMenu({ kind: 'node', x: bounds?.left ?? e.clientX, y: bounds?.bottom ?? e.clientY, nodeId });
|
|
66
69
|
}, []);
|
|
67
70
|
|
|
68
71
|
const openCanvasMenu = useCallback((e: MouseEvent, canvasX: number, canvasY: number) => {
|
|
@@ -113,6 +116,21 @@ const GROUP_COLOR_PRESETS = [
|
|
|
113
116
|
] as const;
|
|
114
117
|
|
|
115
118
|
export function ContextMenu({ menu, onClose }: ContextMenuProps) {
|
|
119
|
+
const root = useRef<HTMLDivElement>(null);
|
|
120
|
+
useLayoutEffect(() => {
|
|
121
|
+
const element = root.current;
|
|
122
|
+
const trigger = document.activeElement;
|
|
123
|
+
element?.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus();
|
|
124
|
+
return () => {
|
|
125
|
+
if (element?.contains(document.activeElement) && trigger instanceof HTMLElement && trigger.isConnected) {
|
|
126
|
+
// Card controls hide when the menu takes focus. Reveal the opener
|
|
127
|
+
// through its card before restoring focus to the button itself.
|
|
128
|
+
trigger.closest<HTMLElement>('.canvas-node')?.focus({ preventScroll: true });
|
|
129
|
+
trigger.focus({ preventScroll: true });
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}, [menu]);
|
|
133
|
+
|
|
116
134
|
let items: MenuItem[];
|
|
117
135
|
if (menu.kind === 'node') {
|
|
118
136
|
const node = nodes.value.get(menu.nodeId);
|
|
@@ -215,6 +233,9 @@ export function ContextMenu({ menu, onClose }: ContextMenuProps) {
|
|
|
215
233
|
|
|
216
234
|
return (
|
|
217
235
|
<div
|
|
236
|
+
ref={root}
|
|
237
|
+
role="menu"
|
|
238
|
+
aria-label={menu.kind === 'node' ? 'Node actions' : menu.kind === 'edge' ? 'Edge actions' : 'Canvas actions'}
|
|
218
239
|
class="context-menu"
|
|
219
240
|
style={{
|
|
220
241
|
position: 'fixed',
|
|
@@ -224,6 +245,27 @@ export function ContextMenu({ menu, onClose }: ContextMenuProps) {
|
|
|
224
245
|
}}
|
|
225
246
|
onPointerDown={(e) => e.stopPropagation()}
|
|
226
247
|
onClick={(e) => e.stopPropagation()}
|
|
248
|
+
onBlur={(e) => {
|
|
249
|
+
if (e.relatedTarget instanceof Node && !e.currentTarget.contains(e.relatedTarget)) onClose();
|
|
250
|
+
}}
|
|
251
|
+
onKeyDown={(e) => {
|
|
252
|
+
e.stopPropagation();
|
|
253
|
+
if (e.key === 'Escape') {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
onClose();
|
|
256
|
+
} else if (e.target instanceof HTMLButtonElement && ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) {
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
const controls = [...e.currentTarget.querySelectorAll<HTMLElement>('button:not(:disabled), input, select')];
|
|
259
|
+
const index = controls.indexOf(e.target);
|
|
260
|
+
const next =
|
|
261
|
+
e.key === 'Home'
|
|
262
|
+
? 0
|
|
263
|
+
: e.key === 'End'
|
|
264
|
+
? controls.length - 1
|
|
265
|
+
: (index + (e.key === 'ArrowUp' ? -1 : 1) + controls.length) % controls.length;
|
|
266
|
+
controls[next]?.focus();
|
|
267
|
+
}
|
|
268
|
+
}}
|
|
227
269
|
>
|
|
228
270
|
{items.map((item) => {
|
|
229
271
|
const baseKey = item.separator
|
|
@@ -251,6 +293,8 @@ export function ContextMenu({ menu, onClose }: ContextMenuProps) {
|
|
|
251
293
|
<button
|
|
252
294
|
key={itemKey}
|
|
253
295
|
type="button"
|
|
296
|
+
role="menuitem"
|
|
297
|
+
disabled={!item.action}
|
|
254
298
|
class={`context-menu-item${item.danger ? ' is-danger' : ''}`}
|
|
255
299
|
onClick={() => {
|
|
256
300
|
item.action?.();
|
|
@@ -622,6 +666,22 @@ function buildNodeMenuItems(node: CanvasNodeState): MenuItem[] {
|
|
|
622
666
|
items.push({ separator: true });
|
|
623
667
|
|
|
624
668
|
// Type-specific
|
|
669
|
+
if (canCopyPrimitiveJson(node)) {
|
|
670
|
+
items.push({ label: 'Copy JSON', action: () => void copyPrimitiveJson(node.id) });
|
|
671
|
+
}
|
|
672
|
+
if (node.type === 'markdown') {
|
|
673
|
+
items.push({
|
|
674
|
+
label: node.data.display === 'heading' ? 'Display as card' : 'Display as heading',
|
|
675
|
+
action: () =>
|
|
676
|
+
void updateNodeFromClient(node.id, { data: { display: node.data.display === 'heading' ? 'card' : 'heading' } }),
|
|
677
|
+
});
|
|
678
|
+
const board = activeBoard();
|
|
679
|
+
if (board)
|
|
680
|
+
items.push({
|
|
681
|
+
label: board.readmeNodeId === node.id ? 'Remove as README' : 'Set as README',
|
|
682
|
+
action: () => void setBoardReadme(board.id, board.readmeNodeId === node.id ? null : node.id),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
625
685
|
if ((node.type === 'markdown' || node.type === 'file' || node.type === 'image') && localPath) {
|
|
626
686
|
items.push({
|
|
627
687
|
label: 'Open in browser',
|