pmx-canvas 0.8.2 → 0.9.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 +601 -22
- package/CHANGELOG.md +49 -0
- package/Readme.md +131 -532
- package/dist/canvas/global.css +2724 -476
- package/dist/canvas/index.js +168 -107
- package/dist/canvas/mcp-app.html +2899 -596
- package/dist/json-render/index.js +20 -20
- package/dist/types/client/canvas/BarHint.d.ts +12 -2
- package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
- package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
- package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
- package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
- package/dist/types/client/canvas/CommandBar.d.ts +3 -4
- package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
- package/dist/types/client/canvas/ContextChip.d.ts +3 -0
- package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
- package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
- package/dist/types/client/canvas/EmptyState.d.ts +4 -4
- package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
- package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
- package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
- package/dist/types/client/canvas/HomeView.d.ts +9 -7
- package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
- package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
- package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
- package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
- package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
- package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
- package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
- package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
- package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
- package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
- package/dist/types/client/canvas/canvas-area.d.ts +1 -1
- package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
- package/dist/types/client/canvas/graph-model.d.ts +37 -0
- package/dist/types/client/canvas/memory-layout.d.ts +23 -0
- package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
- package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
- package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
- package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
- package/dist/types/client/icons.d.ts +0 -10
- package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
- package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
- package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
- package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
- package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
- package/dist/types/client/state/boards-store.d.ts +81 -9
- package/dist/types/client/state/canvas-store.d.ts +14 -1
- package/dist/types/client/state/context-chip-store.d.ts +22 -0
- package/dist/types/client/state/context-status-store.d.ts +35 -0
- package/dist/types/client/state/intent-bridge.d.ts +11 -8
- package/dist/types/client/state/knowledge-store.d.ts +22 -0
- package/dist/types/client/state/near-pin-store.d.ts +44 -1
- package/dist/types/client/state/presence-store.d.ts +4 -4
- package/dist/types/client/state/presentation.d.ts +28 -0
- package/dist/types/client/state/relation-store.d.ts +10 -0
- package/dist/types/client/state/session-store.d.ts +41 -2
- package/dist/types/client/state/workspace-settings.d.ts +7 -0
- package/dist/types/client/types.d.ts +13 -1
- package/dist/types/client/utils/keyboard.d.ts +2 -0
- package/dist/types/client/utils/time-ago.d.ts +4 -0
- package/dist/types/mcp/context-uri-template.d.ts +3 -1
- package/dist/types/server/agent-presence.d.ts +10 -3
- package/dist/types/server/ax-context.d.ts +1 -1
- package/dist/types/server/canvas-db.d.ts +30 -7
- package/dist/types/server/canvas-operations.d.ts +24 -2
- package/dist/types/server/canvas-provenance.d.ts +22 -2
- package/dist/types/server/canvas-serialization.d.ts +2 -0
- package/dist/types/server/canvas-state.d.ts +93 -15
- package/dist/types/server/canvas-validation.d.ts +1 -1
- package/dist/types/server/context-brief.d.ts +94 -6
- package/dist/types/server/context-reads.d.ts +33 -7
- package/dist/types/server/edit-summary.d.ts +20 -0
- package/dist/types/server/index.d.ts +71 -10
- package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
- package/dist/types/server/lifecycle-status.d.ts +10 -0
- package/dist/types/server/memory-graph.d.ts +9 -0
- package/dist/types/server/mutation-history.d.ts +2 -0
- package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
- package/dist/types/server/operations/ops/boards.d.ts +2 -1
- package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
- package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
- package/dist/types/server/operations/ops/nodes.d.ts +5 -0
- package/dist/types/server/operations/ops/query.d.ts +23 -0
- package/dist/types/server/operations/registry.d.ts +1 -0
- package/dist/types/server/placement.d.ts +7 -0
- package/dist/types/server/scope-fence.d.ts +2 -1
- package/dist/types/server/spatial-analysis.d.ts +6 -3
- package/dist/types/server/tour-control.d.ts +32 -0
- package/dist/types/server/wiki-links.d.ts +14 -0
- package/dist/types/server/workbench-auth.d.ts +2 -0
- package/dist/types/shared/agent-context.d.ts +2 -0
- package/dist/types/shared/agent-presence.d.ts +15 -12
- package/dist/types/shared/auto-arrange.d.ts +1 -0
- package/dist/types/shared/boards.d.ts +4 -4
- package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
- package/dist/types/shared/knowledge-presets.d.ts +6 -0
- package/dist/types/shared/lifecycle-status.d.ts +24 -0
- package/dist/types/shared/memory-graph.d.ts +53 -0
- package/dist/types/shared/relations.d.ts +68 -0
- package/dist/types/shared/semantic-attention.d.ts +2 -0
- package/dist/types/shared/tour.d.ts +38 -0
- package/dist/types/shared/workspace-settings.d.ts +11 -0
- package/docs/RELEASE.md +21 -3
- package/docs/adr-001-bun-only-runtime.md +7 -1
- package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
- package/docs/api-reference.md +109 -0
- package/docs/ax-host-adapter-contract.md +1 -1
- package/docs/ax-state-contract.md +5 -4
- package/docs/cli.md +153 -15
- package/docs/demos/pmx-canvas-demo.mp4 +0 -0
- package/docs/design/AgentContext.dc.html +3 -3
- package/docs/design/BoardPins.dc.html +37 -22
- package/docs/design/Categories.dc.html +208 -0
- package/docs/design/CategoryEditor.dc.html +33 -0
- package/docs/design/Chrome.dc.html +52 -9
- package/docs/design/ContextChip.dc.html +224 -0
- package/docs/design/ContextChip600.dc.html +136 -0
- package/docs/design/EmptyState.dc.html +45 -0
- package/docs/design/Flows.dc.html +23 -1
- package/docs/design/HeaderMarks.dc.html +183 -0
- package/docs/design/Home.dc.html +19 -1
- package/docs/design/HomeGraph.dc.html +86 -202
- package/docs/design/HomeMap.dc.html +23 -1
- package/docs/design/HomeTeam.dc.html +32 -14
- package/docs/design/Lifecycle.dc.html +22 -0
- package/docs/design/Lifecycle600.dc.html +18 -0
- package/docs/design/LifecycleVariants.dc.html +157 -0
- package/docs/design/LinkAuthorship.dc.html +139 -0
- package/docs/design/LinksChanged.dc.html +203 -0
- package/docs/design/LinksOptions.dc.html +137 -0
- package/docs/design/LinksOptions600.dc.html +134 -0
- package/docs/design/NearPin.dc.html +9 -10
- package/docs/design/Pane600.dc.html +4 -3
- package/docs/design/Relations.dc.html +3 -2
- package/docs/design/Restore.dc.html +152 -0
- package/docs/design/SeeChange.dc.html +267 -0
- package/docs/design/Uses.dc.html +12 -6
- package/docs/design/canvas.json +146 -19
- package/docs/design.md +449 -16
- package/docs/environment.md +0 -1
- package/docs/evals/curation-effect.md +128 -0
- package/docs/http-api.md +459 -37
- package/docs/mcp.md +146 -18
- package/docs/node-types.md +58 -3
- package/docs/plans/design-gaps-2026-10-08.md +53 -0
- package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
- package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
- package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
- package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
- package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
- package/docs/product-context-vision-2026-09.md +17 -1
- package/docs/product-vision-2026-09.md +1082 -43
- package/docs/product-vision-review-2026-09.md +13 -1
- package/docs/screenshots/workbench-live-dark.png +0 -0
- package/docs/screenshots/workbench-live-light.png +0 -0
- package/docs/sdk.md +62 -2
- package/package.json +1 -1
- 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 +601 -22
- package/skills/pmx-canvas/SKILL.md +216 -42
- package/skills/pmx-canvas/references/full-reference.md +120 -21
- package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
- package/skills/pmx-canvas/references/importing-context.md +132 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
- package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
- package/skills/pmx-canvas-testing/SKILL.md +20 -0
- package/src/cli/agent.ts +2 -0
- package/src/cli/commands/ax.ts +12 -0
- package/src/cli/commands/boards.ts +87 -11
- package/src/cli/commands/edges.ts +28 -0
- package/src/cli/commands/groups.ts +7 -0
- package/src/cli/commands/nodes.ts +1 -0
- package/src/cli/commands/query.ts +34 -2
- package/src/cli/commands/record.ts +199 -48
- package/src/cli/index.ts +6 -1
- package/src/client/App.tsx +177 -70
- package/src/client/canvas/AttentionHistory.tsx +0 -2
- package/src/client/canvas/BarHint.tsx +40 -2
- package/src/client/canvas/BoardCopyDialog.tsx +1 -1
- package/src/client/canvas/BoardFolderDialog.tsx +7 -7
- package/src/client/canvas/BoardLinkDialog.tsx +138 -33
- package/src/client/canvas/BoardPinButton.tsx +33 -0
- package/src/client/canvas/BoardSwitcher.tsx +26 -40
- package/src/client/canvas/CanvasNode.tsx +360 -237
- package/src/client/canvas/CanvasViewport.tsx +45 -7
- package/src/client/canvas/CommandBar.tsx +29 -23
- package/src/client/canvas/CommandPalette.tsx +5 -12
- package/src/client/canvas/ConnectionBanner.tsx +3 -3
- package/src/client/canvas/ContextChip.tsx +296 -0
- package/src/client/canvas/ContextMenu.tsx +63 -5
- package/src/client/canvas/CrossBoardReferences.tsx +60 -0
- package/src/client/canvas/EdgeLayer.tsx +339 -76
- package/src/client/canvas/EmptyState.tsx +33 -18
- package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
- package/src/client/canvas/ExternalWriters.tsx +57 -42
- package/src/client/canvas/HomeGraphView.tsx +942 -0
- package/src/client/canvas/HomeMapView.tsx +641 -0
- package/src/client/canvas/HomeReviewView.tsx +167 -0
- package/src/client/canvas/HomeView.tsx +687 -160
- package/src/client/canvas/IntentLayer.tsx +19 -12
- package/src/client/canvas/KnowledgeDialog.tsx +488 -0
- package/src/client/canvas/LifecycleChrome.tsx +133 -0
- package/src/client/canvas/LifecycleFields.tsx +91 -0
- package/src/client/canvas/LifecyclePanel.tsx +533 -0
- package/src/client/canvas/MemoryInspector.tsx +104 -0
- package/src/client/canvas/Minimap.tsx +3 -3
- package/src/client/canvas/NearPinLayer.tsx +187 -0
- package/src/client/canvas/NodeContextMark.tsx +175 -97
- package/src/client/canvas/Presentation.tsx +23 -57
- package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
- package/src/client/canvas/RelationsPanel.tsx +82 -0
- package/src/client/canvas/SelectionBar.tsx +3 -6
- package/src/client/canvas/SessionPanel.tsx +90 -50
- package/src/client/canvas/SessionReceipt.tsx +311 -39
- package/src/client/canvas/ShortcutOverlay.tsx +1 -1
- package/src/client/canvas/SnapshotPanel.tsx +27 -36
- package/src/client/canvas/ToolRail.tsx +367 -192
- package/src/client/canvas/TopBar.tsx +354 -124
- package/src/client/canvas/canvas-area.ts +10 -3
- package/src/client/canvas/graph-geometry.ts +195 -0
- package/src/client/canvas/graph-model.ts +254 -0
- package/src/client/canvas/memory-layout.ts +106 -0
- package/src/client/canvas/use-chrome-height.ts +20 -0
- package/src/client/canvas/use-focus-trap.ts +31 -5
- package/src/client/canvas/use-memory-graph.ts +58 -0
- package/src/client/canvas/use-node-drag.ts +2 -1
- package/src/client/canvas/use-pan-zoom.ts +64 -7
- package/src/client/icons.tsx +0 -63
- package/src/client/mcp-app/style.css +0 -6
- package/src/client/nodes/AxStepControls.tsx +74 -50
- package/src/client/nodes/BoardNode.tsx +5 -1
- package/src/client/nodes/ContextNode.tsx +18 -11
- package/src/client/nodes/ExtAppFrame.tsx +0 -1
- package/src/client/nodes/FileNode.tsx +46 -42
- package/src/client/nodes/HtmlNode.tsx +32 -0
- package/src/client/nodes/ImageNode.tsx +1 -1
- package/src/client/nodes/InlineFormatBar.tsx +6 -9
- package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
- package/src/client/nodes/MarkdownNode.tsx +377 -109
- package/src/client/nodes/McpAppNode.tsx +50 -20
- package/src/client/nodes/MdFormatBar.tsx +21 -18
- package/src/client/nodes/PromptNode.tsx +1 -1
- package/src/client/nodes/ResponseNode.tsx +1 -1
- package/src/client/nodes/viewer-runtime-state.ts +54 -22
- package/src/client/state/attention-bridge.ts +64 -15
- package/src/client/state/boards-store.ts +193 -39
- package/src/client/state/canvas-store.ts +99 -12
- package/src/client/state/context-chip-store.ts +70 -0
- package/src/client/state/context-status-store.ts +53 -0
- package/src/client/state/intent-bridge.ts +51 -30
- package/src/client/state/knowledge-store.ts +43 -0
- package/src/client/state/near-pin-store.ts +115 -6
- package/src/client/state/presence-store.ts +7 -10
- package/src/client/state/presentation.ts +91 -0
- package/src/client/state/relation-store.ts +81 -0
- package/src/client/state/session-store.ts +144 -18
- package/src/client/state/sse-bridge.ts +206 -29
- package/src/client/state/workspace-settings.ts +11 -0
- package/src/client/theme/global.css +2724 -476
- package/src/client/types.ts +12 -0
- package/src/client/utils/keyboard.ts +10 -0
- package/src/client/utils/time-ago.ts +14 -0
- package/src/json-render/renderer/index.tsx +31 -0
- package/src/mcp/canvas-app.ts +12 -2
- package/src/mcp/context-uri-template.ts +7 -4
- package/src/mcp/server.ts +63 -41
- package/src/mcp/workbench-app.ts +39 -6
- package/src/server/agent-presence.ts +128 -28
- package/src/server/ax-context.ts +27 -13
- package/src/server/board-export.ts +12 -1
- package/src/server/canvas-db.ts +331 -42
- package/src/server/canvas-operations.ts +195 -83
- package/src/server/canvas-provenance.ts +53 -22
- package/src/server/canvas-serialization.ts +2 -0
- package/src/server/canvas-state.ts +950 -122
- package/src/server/canvas-validation.ts +9 -2
- package/src/server/context-brief.ts +345 -38
- package/src/server/context-reads.ts +178 -30
- package/src/server/demo-state.json +355 -24
- package/src/server/edit-summary.ts +74 -0
- package/src/server/html-surface.ts +8 -1
- package/src/server/index.ts +181 -11
- package/src/server/knowledge-lifecycle.ts +304 -0
- package/src/server/lifecycle-status.ts +232 -0
- package/src/server/memory-graph.ts +166 -0
- package/src/server/mutation-history.ts +18 -6
- package/src/server/operations/composites.ts +32 -11
- package/src/server/operations/http.ts +7 -2
- package/src/server/operations/index.ts +4 -0
- package/src/server/operations/mcp.ts +26 -1
- package/src/server/operations/ops/ax-presence.ts +1 -1
- package/src/server/operations/ops/ax-read.ts +57 -6
- package/src/server/operations/ops/ax-timeline.ts +28 -2
- package/src/server/operations/ops/backup.ts +5 -3
- package/src/server/operations/ops/batch.ts +35 -4
- package/src/server/operations/ops/boards.ts +188 -47
- package/src/server/operations/ops/canvas-wire.ts +31 -1
- package/src/server/operations/ops/edges.ts +36 -2
- package/src/server/operations/ops/groups.ts +6 -1
- package/src/server/operations/ops/lifecycle.ts +262 -0
- package/src/server/operations/ops/memory-graph.ts +45 -0
- package/src/server/operations/ops/nodes.ts +107 -24
- package/src/server/operations/ops/query.ts +281 -68
- package/src/server/operations/ops/snapshots.ts +73 -1
- package/src/server/operations/ops/validate.ts +5 -1
- package/src/server/operations/ops/viewport.ts +97 -3
- package/src/server/operations/registry.ts +74 -25
- package/src/server/placement.ts +29 -0
- package/src/server/scope-fence.ts +78 -6
- package/src/server/server.ts +115 -16
- package/src/server/spatial-analysis.ts +48 -23
- package/src/server/tour-control.ts +82 -0
- package/src/server/wiki-links.ts +137 -0
- package/src/server/workbench-auth.ts +11 -0
- package/src/shared/agent-context.ts +3 -0
- package/src/shared/agent-presence.ts +15 -15
- package/src/shared/auto-arrange.ts +2 -0
- package/src/shared/boards.ts +4 -4
- package/src/shared/knowledge-lifecycle.ts +22 -0
- package/src/shared/knowledge-presets.ts +80 -0
- package/src/shared/lifecycle-status.ts +26 -0
- package/src/shared/memory-graph.ts +74 -0
- package/src/shared/relations.ts +98 -0
- package/src/shared/semantic-attention.ts +15 -1
- package/src/shared/tour.ts +23 -5
- package/src/shared/workspace-settings.ts +13 -0
- package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
- package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
- package/dist/types/client/canvas/kind-colors.d.ts +0 -3
- package/dist/types/server/agent-context.d.ts +0 -22
- package/src/client/canvas/ContextPinBar.tsx +0 -25
|
@@ -1,25 +1,138 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Signal, signal } from '@preact/signals';
|
|
2
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
2
3
|
import {
|
|
3
|
-
activeNodeId,
|
|
4
4
|
draggingEdge,
|
|
5
|
+
edges,
|
|
5
6
|
searchHighlightIds,
|
|
6
7
|
selectedEdgeId,
|
|
7
8
|
viewport,
|
|
8
9
|
visibleNodeFor,
|
|
9
10
|
} from '../state/canvas-store';
|
|
11
|
+
import { linkMarks, writerName } from '../state/context-status-store';
|
|
12
|
+
import { HUMAN_STARTED_SESSION_LABEL } from '../../shared/agent-presence';
|
|
13
|
+
import { activeSession } from '../state/presence-store';
|
|
14
|
+
import { activeBoard } from '../state/boards-store';
|
|
15
|
+
import { reportClientViewportSize } from '../state/intent-bridge';
|
|
16
|
+
import { relationDefinition, relationKind, relationText } from '../../shared/relations.js';
|
|
10
17
|
import type { CanvasEdge, CanvasNodeState } from '../types';
|
|
18
|
+
import { relationFilter, relationFocus, relationHighlight } from '../state/relation-store';
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/** Filter only the kinds on this board; shared presets are managed in Home. */
|
|
21
|
+
export function RelationLegend() {
|
|
22
|
+
const board = activeBoard();
|
|
23
|
+
const list = [...edges.value.values()];
|
|
24
|
+
const legendRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const legend = legendRef.current;
|
|
27
|
+
if (!legend) return;
|
|
28
|
+
const observer = new ResizeObserver(() => void reportClientViewportSize());
|
|
29
|
+
observer.observe(legend);
|
|
30
|
+
return () => observer.disconnect();
|
|
31
|
+
}, [board?.id, list.length > 0]);
|
|
32
|
+
if (!board || list.length === 0) return null;
|
|
33
|
+
const kinds = [...new Set(list.map(relationKind))];
|
|
34
|
+
const selected = relationFilter.value?.boardId === board.id ? relationFilter.value.kind : null;
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
ref={legendRef}
|
|
38
|
+
class="relation-legend"
|
|
39
|
+
aria-label="Relation legend"
|
|
40
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
41
|
+
>
|
|
42
|
+
<select
|
|
43
|
+
aria-label="Filter board relations"
|
|
44
|
+
value={selected ?? ''}
|
|
45
|
+
onChange={(event) => {
|
|
46
|
+
const kind = event.currentTarget.value;
|
|
47
|
+
relationFilter.value = kind ? { boardId: board.id, kind } : null;
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<option value="">All relations</option>
|
|
51
|
+
{kinds.map((kind) => (
|
|
52
|
+
<option key={kind} value={kind}>
|
|
53
|
+
{kind} · {list.filter((edge) => relationKind(edge) === kind).length}
|
|
54
|
+
</option>
|
|
55
|
+
))}
|
|
56
|
+
</select>
|
|
57
|
+
{kinds.map((kind) => {
|
|
58
|
+
const definition = relationDefinition({ kind }, board.relationVocabulary);
|
|
59
|
+
return (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
key={kind}
|
|
63
|
+
aria-pressed={selected === kind}
|
|
64
|
+
style={{ '--relation-ink': `var(--c-rel-${definition.ink})` }}
|
|
65
|
+
onClick={() => {
|
|
66
|
+
relationFilter.value = selected === kind ? null : { boardId: board.id, kind };
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<span class="relation-swatch" aria-hidden="true" />
|
|
70
|
+
{kind} <span class="relation-count">{list.filter((edge) => relationKind(edge) === kind).length}</span>
|
|
71
|
+
</button>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The attached agent's name; a session you started that no agent has joined yet names no one. */
|
|
79
|
+
function agentName(): string {
|
|
80
|
+
const label = activeSession.value?.label;
|
|
81
|
+
return label && label !== HUMAN_STARTED_SESSION_LABEL ? label : 'the agent';
|
|
82
|
+
}
|
|
21
83
|
|
|
22
|
-
|
|
84
|
+
/** The hovered link pill's hint, drawn at screen level by `EdgeHint` (the edge layer is SVG in world space). */
|
|
85
|
+
const edgeHint = signal<{ x: number; y: number; text: string; edgeId: string; view: unknown } | null>(null);
|
|
86
|
+
|
|
87
|
+
function clock(iso: string | undefined): string {
|
|
88
|
+
if (!iso) return '';
|
|
89
|
+
const date = new Date(iso);
|
|
90
|
+
return Number.isNaN(date.getTime())
|
|
91
|
+
? ''
|
|
92
|
+
: ` · ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function who(actor: NonNullable<CanvasEdge['changedBy']>): string {
|
|
96
|
+
return actor.actor === 'agent' ? writerName(actor) : 'you';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Who drew the link and when (docs/design/LinkAuthorship.dc.html): "Drawn by
|
|
101
|
+
* Codex · 14:06", or, once someone else changed it, "You relabelled it · 14:20
|
|
102
|
+
* (drawn by Codex · 14:06)". A folded "not seen" state joins it.
|
|
103
|
+
*/
|
|
104
|
+
function linkHint(edge: CanvasEdge, notSeen: boolean): string | null {
|
|
105
|
+
// A link from before authorship was recorded names no one: "you" could be false.
|
|
106
|
+
if (edge.changedBy?.actor !== 'agent' && edge.changedBy?.actor !== 'human') return null;
|
|
107
|
+
const by = who(edge.changedBy);
|
|
108
|
+
const takenOver = !!edge.createdBy && who(edge.createdBy) !== by;
|
|
109
|
+
const lead = takenOver
|
|
110
|
+
? `${by === 'you' ? 'You' : by} relabelled it${clock(edge.changedAt)} (drawn by ${edge.createdBy ? who(edge.createdBy) : ''}${clock(edge.createdAt)})`
|
|
111
|
+
: `${by === 'you' ? 'You drew it' : `Drawn by ${by}`}${clock(edge.createdAt ?? edge.changedAt)}`;
|
|
112
|
+
return notSeen ? `${lead} · not seen by ${agentName()} yet` : lead;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The link pill's hint, outside the zoomed canvas so it keeps screen size and is never clipped. */
|
|
116
|
+
export function EdgeHint() {
|
|
117
|
+
const hint = edgeHint.value;
|
|
118
|
+
// The pill can vanish or move under a resting pointer (the link removed, the
|
|
119
|
+
// view panned or zoomed) without a pointerleave: the hint goes with it.
|
|
120
|
+
if (!hint || hint.view !== viewport.value || !edges.value.has(hint.edgeId)) return null;
|
|
121
|
+
return (
|
|
122
|
+
<span
|
|
123
|
+
class="toolbar-tooltip edge-hint"
|
|
124
|
+
role="tooltip"
|
|
125
|
+
data-testid="edge-hint"
|
|
126
|
+
style={{ left: `${hint.x}px`, top: `${hint.y}px` }}
|
|
127
|
+
>
|
|
128
|
+
<span class="toolbar-tooltip-label">{hint.text}</span>
|
|
129
|
+
</span>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Connection-change glyphs (docs/design/LinksOptions.dc.html, option C), 24-unit paths.
|
|
134
|
+
const LINK_MARK_EYE_OFF =
|
|
135
|
+
'M3 3l18 18M10.6 6.1A10 10 0 0 1 12 6c6.5 0 10 6 10 6a17 17 0 0 1-3 3.6M6.6 6.6C3.8 8.3 2 12 2 12s3.5 6 10 6a9.6 9.6 0 0 0 4.4-1';
|
|
23
136
|
|
|
24
137
|
/**
|
|
25
138
|
* Edges are drawn in world space, so a 1.5px stroke renders as 0.4 screen px at
|
|
@@ -37,6 +150,8 @@ function dashArray(edge: CanvasEdge, scale: number): string | undefined {
|
|
|
37
150
|
const dashed = `${8 * scale} ${4 * scale}`;
|
|
38
151
|
if (edge.style === 'dashed') return dashed;
|
|
39
152
|
if (edge.style === 'dotted') return `${3 * scale} ${3 * scale}`;
|
|
153
|
+
if (!edge.style && relationDefinition(edge, activeBoard()?.relationVocabulary).kind === 'related')
|
|
154
|
+
return `${2 * scale} ${5 * scale}`;
|
|
40
155
|
if (edge.type === 'references' && !edge.style) return dashed;
|
|
41
156
|
return undefined;
|
|
42
157
|
}
|
|
@@ -128,17 +243,24 @@ interface EdgePathProps {
|
|
|
128
243
|
edge: CanvasEdge;
|
|
129
244
|
fromNode: CanvasNodeState;
|
|
130
245
|
toNode: CanvasNodeState;
|
|
246
|
+
start: Anchor;
|
|
247
|
+
end: Anchor;
|
|
131
248
|
focused: boolean; // connected to the active node
|
|
132
249
|
dimmed: boolean; // active node exists but this edge is NOT connected
|
|
133
250
|
selected: boolean; // click-selected — Delete removes it
|
|
134
251
|
scale: number; // inverse-viewport compensation, see edgeChromeScale()
|
|
135
|
-
|
|
252
|
+
part: EdgeLayerPart;
|
|
136
253
|
}
|
|
137
254
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Lines draw below group frames, so a link never covers a group's header; pills
|
|
257
|
+
* draw in their own layer above the frames, so a link inside a group keeps a
|
|
258
|
+
* label (LinkAuthorship.dc.html). Transparent pill hits stay below ports without
|
|
259
|
+
* moving the painted label below the frame's wash.
|
|
260
|
+
*/
|
|
261
|
+
export type EdgeLayerPart = 'lines' | 'pills' | 'pill-hits';
|
|
141
262
|
|
|
263
|
+
function EdgePath({ edge, fromNode, toNode, start, end, focused, dimmed, selected, scale, part }: EdgePathProps) {
|
|
142
264
|
const dx = end.x - start.x;
|
|
143
265
|
const dy = end.y - start.y;
|
|
144
266
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
@@ -148,66 +270,180 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed, selected, scale, on
|
|
|
148
270
|
const cp2 = sideControlPoint(end, curvature);
|
|
149
271
|
|
|
150
272
|
const d = `M ${start.x} ${start.y} C ${cp1.x} ${cp1.y}, ${cp2.x} ${cp2.y}, ${end.x} ${end.y}`;
|
|
151
|
-
const
|
|
152
|
-
const
|
|
273
|
+
const definition = relationDefinition(edge, activeBoard()?.relationVocabulary);
|
|
274
|
+
const color = `var(--c-rel-${definition.ink})`;
|
|
275
|
+
const directed = definition.directed;
|
|
153
276
|
const dash = dashArray(edge, scale);
|
|
154
277
|
|
|
155
|
-
// Labels keep a constant screen size only while nodes are still readable
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// entirely once the board is slivers (chrome scale > 3.4 ≈ zoom < 30%).
|
|
278
|
+
// Labels keep a constant screen size only while nodes are still readable —
|
|
279
|
+
// label chrome caps like node chrome (2.2). Below 35% zoom the label goes and
|
|
280
|
+
// only the link's marks stay (LinkAuthorship.dc.html).
|
|
159
281
|
const labelScale = Math.min(scale, 2.2);
|
|
160
|
-
const
|
|
161
|
-
|
|
282
|
+
const zoom = viewport.value.scale;
|
|
283
|
+
const marks = linkMarks(edge);
|
|
284
|
+
// Reasons belong in the hint/inspector, not a long pill covering both endpoints.
|
|
285
|
+
const label = edge.label ?? edge.kind;
|
|
286
|
+
const showLabel = !!label && zoom >= 0.35;
|
|
287
|
+
// Pill layout in label units (× labelScale): the label, then a hairline and
|
|
288
|
+
// "not seen by …", which keeps only its glyph on a short line or below 60% zoom.
|
|
289
|
+
const pad = showLabel ? 9 : 5;
|
|
290
|
+
const labelWidth = showLabel ? (label?.length ?? 0) * 7 : 0;
|
|
291
|
+
const notSeenText = `not seen by ${agentName()}`;
|
|
292
|
+
const wordedNotSeenWidth = 12 + notSeenText.length * 6.2 + 4;
|
|
293
|
+
const fullWidth = pad * 2 + labelWidth + (marks.notSeen ? (labelWidth ? 12 : 0) + wordedNotSeenWidth : 0);
|
|
294
|
+
// A short line: the full pill plus some line on either side would not fit.
|
|
295
|
+
const notSeenWords = marks.notSeen && zoom >= 0.6 && dist >= (fullWidth + 60) * labelScale;
|
|
296
|
+
const notSeenWidth = marks.notSeen ? (labelWidth ? 12 : 0) + (notSeenWords ? wordedNotSeenWidth : 12) : 0;
|
|
297
|
+
const pillWidth = pad * 2 + labelWidth + notSeenWidth;
|
|
298
|
+
const labelX = -pillWidth / 2 + pad;
|
|
299
|
+
const notSeenX = labelX + labelWidth + (labelWidth ? 12 : 0);
|
|
300
|
+
const hasPill = showLabel || marks.notSeen;
|
|
301
|
+
const mid = hasPill ? bezierMidpoint(start.x, start.y, cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y) : null;
|
|
162
302
|
|
|
163
303
|
const pathId = `edge-path-${edge.id}`;
|
|
304
|
+
const select = (e: MouseEvent) => {
|
|
305
|
+
// The viewport's background click clears the selection — a click
|
|
306
|
+
// that LANDED on an edge must not immediately undo itself.
|
|
307
|
+
e.stopPropagation();
|
|
308
|
+
selectedEdgeId.value = edge.id;
|
|
309
|
+
};
|
|
310
|
+
const showHint = (anchor: Element, point?: { x: number; y: number }) => {
|
|
311
|
+
const text = [
|
|
312
|
+
`${String(fromNode.data.title ?? fromNode.id)} → ${String(toNode.data.title ?? toNode.id)}`,
|
|
313
|
+
relationText(edge),
|
|
314
|
+
linkHint(edge, marks.notSeen),
|
|
315
|
+
]
|
|
316
|
+
.filter(Boolean)
|
|
317
|
+
.join('\n');
|
|
318
|
+
const rect = anchor.getBoundingClientRect();
|
|
319
|
+
edgeHint.value = {
|
|
320
|
+
x: point?.x ?? rect.left + rect.width / 2,
|
|
321
|
+
y: (point?.y ?? rect.bottom) + 8,
|
|
322
|
+
text,
|
|
323
|
+
edgeId: edge.id,
|
|
324
|
+
view: viewport.value,
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
const hideHint = () => {
|
|
328
|
+
edgeHint.value = null;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
if (part === 'pill-hits') {
|
|
332
|
+
if (!mid) return null;
|
|
333
|
+
return (
|
|
334
|
+
<rect
|
|
335
|
+
class="edge-pill-hit"
|
|
336
|
+
data-testid="edge-pill-hit"
|
|
337
|
+
data-edge-id={edge.id}
|
|
338
|
+
x={mid.x - (pillWidth / 2) * labelScale}
|
|
339
|
+
y={mid.y - 10 * labelScale}
|
|
340
|
+
width={pillWidth * labelScale}
|
|
341
|
+
height={20 * labelScale}
|
|
342
|
+
rx={10 * labelScale}
|
|
343
|
+
fill="transparent"
|
|
344
|
+
onClick={select}
|
|
345
|
+
onPointerEnter={(e) => showHint(e.currentTarget)}
|
|
346
|
+
onPointerLeave={hideHint}
|
|
347
|
+
/>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (part === 'pills') {
|
|
352
|
+
if (!mid) return null;
|
|
353
|
+
return (
|
|
354
|
+
<g
|
|
355
|
+
transform={`translate(${mid.x}, ${mid.y})`}
|
|
356
|
+
class="edge-pill"
|
|
357
|
+
data-testid="edge-pill"
|
|
358
|
+
opacity={dimmed && !selected ? 0.35 : 1}
|
|
359
|
+
>
|
|
360
|
+
<rect
|
|
361
|
+
class="edge-label-bg"
|
|
362
|
+
x={(-pillWidth / 2) * labelScale}
|
|
363
|
+
y={-10 * labelScale}
|
|
364
|
+
width={pillWidth * labelScale}
|
|
365
|
+
height={20 * labelScale}
|
|
366
|
+
rx={10 * labelScale}
|
|
367
|
+
/>
|
|
368
|
+
{showLabel && (
|
|
369
|
+
<text
|
|
370
|
+
class="edge-label"
|
|
371
|
+
x={(labelX + labelWidth / 2) * labelScale}
|
|
372
|
+
text-anchor="middle"
|
|
373
|
+
dominant-baseline="central"
|
|
374
|
+
fill={color}
|
|
375
|
+
>
|
|
376
|
+
{label}
|
|
377
|
+
</text>
|
|
378
|
+
)}
|
|
379
|
+
{marks.notSeen && (
|
|
380
|
+
<g class="edge-link-mark is-not-seen" data-testid="edge-link-mark">
|
|
381
|
+
{labelWidth > 0 && (
|
|
382
|
+
<line
|
|
383
|
+
x1={(notSeenX - 6) * labelScale}
|
|
384
|
+
x2={(notSeenX - 6) * labelScale}
|
|
385
|
+
y1={-6 * labelScale}
|
|
386
|
+
y2={6 * labelScale}
|
|
387
|
+
stroke="var(--c-line)"
|
|
388
|
+
stroke-width={labelScale}
|
|
389
|
+
/>
|
|
390
|
+
)}
|
|
391
|
+
<path
|
|
392
|
+
transform={`translate(${notSeenX * labelScale}, ${-5 * labelScale}) scale(${(10 / 24) * labelScale})`}
|
|
393
|
+
d={LINK_MARK_EYE_OFF}
|
|
394
|
+
fill="none"
|
|
395
|
+
stroke="currentColor"
|
|
396
|
+
stroke-width="2"
|
|
397
|
+
stroke-linecap="round"
|
|
398
|
+
stroke-linejoin="round"
|
|
399
|
+
/>
|
|
400
|
+
{notSeenWords && (
|
|
401
|
+
<text
|
|
402
|
+
class="edge-label"
|
|
403
|
+
x={(notSeenX + 14) * labelScale}
|
|
404
|
+
text-anchor="start"
|
|
405
|
+
dominant-baseline="central"
|
|
406
|
+
fill="currentColor"
|
|
407
|
+
>
|
|
408
|
+
{notSeenText}
|
|
409
|
+
</text>
|
|
410
|
+
)}
|
|
411
|
+
</g>
|
|
412
|
+
)}
|
|
413
|
+
</g>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
164
416
|
|
|
165
417
|
return (
|
|
166
418
|
<g>
|
|
167
419
|
{/* Invisible wide hitbox — the ONE interactive part of the edge layer
|
|
168
420
|
(the svg itself is pointer-events:none): click selects the edge
|
|
169
|
-
(Delete then removes it)
|
|
421
|
+
(Delete then removes it); usePanZoom owns right-click versus drag. */}
|
|
170
422
|
<path
|
|
423
|
+
data-edge-id={edge.id}
|
|
171
424
|
d={d}
|
|
172
425
|
fill="none"
|
|
173
426
|
stroke="transparent"
|
|
174
427
|
stroke-width={12 * scale}
|
|
175
428
|
style={{ cursor: 'pointer', pointerEvents: 'stroke' }}
|
|
176
|
-
onClick={
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
e.stopPropagation();
|
|
180
|
-
selectedEdgeId.value = edge.id;
|
|
181
|
-
}}
|
|
182
|
-
onDblClick={onContextMenu ? (e) => onContextMenu(e as unknown as MouseEvent, edge.id) : undefined}
|
|
429
|
+
onClick={select}
|
|
430
|
+
onPointerEnter={(e) => showHint(e.currentTarget, { x: e.clientX, y: e.clientY })}
|
|
431
|
+
onPointerLeave={hideHint}
|
|
183
432
|
/>
|
|
184
433
|
|
|
185
|
-
{/* Glow layer for focused or selected edges */}
|
|
186
|
-
{(focused || selected) && (
|
|
187
|
-
<path
|
|
188
|
-
d={d}
|
|
189
|
-
fill="none"
|
|
190
|
-
stroke={color}
|
|
191
|
-
stroke-width={6 * scale}
|
|
192
|
-
stroke-dasharray={dash}
|
|
193
|
-
opacity={selected ? 0.3 : 0.15}
|
|
194
|
-
style={{ filter: 'blur(3px)' }}
|
|
195
|
-
/>
|
|
196
|
-
)}
|
|
197
|
-
|
|
198
434
|
{/* Visible edge — the `edge-arrow` marker uses the default
|
|
199
435
|
markerUnits="strokeWidth", so the arrowhead scales with this. */}
|
|
200
436
|
<path
|
|
201
437
|
id={pathId}
|
|
202
438
|
d={d}
|
|
203
|
-
class={selected ? 'edge-selected' :
|
|
439
|
+
class={selected ? 'edge-line edge-selected' : 'edge-line'}
|
|
204
440
|
fill="none"
|
|
205
441
|
stroke={color}
|
|
206
442
|
stroke-width={(selected ? 3 : focused ? 2.5 : 1.5) * scale}
|
|
207
443
|
stroke-dasharray={dash}
|
|
208
444
|
marker-end={directed ? 'url(#edge-arrow)' : undefined}
|
|
209
445
|
opacity={selected ? 1 : dimmed ? 0.2 : focused ? 1 : 0.85}
|
|
210
|
-
style={{
|
|
446
|
+
style={{ color }}
|
|
211
447
|
/>
|
|
212
448
|
|
|
213
449
|
{/* Animated pulse dot */}
|
|
@@ -218,23 +454,6 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed, selected, scale, on
|
|
|
218
454
|
</animateMotion>
|
|
219
455
|
</circle>
|
|
220
456
|
)}
|
|
221
|
-
|
|
222
|
-
{/* Label at midpoint */}
|
|
223
|
-
{mid && edge.label && (
|
|
224
|
-
<g transform={`translate(${mid.x}, ${mid.y})`}>
|
|
225
|
-
<rect
|
|
226
|
-
class="edge-label-bg"
|
|
227
|
-
x={-(edge.label.length * 3.5 + 8) * labelScale}
|
|
228
|
-
y={-10 * labelScale}
|
|
229
|
-
width={(edge.label.length * 7 + 16) * labelScale}
|
|
230
|
-
height={20 * labelScale}
|
|
231
|
-
rx={4 * labelScale}
|
|
232
|
-
/>
|
|
233
|
-
<text class="edge-label" text-anchor="middle" dominant-baseline="central" fill="var(--c-text)">
|
|
234
|
-
{edge.label}
|
|
235
|
-
</text>
|
|
236
|
-
</g>
|
|
237
|
-
)}
|
|
238
457
|
</g>
|
|
239
458
|
);
|
|
240
459
|
}
|
|
@@ -243,20 +462,52 @@ function EdgePath({ edge, fromNode, toNode, focused, dimmed, selected, scale, on
|
|
|
243
462
|
interface EdgeLayerProps {
|
|
244
463
|
nodes: Signal<Map<string, CanvasNodeState>>;
|
|
245
464
|
edges: Signal<Map<string, CanvasEdge>>;
|
|
246
|
-
|
|
465
|
+
part: EdgeLayerPart;
|
|
247
466
|
}
|
|
248
467
|
|
|
249
|
-
export function EdgeLayer({ nodes, edges,
|
|
468
|
+
export function EdgeLayer({ nodes, edges, part }: EdgeLayerProps) {
|
|
250
469
|
const nodeMap = nodes.value;
|
|
251
470
|
const edgeList = Array.from(edges.value.values());
|
|
252
|
-
const focusId =
|
|
471
|
+
const focusId = relationFocus.value;
|
|
253
472
|
const hasFocus = focusId !== null;
|
|
254
473
|
const searchSet = searchHighlightIds.value;
|
|
255
474
|
const hasSearch = searchSet !== null;
|
|
256
475
|
const scale = edgeChromeScale(viewport.value.scale);
|
|
257
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
|
+
|
|
258
509
|
// A drag-to-connect preview must draw on a board with no edges yet.
|
|
259
|
-
if (edgeList.length === 0 && !draggingEdge.value) return null;
|
|
510
|
+
if (edgeList.length === 0 && (part !== 'lines' || !draggingEdge.value)) return null;
|
|
260
511
|
|
|
261
512
|
const PAD = 96;
|
|
262
513
|
const worldNodes = Array.from(nodeMap.values());
|
|
@@ -270,8 +521,9 @@ export function EdgeLayer({ nodes, edges, onEdgeContextMenu }: EdgeLayerProps) {
|
|
|
270
521
|
|
|
271
522
|
return (
|
|
272
523
|
<svg
|
|
273
|
-
aria-label=
|
|
274
|
-
|
|
524
|
+
aria-label={part === 'lines' ? 'Canvas connections' : undefined}
|
|
525
|
+
aria-hidden={part !== 'lines' ? 'true' : undefined}
|
|
526
|
+
role={part === 'lines' ? 'img' : undefined}
|
|
275
527
|
viewBox={`${minX} ${minY} ${width} ${height}`}
|
|
276
528
|
width={width}
|
|
277
529
|
height={height}
|
|
@@ -279,13 +531,17 @@ export function EdgeLayer({ nodes, edges, onEdgeContextMenu }: EdgeLayerProps) {
|
|
|
279
531
|
position: 'absolute',
|
|
280
532
|
top: `${minY}px`,
|
|
281
533
|
left: `${minX}px`,
|
|
534
|
+
// Painting stays unchanged. Hit priority is card > port > pill > group
|
|
535
|
+
// interior > line, including raised and nested group frames.
|
|
536
|
+
zIndex:
|
|
537
|
+
part === 'pills' ? 1 : part === 'pill-hits' ? 'calc(var(--node-port-z, -1) - 1)' : 'var(--edge-line-z, -5)',
|
|
282
538
|
pointerEvents: 'none',
|
|
283
539
|
overflow: 'visible',
|
|
284
540
|
'--edge-chrome-scale': scale.toFixed(3),
|
|
285
541
|
'--edge-label-scale': Math.min(scale, 2.2).toFixed(3),
|
|
286
542
|
}}
|
|
287
543
|
>
|
|
288
|
-
<title>Canvas connections</title>
|
|
544
|
+
{part === 'lines' && <title>Canvas connections</title>}
|
|
289
545
|
<defs>
|
|
290
546
|
{/* userSpaceOnUse decouples the arrowhead from the UNCAPPED stroke
|
|
291
547
|
compensation: the line stays a visible hairline at any zoom, but
|
|
@@ -301,32 +557,39 @@ export function EdgeLayer({ nodes, edges, onEdgeContextMenu }: EdgeLayerProps) {
|
|
|
301
557
|
markerHeight={8 * Math.min(scale, 2.2)}
|
|
302
558
|
orient="auto-start-reverse"
|
|
303
559
|
>
|
|
304
|
-
<path d="M 0 1 L 10 5 L 0 9 z" fill="
|
|
560
|
+
<path d="M 0 1 L 10 5 L 0 9 z" fill="context-stroke" opacity="0.75" />
|
|
305
561
|
</marker>
|
|
306
562
|
</defs>
|
|
307
563
|
{edgeList.map((edge) => {
|
|
564
|
+
// Remote endpoints are navigable links, never a same-ID card on this board.
|
|
565
|
+
if (edge.toBoardId) return null;
|
|
308
566
|
// An endpoint hidden behind a collapsed group's chip is drawn to the chip.
|
|
309
567
|
const fromNode = visibleNodeFor(edge.from);
|
|
310
568
|
const toNode = visibleNodeFor(edge.to);
|
|
311
|
-
|
|
569
|
+
const anchors = ports.get(edge.id);
|
|
570
|
+
if (!fromNode || !toNode || !anchors) return null;
|
|
312
571
|
const isConnected = hasFocus && (edge.from === focusId || edge.to === focusId);
|
|
313
572
|
const searchDimmed = hasSearch && !(searchSet.has(edge.from) || searchSet.has(edge.to));
|
|
573
|
+
const highlight = relationHighlight.value;
|
|
314
574
|
return (
|
|
315
575
|
<EdgePath
|
|
316
576
|
key={edge.id}
|
|
317
577
|
edge={edge}
|
|
318
578
|
fromNode={fromNode}
|
|
319
579
|
toNode={toNode}
|
|
580
|
+
start={anchors.start}
|
|
581
|
+
end={anchors.end}
|
|
320
582
|
focused={isConnected}
|
|
321
|
-
dimmed={(
|
|
583
|
+
dimmed={(highlight !== null && !highlight.links.has(edge.id)) || searchDimmed}
|
|
322
584
|
selected={selectedEdgeId.value === edge.id}
|
|
323
585
|
scale={scale}
|
|
324
|
-
|
|
586
|
+
part={part}
|
|
325
587
|
/>
|
|
326
588
|
);
|
|
327
589
|
})}
|
|
328
590
|
{/* Live preview edge while drag-connecting */}
|
|
329
|
-
{
|
|
591
|
+
{part === 'lines' &&
|
|
592
|
+
draggingEdge.value &&
|
|
330
593
|
(() => {
|
|
331
594
|
const de = draggingEdge.value;
|
|
332
595
|
// Route the preview EXACTLY like the committed edge will route
|
|
@@ -7,14 +7,15 @@ import { modChord } from '../utils/platform';
|
|
|
7
7
|
import { canvasAreaCenter } from './canvas-area';
|
|
8
8
|
import { importFiles } from './import-files';
|
|
9
9
|
import { promptedCreate } from './ToolRail';
|
|
10
|
+
import { agentSessionsEnabled } from '../state/workspace-settings';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Empty board (rail-chrome-v2 phase 7, design item 11): centered onboarding
|
|
13
|
-
* with
|
|
14
|
-
* Every action is real:
|
|
14
|
+
* with direct starter actions and a separate way to start with your agent.
|
|
15
|
+
* Every action is real: Start a note creates the same blank note as M, Drop files
|
|
15
16
|
* opens a picker onto the viewport's import path, Paste a link asks for a URL,
|
|
16
|
-
* Start agent session attaches a browser-keyed session.
|
|
17
|
-
*
|
|
17
|
+
* and the optional Start agent session attaches a browser-keyed session.
|
|
18
|
+
* Agent guidance stays visible independently of the session beta.
|
|
18
19
|
*/
|
|
19
20
|
export function EmptyState({ onOpenPalette }: { onOpenPalette: () => void }) {
|
|
20
21
|
const fileInput = useRef<HTMLInputElement>(null);
|
|
@@ -33,16 +34,8 @@ export function EmptyState({ onOpenPalette }: { onOpenPalette: () => void }) {
|
|
|
33
34
|
<rect x="8" y="8" width="48" height="48" rx="7" />
|
|
34
35
|
<rect x="20" y="20" width="24" height="24" rx="4" stroke-dasharray="4 4" />
|
|
35
36
|
</svg>
|
|
36
|
-
<div>
|
|
37
|
-
<div class="empty-state-title">Nothing on this board yet</div>
|
|
38
|
-
<div class="empty-state-sub">Drop files anywhere, paste a link, or start from a note.</div>
|
|
39
|
-
</div>
|
|
37
|
+
<div class="empty-state-title">What would you like to work on?</div>
|
|
40
38
|
<div class="empty-state-grid">
|
|
41
|
-
<button type="button" class="empty-state-action" onClick={newNote}>
|
|
42
|
-
<IconNodeMarkdown size={16} class="tone-accent" />
|
|
43
|
-
<span class="empty-state-action-label">New markdown note</span>
|
|
44
|
-
<kbd>M</kbd>
|
|
45
|
-
</button>
|
|
46
39
|
<button type="button" class="empty-state-action" onClick={() => fileInput.current?.click()}>
|
|
47
40
|
<svg
|
|
48
41
|
width="16"
|
|
@@ -58,19 +51,41 @@ export function EmptyState({ onOpenPalette }: { onOpenPalette: () => void }) {
|
|
|
58
51
|
<path d="M8 10 V2 M8 2 L5 5 M8 2 L11 5" />
|
|
59
52
|
<path d="M2 10 V13 H14 V10" />
|
|
60
53
|
</svg>
|
|
61
|
-
<span class="empty-state-action-label">
|
|
54
|
+
<span class="empty-state-action-label">
|
|
55
|
+
Drop files here
|
|
56
|
+
<span class="empty-state-action-description">Bring in the material you want to work with.</span>
|
|
57
|
+
</span>
|
|
62
58
|
<span class="empty-state-action-hint">or pick</span>
|
|
63
59
|
</button>
|
|
64
60
|
<button type="button" class="empty-state-action" onClick={() => promptedCreate('webpage')}>
|
|
65
61
|
<IconNodeWebpage size={16} class="tone-ok" />
|
|
66
|
-
<span class="empty-state-action-label">
|
|
62
|
+
<span class="empty-state-action-label">
|
|
63
|
+
Paste a link
|
|
64
|
+
<span class="empty-state-action-description">Add a source, reference, or inspiration.</span>
|
|
65
|
+
</span>
|
|
67
66
|
<kbd>{modChord('V')}</kbd>
|
|
68
67
|
</button>
|
|
69
|
-
<button type="button" class="empty-state-action
|
|
70
|
-
<
|
|
71
|
-
<span class="empty-state-action-label">
|
|
68
|
+
<button type="button" class="empty-state-action" onClick={newNote}>
|
|
69
|
+
<IconNodeMarkdown size={16} class="tone-accent" />
|
|
70
|
+
<span class="empty-state-action-label">
|
|
71
|
+
Start a note
|
|
72
|
+
<span class="empty-state-action-description">
|
|
73
|
+
Write down an idea, then ask your agent to help you develop it.
|
|
74
|
+
</span>
|
|
75
|
+
</span>
|
|
76
|
+
<kbd>M</kbd>
|
|
72
77
|
</button>
|
|
73
78
|
</div>
|
|
79
|
+
<div class="empty-state-agent">
|
|
80
|
+
<div class="empty-state-agent-title">Or start with your agent</div>
|
|
81
|
+
<div class="empty-state-sub">Give your agent a task and ask it to work on this board.</div>
|
|
82
|
+
{agentSessionsEnabled.value && (
|
|
83
|
+
<button type="button" class="empty-state-action is-agent" onClick={() => void startSession()}>
|
|
84
|
+
<IconSteer size={16} class="tone-purple" />
|
|
85
|
+
<span class="empty-state-action-label">Start agent session</span>
|
|
86
|
+
</button>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
74
89
|
<button type="button" class="empty-state-hint" onClick={onOpenPalette}>
|
|
75
90
|
? shortcuts · {modChord('K')} palette
|
|
76
91
|
</button>
|