pmx-canvas 0.10.0 → 0.12.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/extension.mjs +3 -1
- package/.github/extensions/pmx-canvas/runtime.json +1 -1
- package/.github/extensions/pmx-canvas/tools.json +688 -128
- package/CHANGELOG.md +69 -0
- package/dist/amp-plugin/pmx-canvas/LICENSE +21 -0
- package/dist/amp-plugin/pmx-canvas/THIRD_PARTY_LICENSES.txt +331 -0
- package/dist/amp-plugin/pmx-canvas/index.js +5445 -0
- package/dist/amp-plugin/pmx-canvas/runtime.json +3 -0
- package/dist/amp-plugin/pmx-canvas/shared-runtime.mjs +194 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/SKILL.md +251 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/evals.json +1086 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/jwt.ts +17 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/login.ts +12 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/auth/middleware.ts +13 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/code-exploration/src/routes/auth.ts +13 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/evals/fixtures/investigation-board/src/handlers/users.ts +27 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/mcp.json +36 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/ax-html-control-surface.md +101 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/codex-app-adapter.md +151 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/excalidraw-diagram-authoring.md +150 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/full-reference.md +1785 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/github-copilot-app-adapter.md +223 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/html-primitives.md +139 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/importing-context.md +132 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/installing-pmx-canvas.md +251 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/tours-and-recording.md +111 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas/references/workbench-workflows.md +900 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-orchestration/SKILL.md +141 -0
- package/dist/amp-plugin/pmx-canvas/skills/pmx-canvas-testing/SKILL.md +266 -0
- package/dist/amp-plugin/pmx-canvas/start-mcp.mjs +14857 -0
- package/dist/amp-plugin/pmx-canvas/tools.json +10963 -0
- package/dist/canvas/chunks/abnfDiagram-N423BO3Z-cwtsds5d.js +1 -0
- package/dist/canvas/chunks/architecture-TIHT7OUA-x63cwejs.js +1 -0
- package/dist/canvas/chunks/architectureDiagram-T3A2C74G-b4xc9tpz.js +36 -0
- package/dist/canvas/chunks/blockDiagram-VBNYF7ZC-ahp3yxxs.js +132 -0
- package/dist/canvas/chunks/c4Diagram-5PPSVZJV-nappnay7.js +10 -0
- package/dist/canvas/chunks/classDiagram-JCYQIIEL-k1j2xc1t.js +1 -0
- package/dist/canvas/chunks/classDiagram-v2-OCEON4UE-33x08xf2.js +1 -0
- package/dist/canvas/chunks/cose-bilkent-JH36ORCC-a428syda.js +1 -0
- package/dist/canvas/chunks/cynefin-VYW2F7L2-8v6jxfz8.js +1 -0
- package/dist/canvas/chunks/cynefinDiagram-MW4NZA55-nspsqebj.js +62 -0
- package/dist/canvas/chunks/dagre-VZM6K2ZE-8y6177fh.js +4 -0
- package/dist/canvas/chunks/diagram-7IWD3JNH-vym028wn.js +30 -0
- package/dist/canvas/chunks/diagram-B4RE2ZJO-jddqb8sy.js +3 -0
- package/dist/canvas/chunks/diagram-LBJQPF4R-bjwbpkrn.js +24 -0
- package/dist/canvas/chunks/diagram-Q27KOJAE-j6ph6hcx.js +24 -0
- package/dist/canvas/chunks/diagram-UB23O5K3-p09w8jej.js +41 -0
- package/dist/canvas/chunks/ebnfDiagram-BXEA7PRR-j975ycq5.js +1 -0
- package/dist/canvas/chunks/erDiagram-JOGREHBK-sxp6hjn8.js +85 -0
- package/dist/canvas/chunks/eventmodeling-45OFAUF4-xzfgn6xg.js +1 -0
- package/dist/canvas/chunks/flowDiagram-UKHOOZJN-2nsmkskb.js +1 -0
- package/dist/canvas/chunks/ganttDiagram-PKOTCBZU-4tv230y2.js +292 -0
- package/dist/canvas/chunks/gitGraph-TEB2WS4Q-macbhand.js +1 -0
- package/dist/canvas/chunks/gitGraphDiagram-DS77QQ5N-mqgmnw9g.js +106 -0
- package/dist/canvas/chunks/index-037z5dz2.js +232 -0
- package/dist/canvas/chunks/index-081n3har.js +207 -0
- package/dist/canvas/chunks/index-5n03dmb9.js +2 -0
- package/dist/canvas/chunks/index-5tv8v1hk.js +1 -0
- package/dist/canvas/chunks/index-60pgzdqe.js +157 -0
- package/dist/canvas/chunks/index-6ceya556.js +332 -0
- package/dist/canvas/chunks/index-8g1vb57n.js +2 -0
- package/dist/canvas/chunks/index-8rjes9r6.js +2 -0
- package/dist/canvas/chunks/index-9022d1n9.js +16 -0
- package/dist/canvas/chunks/index-9b02042t.js +2 -0
- package/dist/canvas/chunks/index-ap4q4vez.js +2 -0
- package/dist/canvas/chunks/index-dyp2nhd3.js +2 -0
- package/dist/canvas/chunks/index-frvta4tm.js +2 -0
- package/dist/canvas/chunks/index-gf4e0et4.js +2 -0
- package/dist/canvas/chunks/index-h9ycj8n2.js +2 -0
- package/dist/canvas/chunks/index-hj99nz1f.js +1 -0
- package/dist/canvas/chunks/index-mergmj97.js +2 -0
- package/dist/canvas/chunks/index-nwsya6dj.js +301 -0
- package/dist/canvas/chunks/index-nz8qbd1p.js +2 -0
- package/dist/canvas/chunks/index-qhfrazv3.js +2 -0
- package/dist/canvas/chunks/index-v119nznk.js +4 -0
- package/dist/canvas/chunks/index-vpt37m3d.js +89 -0
- package/dist/canvas/chunks/index-y9ma6azy.js +2 -0
- package/dist/canvas/chunks/index-zx0j3a2z.js +179 -0
- package/dist/canvas/chunks/info-DKCQHKI2-a0ykzs9z.js +1 -0
- package/dist/canvas/chunks/infoDiagram-6WML65LV-qv97qq6a.js +2 -0
- package/dist/canvas/chunks/ishikawaDiagram-WSZJBQD7-p9306gh0.js +70 -0
- package/dist/canvas/chunks/journeyDiagram-NVQOT4AX-sv67zats.js +139 -0
- package/dist/canvas/chunks/kanban-definition-27J2QSJJ-k2pfam6p.js +89 -0
- package/dist/canvas/chunks/katex-dmg8k4zm.js +257 -0
- package/dist/canvas/chunks/mermaid.core-83r0fdce.js +12 -0
- package/dist/canvas/chunks/mindmap-definition-FAOFIHXS-vg2newm1.js +96 -0
- package/dist/canvas/chunks/packet-7NZHBO7P-tqnabcdv.js +1 -0
- package/dist/canvas/chunks/pegDiagram-VL7TDLO6-k2h7zc5n.js +1 -0
- package/dist/canvas/chunks/pie-RZYD4A2V-wke6gyhe.js +1 -0
- package/dist/canvas/chunks/pieDiagram-7S7Q4E2Y-hg169p1v.js +39 -0
- package/dist/canvas/chunks/quadrantDiagram-CIZ2JOQS-bxtvsq0b.js +7 -0
- package/dist/canvas/chunks/radar-I7S5WNFK-w7ka0drw.js +1 -0
- package/dist/canvas/chunks/railroad-3IZDKUUU-y3vg318x.js +1 -0
- package/dist/canvas/chunks/railroad-abnf-AHOZXSZD-6hqvcz3c.js +1 -0
- package/dist/canvas/chunks/railroad-ebnf-EBAXGLYW-vcae260m.js +1 -0
- package/dist/canvas/chunks/railroad-peg-LSFZ7HO6-0dbyme23.js +1 -0
- package/dist/canvas/chunks/railroadDiagram-AXF67PYL-njyhcp4p.js +1 -0
- package/dist/canvas/chunks/requirementDiagram-LRYGKXZP-2z6v09gm.js +84 -0
- package/dist/canvas/chunks/sankeyDiagram-W5VNT64P-15mzam0e.js +40 -0
- package/dist/canvas/chunks/sequenceDiagram-SI44F4Z6-rbshyd2c.js +162 -0
- package/dist/canvas/chunks/sizeCapture-X5ZJPWSS-sg26xwts.js +1 -0
- package/dist/canvas/chunks/stateDiagram-OKZ733FA-6eahp98e.js +1 -0
- package/dist/canvas/chunks/stateDiagram-v2-UEYNNEHI-tty0ggn5.js +1 -0
- package/dist/canvas/chunks/swimlanes-SLNWSIFB-ze5db10t.js +1 -0
- package/dist/canvas/chunks/swimlanesDiagram-ULZ7WXOC-thze75gq.js +8 -0
- package/dist/canvas/chunks/timeline-definition-Z64GVDOM-vnp7jyy8.js +120 -0
- package/dist/canvas/chunks/treeView-QDETBFTQ-6ysffnm6.js +1 -0
- package/dist/canvas/chunks/treemap-6X3UGDF4-m3h9p066.js +1 -0
- package/dist/canvas/chunks/vennDiagram-T6HMQDX7-0qrnb7mj.js +34 -0
- package/dist/canvas/chunks/wardley-OPB4EBWU-dp37drwx.js +1 -0
- package/dist/canvas/chunks/wardleyDiagram-T6FBY63Y-8nxg8wqt.js +78 -0
- package/dist/canvas/chunks/xychartDiagram-ELKLHX3M-nj921e7s.js +7 -0
- package/dist/canvas/global.css +483 -230
- package/dist/canvas/index.js +158 -219
- package/dist/canvas/mcp-app.html +4301 -548
- 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/BoardMoveDialog.d.ts +2 -0
- package/dist/types/client/canvas/CanvasNode.d.ts +0 -11
- package/dist/types/client/canvas/EdgeEditor.d.ts +19 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +1 -2
- package/dist/types/client/canvas/GroupNavigation.d.ts +2 -0
- package/dist/types/client/canvas/TextPrompt.d.ts +4 -0
- 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/PromptNode.d.ts +1 -0
- 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/WorkCard.d.ts +7 -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-ax-surface-bridge.d.ts +1 -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 +18 -1
- package/dist/types/client/state/human-store.d.ts +2 -0
- package/dist/types/client/state/intent-bridge.d.ts +5 -2
- package/dist/types/client/state/presentation.d.ts +17 -6
- package/dist/types/client/state/relation-store.d.ts +11 -0
- package/dist/types/client/types.d.ts +1 -1
- package/dist/types/json-render/catalog.d.ts +2 -2
- package/dist/types/json-render/server.d.ts +2 -1
- package/dist/types/server/agent-presence.d.ts +1 -0
- package/dist/types/server/ax-interaction.d.ts +1 -0
- package/dist/types/server/ax-state-manager.d.ts +10 -2
- package/dist/types/server/board-local.d.ts +4 -0
- package/dist/types/server/board-move.d.ts +38 -0
- package/dist/types/server/canvas-db.d.ts +12 -2
- package/dist/types/server/canvas-operations.d.ts +5 -6
- package/dist/types/server/canvas-provenance.d.ts +1 -1
- package/dist/types/server/canvas-serialization.d.ts +1 -0
- package/dist/types/server/canvas-state.d.ts +86 -20
- package/dist/types/server/card-relocation.d.ts +6 -0
- package/dist/types/server/context-reads.d.ts +3 -1
- package/dist/types/server/human-presence.d.ts +1 -1
- package/dist/types/server/index.d.ts +26 -10
- package/dist/types/server/intent-registry.d.ts +8 -4
- package/dist/types/server/knowledge-lifecycle.d.ts +4 -2
- package/dist/types/server/mutation-history.d.ts +7 -5
- package/dist/types/server/operations/ops/boards.d.ts +0 -1
- package/dist/types/server/operations/registry.d.ts +2 -0
- package/dist/types/server/tour-control.d.ts +13 -3
- package/dist/types/server/trace-manager.d.ts +1 -0
- package/dist/types/shared/ax-kinds.d.ts +6 -0
- package/dist/types/shared/ax-surface-protocol.d.ts +8 -9
- package/dist/types/shared/human-presence.d.ts +1 -0
- package/dist/types/shared/lifecycle-status.d.ts +2 -0
- package/dist/types/shared/tour.d.ts +8 -0
- package/docs/RELEASE.md +21 -14
- package/docs/agent-plugin.md +16 -2
- package/docs/amp-plugin.md +271 -0
- package/docs/cli.md +42 -3
- package/docs/design/Chrome.dc.html +32 -6
- package/docs/design/GuestName.dc.html +50 -0
- package/docs/design/PluginLogo.svg +7 -0
- 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/WorkCards.dc.html +39 -0
- package/docs/design/Zoom.dc.html +37 -60
- package/docs/design/canvas.json +17 -1
- package/docs/design/plugin-logo.png +0 -0
- package/docs/design.md +280 -12
- package/docs/evals/agents-md-sections.md +38 -0
- package/docs/http-api.md +179 -11
- package/docs/mcp.md +69 -9
- package/docs/node-types.md +109 -4
- package/docs/plans/plan-009-tech-debt-backlog.md +3 -3
- package/docs/plans/plan-019-post-0.9-roadmap.md +327 -31
- package/docs/plans/plan-020-wave-5-work-planning.md +553 -0
- package/docs/product-vision-2026-09.md +42 -17
- package/docs/sdk.md +18 -0
- package/package.json +7 -4
- package/plugins/pmx-canvas/assets/preview.png +0 -0
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/extension.mjs +3 -1
- package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/plugin.json +2 -2
- package/plugins/pmx-canvas/runtime.json +1 -1
- package/plugins/pmx-canvas/tools.json +688 -128
- package/skills/data-analysis/SKILL.md +13 -26
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +3 -0
- package/skills/pmx-canvas/SKILL.md +232 -849
- package/skills/pmx-canvas/evals/evals.json +26 -3
- package/skills/pmx-canvas/references/full-reference.md +89 -24
- package/skills/pmx-canvas/references/html-primitives.md +7 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas/references/tours-and-recording.md +29 -2
- package/skills/pmx-canvas/references/workbench-workflows.md +900 -0
- package/skills/pmx-canvas-testing/SKILL.md +7 -5
- package/skills/published-consumer-e2e/SKILL.md +25 -8
- package/skills/published-consumer-e2e/scripts/run-published-consumer-e2e.sh +51 -77
- package/skills/web-artifacts-builder/LICENSE.txt +202 -0
- package/skills/web-artifacts-builder/SKILL.md +6 -0
- package/skills/web-artifacts-builder/scripts/init-artifact.sh +2 -2
- package/src/cli/commands/apps.ts +1 -0
- package/src/cli/commands/boards.ts +30 -0
- package/src/client/App.tsx +20 -4
- package/src/client/canvas/AttentionHistory.tsx +10 -2
- package/src/client/canvas/BarHint.tsx +6 -3
- package/src/client/canvas/BoardMoveDialog.tsx +225 -0
- package/src/client/canvas/CanvasNode.tsx +79 -49
- package/src/client/canvas/CanvasViewport.tsx +42 -14
- package/src/client/canvas/ContextMenu.tsx +97 -15
- package/src/client/canvas/EdgeEditor.tsx +149 -0
- package/src/client/canvas/EdgeLayer.tsx +441 -174
- package/src/client/canvas/ExpandedNodeOverlay.tsx +5 -2
- package/src/client/canvas/GroupNavigation.tsx +85 -0
- package/src/client/canvas/Presentation.tsx +53 -3
- package/src/client/canvas/RelationsPanel.tsx +9 -0
- package/src/client/canvas/SelectionBar.tsx +21 -0
- package/src/client/canvas/SessionPanel.tsx +35 -1
- package/src/client/canvas/TextPrompt.tsx +18 -1
- package/src/client/canvas/TopBar.tsx +54 -9
- package/src/client/canvas/auto-fit.ts +9 -11
- package/src/client/canvas/canvas-area.ts +2 -2
- 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 +302 -1465
- 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 +14 -24
- package/src/client/nodes/McpAppNode.tsx +48 -154
- package/src/client/nodes/MermaidNode.tsx +80 -69
- package/src/client/nodes/PromptNode.tsx +44 -17
- package/src/client/nodes/ResponseNode.tsx +8 -6
- package/src/client/nodes/StatusNode.tsx +34 -7
- package/src/client/nodes/StatusSummary.tsx +16 -3
- package/src/client/nodes/TraceNode.tsx +8 -8
- package/src/client/nodes/ViewerFrame.tsx +273 -0
- package/src/client/nodes/ViewerHost.tsx +41 -0
- package/src/client/nodes/WebpageNode.tsx +22 -24
- package/src/client/nodes/WorkCard.tsx +175 -0
- package/src/client/nodes/iframe-document-url.ts +20 -27
- package/src/client/nodes/primitive-copy.ts +97 -0
- package/src/client/nodes/use-ax-surface-bridge.ts +18 -14
- 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/attention-bridge.ts +5 -0
- package/src/client/state/attention-store.ts +1 -0
- package/src/client/state/canvas-store.ts +202 -26
- package/src/client/state/human-store.ts +22 -2
- package/src/client/state/intent-bridge.ts +20 -5
- package/src/client/state/presentation.ts +135 -15
- package/src/client/state/relation-store.ts +127 -1
- package/src/client/state/sse-bridge.ts +18 -19
- package/src/client/theme/global.css +483 -230
- package/src/client/theme/surface-theme.css +6 -6
- package/src/client/types.ts +1 -0
- 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/mcp/server.ts +13 -3
- package/src/server/agent-presence.ts +43 -23
- package/src/server/ax-flow-loop.ts +6 -0
- package/src/server/ax-gate-ttl.ts +8 -1
- package/src/server/ax-interaction.ts +16 -10
- package/src/server/ax-state-manager.ts +17 -4
- package/src/server/ax-wait.ts +20 -6
- package/src/server/board-export.ts +54 -30
- package/src/server/board-local.ts +20 -0
- package/src/server/board-move.ts +297 -0
- package/src/server/canvas-db.ts +249 -158
- package/src/server/canvas-geometry.ts +1 -0
- package/src/server/canvas-operations.ts +139 -109
- package/src/server/canvas-schema.ts +21 -0
- package/src/server/canvas-serialization.ts +2 -0
- package/src/server/canvas-state.ts +797 -159
- package/src/server/card-relocation.ts +53 -0
- package/src/server/code-graph.ts +4 -3
- package/src/server/context-reads.ts +83 -55
- package/src/server/demo-state.json +140 -114
- package/src/server/file-watcher.ts +30 -10
- package/src/server/html-primitives.ts +3 -9
- package/src/server/html-surface.ts +8 -0
- package/src/server/human-presence.ts +18 -5
- package/src/server/index.ts +54 -24
- package/src/server/intent-registry.ts +43 -5
- package/src/server/knowledge-lifecycle.ts +32 -9
- package/src/server/lifecycle-status.ts +29 -1
- package/src/server/mcp-app-runtime.ts +26 -7
- package/src/server/mutation-history.ts +37 -8
- package/src/server/operations/composites.ts +6 -3
- package/src/server/operations/mcp-contracts.ts +32 -2
- package/src/server/operations/mcp.ts +36 -9
- package/src/server/operations/ops/annotation.ts +22 -1
- package/src/server/operations/ops/ax-await.ts +1 -0
- package/src/server/operations/ops/ax-timeline.ts +28 -2
- package/src/server/operations/ops/batch.ts +25 -11
- package/src/server/operations/ops/boards.ts +45 -6
- package/src/server/operations/ops/nodes.ts +70 -0
- package/src/server/operations/ops/query.ts +32 -3
- package/src/server/operations/ops/snapshots.ts +1 -0
- package/src/server/operations/ops/viewport.ts +11 -9
- package/src/server/operations/registry.ts +49 -4
- package/src/server/server.ts +69 -75
- package/src/server/tour-control.ts +97 -12
- package/src/server/trace-manager.ts +4 -0
- package/src/server/web-artifacts/scripts/init-artifact.sh +2 -2
- package/src/shared/agent-context.ts +7 -1
- package/src/shared/auto-arrange.ts +5 -4
- package/src/shared/ax-kinds.ts +13 -0
- package/src/shared/ax-surface-protocol.ts +20 -9
- package/src/shared/content-height-reporter.ts +4 -3
- package/src/shared/human-presence.ts +1 -0
- package/src/shared/lifecycle-status.ts +2 -0
- package/src/shared/tour.ts +8 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Signal, signal } from '@preact/signals';
|
|
2
|
-
import { useEffect, useRef } from 'preact/hooks';
|
|
1
|
+
import { type Signal, computed, signal } from '@preact/signals';
|
|
2
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'preact/hooks';
|
|
3
3
|
import {
|
|
4
4
|
draggingEdge,
|
|
5
5
|
edges,
|
|
@@ -7,15 +7,23 @@ import {
|
|
|
7
7
|
selectedEdgeId,
|
|
8
8
|
viewport,
|
|
9
9
|
visibleNodeFor,
|
|
10
|
+
groupContents,
|
|
10
11
|
} from '../state/canvas-store';
|
|
11
12
|
import { linkMarks, writerName } from '../state/context-status-store';
|
|
12
13
|
import { HUMAN_STARTED_SESSION_LABEL } from '../../shared/agent-presence';
|
|
13
14
|
import { activeSession } from '../state/presence-store';
|
|
14
|
-
import { activeBoard } from '../state/boards-store';
|
|
15
|
+
import { activeBoard, boardIncarnation } from '../state/boards-store';
|
|
15
16
|
import { reportClientViewportSize } from '../state/intent-bridge';
|
|
16
17
|
import { relationDefinition, relationKind, relationText } from '../../shared/relations.js';
|
|
17
18
|
import type { CanvasEdge, CanvasNodeState } from '../types';
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
dependencyAnalysis,
|
|
21
|
+
relationWarning,
|
|
22
|
+
relationFilter,
|
|
23
|
+
relationFocus,
|
|
24
|
+
relationHighlight,
|
|
25
|
+
} from '../state/relation-store';
|
|
26
|
+
import { edgeEditorAnchor } from './EdgeEditor';
|
|
19
27
|
|
|
20
28
|
/** Filter only the kinds on this board; shared presets are managed in Home. */
|
|
21
29
|
export function RelationLegend() {
|
|
@@ -71,6 +79,17 @@ export function RelationLegend() {
|
|
|
71
79
|
</button>
|
|
72
80
|
);
|
|
73
81
|
})}
|
|
82
|
+
{dependencyAnalysis.value.hasDependencies && (
|
|
83
|
+
<>
|
|
84
|
+
<span class={dependencyAnalysis.value.cycles ? 'relation-warning' : ''}>
|
|
85
|
+
{dependencyAnalysis.value.cycles ? '⚠ ' : ''}
|
|
86
|
+
{dependencyAnalysis.value.cycles} dependency cycles
|
|
87
|
+
</span>
|
|
88
|
+
{dependencyAnalysis.value.focused && (
|
|
89
|
+
<span>Change selection → {dependencyAnalysis.value.affected.size} affected</span>
|
|
90
|
+
)}
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
74
93
|
</div>
|
|
75
94
|
);
|
|
76
95
|
}
|
|
@@ -146,6 +165,8 @@ export function edgeChromeScale(scale: number): number {
|
|
|
146
165
|
return scale < 1 ? 1 / scale : 1;
|
|
147
166
|
}
|
|
148
167
|
|
|
168
|
+
const edgeZoom = computed(() => viewport.value.scale);
|
|
169
|
+
|
|
149
170
|
function dashArray(edge: CanvasEdge, scale: number): string | undefined {
|
|
150
171
|
const dashed = `${8 * scale} ${4 * scale}`;
|
|
151
172
|
if (edge.style === 'dashed') return dashed;
|
|
@@ -238,13 +259,56 @@ function bezierMidpoint(
|
|
|
238
259
|
};
|
|
239
260
|
}
|
|
240
261
|
|
|
262
|
+
interface LabelObstacle {
|
|
263
|
+
x: number;
|
|
264
|
+
y: number;
|
|
265
|
+
width: number;
|
|
266
|
+
height: number;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Nearest clear position along either axis through the original curve midpoint.
|
|
270
|
+
* Expanded obstacles describe forbidden pill centres. Boundary candidates include
|
|
271
|
+
* the outside of the whole occupied area, so even a crowded board has a solution.
|
|
272
|
+
* World-space placement is independent of pan; all three layers use the same order.
|
|
273
|
+
*/
|
|
274
|
+
function placeLabel(
|
|
275
|
+
mid: { x: number; y: number },
|
|
276
|
+
width: number,
|
|
277
|
+
height: number,
|
|
278
|
+
obstacles: LabelObstacle[],
|
|
279
|
+
gap: number,
|
|
280
|
+
) {
|
|
281
|
+
const forbidden = obstacles.map((box) => ({
|
|
282
|
+
left: box.x - width / 2 - gap,
|
|
283
|
+
right: box.x + box.width + width / 2 + gap,
|
|
284
|
+
top: box.y - height / 2 - gap,
|
|
285
|
+
bottom: box.y + box.height + height / 2 + gap,
|
|
286
|
+
}));
|
|
287
|
+
const clear = (point: { x: number; y: number }) =>
|
|
288
|
+
forbidden.every(
|
|
289
|
+
(box) => point.x <= box.left || point.x >= box.right || point.y <= box.top || point.y >= box.bottom,
|
|
290
|
+
);
|
|
291
|
+
if (clear(mid)) return mid;
|
|
292
|
+
const candidates = [mid];
|
|
293
|
+
for (const box of forbidden) {
|
|
294
|
+
if (mid.x > box.left && mid.x < box.right) {
|
|
295
|
+
candidates.push({ x: mid.x, y: box.top }, { x: mid.x, y: box.bottom });
|
|
296
|
+
}
|
|
297
|
+
if (mid.y > box.top && mid.y < box.bottom) {
|
|
298
|
+
candidates.push({ x: box.left, y: mid.y }, { x: box.right, y: mid.y });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const distance = (point: { x: number; y: number }) => (point.x - mid.x) ** 2 + (point.y - mid.y) ** 2;
|
|
302
|
+
candidates.sort((a, b) => distance(a) - distance(b));
|
|
303
|
+
return candidates.find(clear)!;
|
|
304
|
+
}
|
|
305
|
+
|
|
241
306
|
// ── EdgePath component ───────────────────────────────────────
|
|
242
307
|
interface EdgePathProps {
|
|
243
308
|
edge: CanvasEdge;
|
|
244
309
|
fromNode: CanvasNodeState;
|
|
245
310
|
toNode: CanvasNodeState;
|
|
246
|
-
|
|
247
|
-
end: Anchor;
|
|
311
|
+
layout: ReturnType<typeof edgeLayout>;
|
|
248
312
|
focused: boolean; // connected to the active node
|
|
249
313
|
dimmed: boolean; // active node exists but this edge is NOT connected
|
|
250
314
|
selected: boolean; // click-selected — Delete removes it
|
|
@@ -260,7 +324,15 @@ interface EdgePathProps {
|
|
|
260
324
|
*/
|
|
261
325
|
export type EdgeLayerPart = 'lines' | 'pills' | 'pill-hits';
|
|
262
326
|
|
|
263
|
-
function
|
|
327
|
+
function edgeLayout(
|
|
328
|
+
edge: CanvasEdge,
|
|
329
|
+
start: Anchor,
|
|
330
|
+
end: Anchor,
|
|
331
|
+
scale: number,
|
|
332
|
+
obstacles: LabelObstacle[],
|
|
333
|
+
textWidth: (text: string, weight?: number) => number,
|
|
334
|
+
parallelCount: number,
|
|
335
|
+
) {
|
|
264
336
|
const dx = end.x - start.x;
|
|
265
337
|
const dy = end.y - start.y;
|
|
266
338
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
@@ -279,26 +351,85 @@ function EdgePath({ edge, fromNode, toNode, start, end, focused, dimmed, selecte
|
|
|
279
351
|
// label chrome caps like node chrome (2.2). Below 35% zoom the label goes and
|
|
280
352
|
// only the link's marks stay (LinkAuthorship.dc.html).
|
|
281
353
|
const labelScale = Math.min(scale, 2.2);
|
|
282
|
-
const zoom =
|
|
354
|
+
const zoom = edgeZoom.value;
|
|
283
355
|
const marks = linkMarks(edge);
|
|
284
356
|
// Reasons belong in the hint/inspector, not a long pill covering both endpoints.
|
|
285
|
-
const
|
|
357
|
+
const baseLabel = edge.label ?? edge.kind;
|
|
358
|
+
const label = parallelCount > 1 ? `${baseLabel ?? relationKind(edge)} · ×${parallelCount}` : baseLabel;
|
|
359
|
+
const warning = [
|
|
360
|
+
dependencyAnalysis.value.cycleEdges.has(edge.id) ? 'Cycle' : '',
|
|
361
|
+
relationWarning(edge).length ? 'Source needs review' : '',
|
|
362
|
+
]
|
|
363
|
+
.filter(Boolean)
|
|
364
|
+
.join(' · ');
|
|
365
|
+
const warningText = warning ? `⚠ ${warning}` : '';
|
|
366
|
+
const warningWidth = warning ? textWidth(warningText, 500) + 12 : 0;
|
|
286
367
|
const showLabel = !!label && zoom >= 0.35;
|
|
287
368
|
// Pill layout in label units (× labelScale): the label, then a hairline and
|
|
288
369
|
// "not seen by …", which keeps only its glyph on a short line or below 60% zoom.
|
|
289
370
|
const pad = showLabel ? 9 : 5;
|
|
290
|
-
const labelWidth = showLabel ? (label
|
|
371
|
+
const labelWidth = showLabel ? textWidth(label) : 0;
|
|
291
372
|
const notSeenText = `not seen by ${agentName()}`;
|
|
292
|
-
const wordedNotSeenWidth = 12 + notSeenText
|
|
373
|
+
const wordedNotSeenWidth = 12 + textWidth(notSeenText, 500) + 4;
|
|
293
374
|
const fullWidth = pad * 2 + labelWidth + (marks.notSeen ? (labelWidth ? 12 : 0) + wordedNotSeenWidth : 0);
|
|
294
375
|
// A short line: the full pill plus some line on either side would not fit.
|
|
295
376
|
const notSeenWords = marks.notSeen && zoom >= 0.6 && dist >= (fullWidth + 60) * labelScale;
|
|
296
377
|
const notSeenWidth = marks.notSeen ? (labelWidth ? 12 : 0) + (notSeenWords ? wordedNotSeenWidth : 12) : 0;
|
|
297
|
-
const pillWidth = pad * 2 + labelWidth + notSeenWidth;
|
|
378
|
+
const pillWidth = pad * 2 + labelWidth + notSeenWidth + warningWidth;
|
|
298
379
|
const labelX = -pillWidth / 2 + pad;
|
|
299
380
|
const notSeenX = labelX + labelWidth + (labelWidth ? 12 : 0);
|
|
300
|
-
const hasPill = showLabel || marks.notSeen;
|
|
301
|
-
const
|
|
381
|
+
const hasPill = showLabel || marks.notSeen || warningWidth > 0;
|
|
382
|
+
const curveMid = bezierMidpoint(start.x, start.y, cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
|
|
383
|
+
const mid = hasPill ? placeLabel(curveMid, pillWidth * labelScale, 20 * labelScale, obstacles, 4 * scale) : null;
|
|
384
|
+
if (mid) {
|
|
385
|
+
obstacles.push({
|
|
386
|
+
x: mid.x - (pillWidth / 2) * labelScale,
|
|
387
|
+
y: mid.y - 10 * labelScale,
|
|
388
|
+
width: pillWidth * labelScale,
|
|
389
|
+
height: 20 * labelScale,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
d,
|
|
394
|
+
color,
|
|
395
|
+
directed,
|
|
396
|
+
dash,
|
|
397
|
+
labelScale,
|
|
398
|
+
marks,
|
|
399
|
+
label,
|
|
400
|
+
showLabel,
|
|
401
|
+
labelWidth,
|
|
402
|
+
notSeenText,
|
|
403
|
+
notSeenWords,
|
|
404
|
+
pillWidth,
|
|
405
|
+
labelX,
|
|
406
|
+
notSeenX,
|
|
407
|
+
curveMid,
|
|
408
|
+
mid,
|
|
409
|
+
warningText,
|
|
410
|
+
warningX: labelX + labelWidth + notSeenWidth + 6,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function EdgePath({ edge, fromNode, toNode, layout, focused, dimmed, selected, scale, part }: EdgePathProps) {
|
|
415
|
+
const {
|
|
416
|
+
d,
|
|
417
|
+
color,
|
|
418
|
+
directed,
|
|
419
|
+
dash,
|
|
420
|
+
labelScale,
|
|
421
|
+
marks,
|
|
422
|
+
label,
|
|
423
|
+
showLabel,
|
|
424
|
+
labelWidth,
|
|
425
|
+
notSeenText,
|
|
426
|
+
notSeenWords,
|
|
427
|
+
pillWidth,
|
|
428
|
+
labelX,
|
|
429
|
+
notSeenX,
|
|
430
|
+
curveMid,
|
|
431
|
+
mid,
|
|
432
|
+
} = layout;
|
|
302
433
|
|
|
303
434
|
const pathId = `edge-path-${edge.id}`;
|
|
304
435
|
const select = (e: MouseEvent) => {
|
|
@@ -306,12 +437,15 @@ function EdgePath({ edge, fromNode, toNode, start, end, focused, dimmed, selecte
|
|
|
306
437
|
// that LANDED on an edge must not immediately undo itself.
|
|
307
438
|
e.stopPropagation();
|
|
308
439
|
selectedEdgeId.value = edge.id;
|
|
440
|
+
edgeHint.value = null;
|
|
309
441
|
};
|
|
310
442
|
const showHint = (anchor: Element, point?: { x: number; y: number }) => {
|
|
443
|
+
if (selectedEdgeId.value === edge.id) return;
|
|
311
444
|
const text = [
|
|
312
445
|
`${String(fromNode.data.title ?? fromNode.id)} → ${String(toNode.data.title ?? toNode.id)}`,
|
|
313
446
|
relationText(edge),
|
|
314
447
|
linkHint(edge, marks.notSeen),
|
|
448
|
+
...relationWarning(edge),
|
|
315
449
|
]
|
|
316
450
|
.filter(Boolean)
|
|
317
451
|
.join('\n');
|
|
@@ -355,6 +489,7 @@ function EdgePath({ edge, fromNode, toNode, start, end, focused, dimmed, selecte
|
|
|
355
489
|
transform={`translate(${mid.x}, ${mid.y})`}
|
|
356
490
|
class="edge-pill"
|
|
357
491
|
data-testid="edge-pill"
|
|
492
|
+
data-edge-id={edge.id}
|
|
358
493
|
opacity={dimmed && !selected ? 0.35 : 1}
|
|
359
494
|
>
|
|
360
495
|
<rect
|
|
@@ -410,12 +545,39 @@ function EdgePath({ edge, fromNode, toNode, start, end, focused, dimmed, selecte
|
|
|
410
545
|
)}
|
|
411
546
|
</g>
|
|
412
547
|
)}
|
|
548
|
+
{layout.warningText && (
|
|
549
|
+
<text
|
|
550
|
+
class="edge-label"
|
|
551
|
+
x={layout.warningX * labelScale}
|
|
552
|
+
text-anchor="start"
|
|
553
|
+
dominant-baseline="central"
|
|
554
|
+
fill="var(--c-warn)"
|
|
555
|
+
>
|
|
556
|
+
{layout.warningText}
|
|
557
|
+
</text>
|
|
558
|
+
)}
|
|
413
559
|
</g>
|
|
414
560
|
);
|
|
415
561
|
}
|
|
416
562
|
|
|
417
563
|
return (
|
|
418
564
|
<g>
|
|
565
|
+
{/* A displaced label remains attached to its original curve, not a new
|
|
566
|
+
relation. No arrow; same semantic ink, below cards like the curve. */}
|
|
567
|
+
{mid && curveMid && (mid.x !== curveMid.x || mid.y !== curveMid.y) && (
|
|
568
|
+
<line
|
|
569
|
+
data-testid="edge-label-leader"
|
|
570
|
+
data-edge-id={edge.id}
|
|
571
|
+
x1={curveMid.x}
|
|
572
|
+
y1={curveMid.y}
|
|
573
|
+
x2={mid.x}
|
|
574
|
+
y2={mid.y}
|
|
575
|
+
stroke={color}
|
|
576
|
+
stroke-width={scale}
|
|
577
|
+
stroke-dasharray={`${3 * scale} ${3 * scale}`}
|
|
578
|
+
opacity={dimmed && !selected ? 0.2 : 0.85}
|
|
579
|
+
/>
|
|
580
|
+
)}
|
|
419
581
|
{/* Invisible wide hitbox — the ONE interactive part of the edge layer
|
|
420
582
|
(the svg itself is pointer-events:none): click selects the edge
|
|
421
583
|
(Delete then removes it); usePanZoom owns right-click versus drag. */}
|
|
@@ -462,52 +624,146 @@ function EdgePath({ edge, fromNode, toNode, start, end, focused, dimmed, selecte
|
|
|
462
624
|
interface EdgeLayerProps {
|
|
463
625
|
nodes: Signal<Map<string, CanvasNodeState>>;
|
|
464
626
|
edges: Signal<Map<string, CanvasEdge>>;
|
|
465
|
-
part: EdgeLayerPart;
|
|
466
627
|
}
|
|
467
628
|
|
|
468
|
-
export function EdgeLayer({ nodes, edges
|
|
469
|
-
const
|
|
470
|
-
const
|
|
629
|
+
export function EdgeLayer({ nodes, edges }: EdgeLayerProps) {
|
|
630
|
+
const textContext = useMemo(() => document.createElement('canvas').getContext('2d'), []);
|
|
631
|
+
const font = useMemo(
|
|
632
|
+
() =>
|
|
633
|
+
signal({
|
|
634
|
+
family: getComputedStyle(document.documentElement).getPropertyValue('--font-ui').trim() || 'sans-serif',
|
|
635
|
+
version: 0,
|
|
636
|
+
}),
|
|
637
|
+
[],
|
|
638
|
+
);
|
|
639
|
+
useLayoutEffect(() => {
|
|
640
|
+
// Re-measure when Plex replaces the fallback font, without waiting for a
|
|
641
|
+
// pan/zoom. All three paint/hit layers share the same glyph advances.
|
|
642
|
+
const refresh = () => {
|
|
643
|
+
font.value = {
|
|
644
|
+
family: getComputedStyle(document.documentElement).getPropertyValue('--font-ui').trim() || 'sans-serif',
|
|
645
|
+
version: font.value.version + 1,
|
|
646
|
+
};
|
|
647
|
+
};
|
|
648
|
+
document.fonts?.addEventListener('loadingdone', refresh);
|
|
649
|
+
return () => document.fonts?.removeEventListener('loadingdone', refresh);
|
|
650
|
+
}, []);
|
|
651
|
+
const geometry = useMemo(
|
|
652
|
+
() =>
|
|
653
|
+
computed(() => {
|
|
654
|
+
const family = font.value.family;
|
|
655
|
+
const textWidth = (text: string, weight = 400) => {
|
|
656
|
+
// Non-rendering DOMs have no canvas context. Keep their bounds conservative.
|
|
657
|
+
if (!textContext) return text.length * 11;
|
|
658
|
+
textContext.font = `${weight} 11px ${family}`;
|
|
659
|
+
const metrics = textContext.measureText(text);
|
|
660
|
+
return Math.ceil(Math.max(metrics.width, metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight));
|
|
661
|
+
};
|
|
662
|
+
const nodeMap = nodes.value;
|
|
663
|
+
const inside = groupContents.value;
|
|
664
|
+
const edgeList = Array.from(edges.value.values()).filter(
|
|
665
|
+
(edge) => !inside || inside.has(edge.from) || inside.has(edge.to),
|
|
666
|
+
);
|
|
667
|
+
const resolve = (id: string) => {
|
|
668
|
+
const visible = visibleNodeFor(id);
|
|
669
|
+
return nodeMap.get(visible?.id ?? id) ?? nodeMap.get(id);
|
|
670
|
+
};
|
|
671
|
+
const pairs = new Map<string, number>();
|
|
672
|
+
const pairKey = (edge: CanvasEdge) => JSON.stringify([edge.from, edge.to].sort());
|
|
673
|
+
for (const edge of edgeList) if (!edge.toBoardId) pairs.set(pairKey(edge), (pairs.get(pairKey(edge)) ?? 0) + 1);
|
|
674
|
+
const scale = edgeChromeScale(edgeZoom.value);
|
|
675
|
+
|
|
676
|
+
// Allocate a port to every incident link, including parallel and reverse links.
|
|
677
|
+
const ports = new Map<string, { start: Anchor; end: Anchor }>();
|
|
678
|
+
const sides = new Map<string, Array<{ anchor: Anchor; node: CanvasNodeState; order: number; id: string }>>();
|
|
679
|
+
for (const edge of edgeList) {
|
|
680
|
+
if (edge.toBoardId) continue;
|
|
681
|
+
const from = resolve(edge.from);
|
|
682
|
+
const to = resolve(edge.to);
|
|
683
|
+
if (!from || !to || from.id === to.id) continue;
|
|
684
|
+
const start = computeAnchor(from, to);
|
|
685
|
+
const end = computeAnchor(to, from);
|
|
686
|
+
ports.set(edge.id, { start, end });
|
|
687
|
+
for (const [anchor, node, other] of [
|
|
688
|
+
[start, from, to],
|
|
689
|
+
[end, to, from],
|
|
690
|
+
] as const) {
|
|
691
|
+
const key = `${node.id}:${anchor.side}`;
|
|
692
|
+
const vertical = anchor.side === 'left' || anchor.side === 'right';
|
|
693
|
+
const group = sides.get(key) ?? [];
|
|
694
|
+
group.push({ anchor, node, order: vertical ? other.position.y : other.position.x, id: edge.id });
|
|
695
|
+
sides.set(key, group);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
for (const group of sides.values()) {
|
|
699
|
+
if (group.length < 2) continue;
|
|
700
|
+
group.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
|
|
701
|
+
group.forEach(({ anchor, node }, index) => {
|
|
702
|
+
const fraction = (index + 1) / (group.length + 1);
|
|
703
|
+
if (anchor.side === 'left' || anchor.side === 'right')
|
|
704
|
+
anchor.y = node.position.y + node.size.height * fraction;
|
|
705
|
+
else anchor.x = node.position.x + node.size.width * fraction;
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Reserve the actual four outward handle rectangles, not a padded card:
|
|
710
|
+
// padding the whole card falsely closes row gaps at fractional zoom.
|
|
711
|
+
// Expanded groups have only their 24px edge row, and no ports.
|
|
712
|
+
const obstacles: LabelObstacle[] = [];
|
|
713
|
+
const portSize = 32 / edgeZoom.value;
|
|
714
|
+
for (const node of nodeMap.values()) {
|
|
715
|
+
if (resolve(node.id)?.id !== node.id) continue;
|
|
716
|
+
const frame = node.type === 'group' && !node.collapsed;
|
|
717
|
+
const { x, y } = node.position;
|
|
718
|
+
const { width, height } = node.size;
|
|
719
|
+
obstacles.push({
|
|
720
|
+
x,
|
|
721
|
+
y: y - (frame ? 12 : 0),
|
|
722
|
+
width,
|
|
723
|
+
height: frame ? 24 : height,
|
|
724
|
+
});
|
|
725
|
+
if (node.type !== 'group') {
|
|
726
|
+
for (const [px, py] of [
|
|
727
|
+
[x + width / 2 - portSize / 2, y - portSize],
|
|
728
|
+
[x + width / 2 - portSize / 2, y + height],
|
|
729
|
+
[x - portSize, y + height / 2 - portSize / 2],
|
|
730
|
+
[x + width, y + height / 2 - portSize / 2],
|
|
731
|
+
]) {
|
|
732
|
+
obstacles.push({ x: px, y: py, width: portSize, height: portSize });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const layouts = new Map<string, ReturnType<typeof edgeLayout>>();
|
|
737
|
+
for (const edge of edgeList) {
|
|
738
|
+
const anchors = ports.get(edge.id);
|
|
739
|
+
if (anchors)
|
|
740
|
+
layouts.set(
|
|
741
|
+
edge.id,
|
|
742
|
+
edgeLayout(edge, anchors.start, anchors.end, scale, obstacles, textWidth, pairs.get(pairKey(edge)) ?? 1),
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
return { nodeMap, edgeList, layouts, scale, resolve };
|
|
746
|
+
}),
|
|
747
|
+
[nodes, edges, textContext, font],
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
const { nodeMap, edgeList, layouts, scale, resolve } = geometry.value;
|
|
751
|
+
const selected = selectedEdgeId.value;
|
|
752
|
+
const layout = selected ? layouts.get(selected) : null;
|
|
753
|
+
const point = layout?.mid ?? layout?.curveMid;
|
|
754
|
+
useLayoutEffect(() => {
|
|
755
|
+
edgeEditorAnchor.value = selected && point ? { id: selected, incarnation: boardIncarnation, point } : null;
|
|
756
|
+
}, [selected, point?.x, point?.y, boardIncarnation]);
|
|
757
|
+
useEffect(
|
|
758
|
+
() => () => {
|
|
759
|
+
edgeEditorAnchor.value = null;
|
|
760
|
+
},
|
|
761
|
+
[],
|
|
762
|
+
);
|
|
471
763
|
const focusId = relationFocus.value;
|
|
472
764
|
const hasFocus = focusId !== null;
|
|
473
765
|
const searchSet = searchHighlightIds.value;
|
|
474
766
|
const hasSearch = searchSet !== null;
|
|
475
|
-
const scale = edgeChromeScale(viewport.value.scale);
|
|
476
|
-
|
|
477
|
-
// Allocate a port to every incident link, including parallel and reverse links.
|
|
478
|
-
const ports = new Map<string, { start: Anchor; end: Anchor }>();
|
|
479
|
-
const sides = new Map<string, Array<{ anchor: Anchor; node: CanvasNodeState; order: number; id: string }>>();
|
|
480
|
-
for (const edge of edgeList) {
|
|
481
|
-
if (edge.toBoardId) continue;
|
|
482
|
-
const from = visibleNodeFor(edge.from);
|
|
483
|
-
const to = visibleNodeFor(edge.to);
|
|
484
|
-
if (!from || !to || from.id === to.id) continue;
|
|
485
|
-
const start = computeAnchor(from, to);
|
|
486
|
-
const end = computeAnchor(to, from);
|
|
487
|
-
ports.set(edge.id, { start, end });
|
|
488
|
-
for (const [anchor, node, other] of [
|
|
489
|
-
[start, from, to],
|
|
490
|
-
[end, to, from],
|
|
491
|
-
] as const) {
|
|
492
|
-
const key = `${node.id}:${anchor.side}`;
|
|
493
|
-
const vertical = anchor.side === 'left' || anchor.side === 'right';
|
|
494
|
-
const group = sides.get(key) ?? [];
|
|
495
|
-
group.push({ anchor, node, order: vertical ? other.position.y : other.position.x, id: edge.id });
|
|
496
|
-
sides.set(key, group);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
for (const group of sides.values()) {
|
|
500
|
-
if (group.length < 2) continue;
|
|
501
|
-
group.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
|
|
502
|
-
group.forEach(({ anchor, node }, index) => {
|
|
503
|
-
const fraction = (index + 1) / (group.length + 1);
|
|
504
|
-
if (anchor.side === 'left' || anchor.side === 'right') anchor.y = node.position.y + node.size.height * fraction;
|
|
505
|
-
else anchor.x = node.position.x + node.size.width * fraction;
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// A drag-to-connect preview must draw on a board with no edges yet.
|
|
510
|
-
if (edgeList.length === 0 && (part !== 'lines' || !draggingEdge.value)) return null;
|
|
511
767
|
|
|
512
768
|
const PAD = 96;
|
|
513
769
|
const worldNodes = Array.from(nodeMap.values());
|
|
@@ -520,128 +776,139 @@ export function EdgeLayer({ nodes, edges, part }: EdgeLayerProps) {
|
|
|
520
776
|
const height = Math.max(1, maxY - minY);
|
|
521
777
|
|
|
522
778
|
return (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
779
|
+
<>
|
|
780
|
+
{(['lines', 'pills', 'pill-hits'] as const).map((part) =>
|
|
781
|
+
// A drag-to-connect preview must draw on a board with no edges yet.
|
|
782
|
+
edgeList.length === 0 && (part !== 'lines' || !draggingEdge.value) ? null : (
|
|
783
|
+
<svg
|
|
784
|
+
key={part}
|
|
785
|
+
aria-label={part === 'lines' ? 'Canvas connections' : undefined}
|
|
786
|
+
aria-hidden={part !== 'lines' ? 'true' : undefined}
|
|
787
|
+
role={part === 'lines' ? 'img' : undefined}
|
|
788
|
+
viewBox={`${minX} ${minY} ${width} ${height}`}
|
|
789
|
+
width={width}
|
|
790
|
+
height={height}
|
|
791
|
+
style={{
|
|
792
|
+
position: 'absolute',
|
|
793
|
+
top: `${minY}px`,
|
|
794
|
+
left: `${minX}px`,
|
|
795
|
+
// Painting stays unchanged. Hit priority is card > port > pill > group
|
|
796
|
+
// interior > line, including raised and nested group frames.
|
|
797
|
+
zIndex:
|
|
798
|
+
part === 'pills'
|
|
799
|
+
? 1
|
|
800
|
+
: part === 'pill-hits'
|
|
801
|
+
? 'calc(var(--node-port-z, -1) - 1)'
|
|
802
|
+
: 'var(--edge-line-z, -5)',
|
|
803
|
+
pointerEvents: 'none',
|
|
804
|
+
overflow: 'visible',
|
|
805
|
+
'--edge-chrome-scale': scale.toFixed(3),
|
|
806
|
+
'--edge-label-scale': Math.min(scale, 2.2).toFixed(3),
|
|
807
|
+
}}
|
|
808
|
+
>
|
|
809
|
+
{part === 'lines' && <title>Canvas connections</title>}
|
|
810
|
+
<defs>
|
|
811
|
+
{/* userSpaceOnUse decouples the arrowhead from the UNCAPPED stroke
|
|
547
812
|
compensation: the line stays a visible hairline at any zoom, but
|
|
548
813
|
the head caps at the node-chrome scale (2.2) instead of dwarfing
|
|
549
814
|
sliver nodes at overview zoom. */}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
815
|
+
<marker
|
|
816
|
+
id="edge-arrow"
|
|
817
|
+
viewBox="0 0 10 10"
|
|
818
|
+
refX="9"
|
|
819
|
+
refY="5"
|
|
820
|
+
markerUnits="userSpaceOnUse"
|
|
821
|
+
markerWidth={8 * Math.min(scale, 2.2)}
|
|
822
|
+
markerHeight={8 * Math.min(scale, 2.2)}
|
|
823
|
+
orient="auto-start-reverse"
|
|
824
|
+
>
|
|
825
|
+
<path d="M 0 1 L 10 5 L 0 9 z" fill="context-stroke" opacity="0.75" />
|
|
826
|
+
</marker>
|
|
827
|
+
</defs>
|
|
828
|
+
{edgeList.map((edge) => {
|
|
829
|
+
// Remote endpoints are navigable links, never a same-ID card on this board.
|
|
830
|
+
if (edge.toBoardId) return null;
|
|
831
|
+
// An endpoint hidden behind a collapsed group's chip is drawn to the chip.
|
|
832
|
+
const fromNode = resolve(edge.from);
|
|
833
|
+
const toNode = resolve(edge.to);
|
|
834
|
+
const layout = layouts.get(edge.id);
|
|
835
|
+
if (!fromNode || !toNode || !layout) return null;
|
|
836
|
+
const isConnected = hasFocus && (edge.from === focusId || edge.to === focusId);
|
|
837
|
+
const searchDimmed = hasSearch && !(searchSet.has(edge.from) || searchSet.has(edge.to));
|
|
838
|
+
const highlight = relationHighlight.value;
|
|
839
|
+
return (
|
|
840
|
+
<EdgePath
|
|
841
|
+
key={edge.id}
|
|
842
|
+
edge={edge}
|
|
843
|
+
fromNode={fromNode}
|
|
844
|
+
toNode={toNode}
|
|
845
|
+
layout={layout}
|
|
846
|
+
focused={isConnected}
|
|
847
|
+
dimmed={(highlight !== null && !highlight.links.has(edge.id)) || searchDimmed}
|
|
848
|
+
selected={selectedEdgeId.value === edge.id}
|
|
849
|
+
scale={scale}
|
|
850
|
+
part={part}
|
|
851
|
+
/>
|
|
852
|
+
);
|
|
853
|
+
})}
|
|
854
|
+
{/* Live preview edge while drag-connecting */}
|
|
855
|
+
{part === 'lines' &&
|
|
856
|
+
draggingEdge.value &&
|
|
857
|
+
(() => {
|
|
858
|
+
const de = draggingEdge.value;
|
|
859
|
+
// Route the preview EXACTLY like the committed edge will route
|
|
860
|
+
// (side-tangent anchors against a 1px rect at the cursor) — the old
|
|
861
|
+
// collinear preview made the line visibly JUMP on release.
|
|
862
|
+
const fromNode = visibleNodeFor(de.fromId);
|
|
863
|
+
const cursorRect = {
|
|
864
|
+
position: { x: de.cursorX, y: de.cursorY },
|
|
865
|
+
size: { width: 1, height: 1 },
|
|
866
|
+
} as CanvasNodeState;
|
|
867
|
+
let previewD: string;
|
|
868
|
+
if (fromNode) {
|
|
869
|
+
const start = computeAnchor(fromNode, cursorRect);
|
|
870
|
+
const end = computeAnchor(cursorRect, fromNode);
|
|
871
|
+
const pdx = end.x - start.x;
|
|
872
|
+
const pdy = end.y - start.y;
|
|
873
|
+
const pdist = Math.sqrt(pdx * pdx + pdy * pdy);
|
|
874
|
+
const curvature = Math.min(Math.max(pdist * 0.35, 32), 160);
|
|
875
|
+
const cp1 = sideControlPoint(start, curvature);
|
|
876
|
+
const cp2 = sideControlPoint(end, curvature);
|
|
877
|
+
previewD = `M ${start.x} ${start.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${end.x} ${end.y}`;
|
|
878
|
+
} else {
|
|
879
|
+
const dx = de.cursorX - de.fromX;
|
|
880
|
+
const dy = de.cursorY - de.fromY;
|
|
881
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
882
|
+
const curve = Math.min(dist * 0.25, 80);
|
|
883
|
+
const nx = dx / (dist || 1);
|
|
884
|
+
const ny = dy / (dist || 1);
|
|
885
|
+
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}`;
|
|
886
|
+
}
|
|
887
|
+
return (
|
|
888
|
+
<g>
|
|
889
|
+
<path
|
|
890
|
+
d={previewD}
|
|
891
|
+
fill="none"
|
|
892
|
+
stroke="var(--c-accent)"
|
|
893
|
+
stroke-width={6 * scale}
|
|
894
|
+
opacity="0.1"
|
|
895
|
+
style={{ filter: 'blur(3px)' }}
|
|
896
|
+
/>
|
|
897
|
+
<path
|
|
898
|
+
d={previewD}
|
|
899
|
+
fill="none"
|
|
900
|
+
stroke="var(--c-accent)"
|
|
901
|
+
stroke-width={2 * scale}
|
|
902
|
+
stroke-dasharray={`${6 * scale} ${4 * scale}`}
|
|
903
|
+
opacity="0.8"
|
|
904
|
+
/>
|
|
905
|
+
<circle cx={de.cursorX} cy={de.cursorY} r={5 * scale} fill="var(--c-accent)" opacity="0.5" />
|
|
906
|
+
</g>
|
|
907
|
+
);
|
|
908
|
+
})()}
|
|
909
|
+
</svg>
|
|
910
|
+
),
|
|
911
|
+
)}
|
|
912
|
+
</>
|
|
646
913
|
);
|
|
647
914
|
}
|