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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ComponentChildren } from 'preact';
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
/** Shared docking only. Map and Graph own their selection and actions. */
|
|
5
|
+
export function MemoryInspector({
|
|
6
|
+
children,
|
|
7
|
+
initiallyCollapsed = false,
|
|
8
|
+
reveal = 0,
|
|
9
|
+
}: {
|
|
10
|
+
children: ComponentChildren;
|
|
11
|
+
initiallyCollapsed?: boolean;
|
|
12
|
+
reveal?: number;
|
|
13
|
+
}) {
|
|
14
|
+
const root = useRef<HTMLDivElement>(null);
|
|
15
|
+
const [height, setHeight] = useState(280);
|
|
16
|
+
const [max, setMax] = useState(400);
|
|
17
|
+
const [collapsed, setCollapsed] = useState(initiallyCollapsed);
|
|
18
|
+
const drag = useRef<{ id: number; y: number; height: number } | null>(null);
|
|
19
|
+
useLayoutEffect(() => {
|
|
20
|
+
if (reveal) setCollapsed(false);
|
|
21
|
+
}, [reveal]);
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
const view = root.current!.closest<HTMLElement>('.home-memory')!;
|
|
24
|
+
const stage = view.querySelector<HTMLElement>('.home-memory-stage')!;
|
|
25
|
+
const region = view.closest<HTMLElement>('.canvas-region');
|
|
26
|
+
const toolbar = view.querySelector<HTMLElement>('.home-memory-toolbar')!;
|
|
27
|
+
const legend = view.querySelector<HTMLElement>('.home-memory-legend')!;
|
|
28
|
+
const measure = () => {
|
|
29
|
+
region?.style.setProperty(
|
|
30
|
+
'--home-memory-content-top',
|
|
31
|
+
`${stage.getBoundingClientRect().top - (region?.getBoundingClientRect().top ?? 0)}px`,
|
|
32
|
+
);
|
|
33
|
+
setMax(Math.max(120, view.clientHeight - toolbar.offsetHeight - legend.offsetHeight - 160));
|
|
34
|
+
};
|
|
35
|
+
const observer = new ResizeObserver(measure);
|
|
36
|
+
for (const element of [view, stage, toolbar, legend]) observer.observe(element);
|
|
37
|
+
measure();
|
|
38
|
+
return () => {
|
|
39
|
+
observer.disconnect();
|
|
40
|
+
region?.style.removeProperty('--home-memory-content-top');
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
ref={root}
|
|
46
|
+
class={`home-memory-inspector${collapsed ? ' is-collapsed' : ''}`}
|
|
47
|
+
style={collapsed ? undefined : { height: Math.min(height, max) }}
|
|
48
|
+
>
|
|
49
|
+
{!collapsed && (
|
|
50
|
+
<div
|
|
51
|
+
class="home-memory-inspector-resize"
|
|
52
|
+
role="separator"
|
|
53
|
+
tabIndex={0}
|
|
54
|
+
aria-label="Resize relation details"
|
|
55
|
+
aria-orientation="horizontal"
|
|
56
|
+
aria-controls="home-memory-details"
|
|
57
|
+
aria-valuemin={120}
|
|
58
|
+
aria-valuemax={max}
|
|
59
|
+
aria-valuenow={Math.min(height, max)}
|
|
60
|
+
onKeyDown={(event) => {
|
|
61
|
+
if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) return;
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
setHeight(
|
|
64
|
+
event.key === 'Home'
|
|
65
|
+
? 120
|
|
66
|
+
: event.key === 'End'
|
|
67
|
+
? max
|
|
68
|
+
: Math.max(120, Math.min(max, Math.min(height, max) + (event.key === 'ArrowUp' ? 32 : -32))),
|
|
69
|
+
);
|
|
70
|
+
}}
|
|
71
|
+
onPointerDown={(event) => {
|
|
72
|
+
if (event.button !== 0) return;
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
drag.current = { id: event.pointerId, y: event.clientY, height: Math.min(height, max) };
|
|
75
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
76
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
77
|
+
}}
|
|
78
|
+
onPointerMove={(event) => {
|
|
79
|
+
const start = drag.current;
|
|
80
|
+
if (start?.id === event.pointerId)
|
|
81
|
+
setHeight(Math.max(120, Math.min(max, start.height + start.y - event.clientY)));
|
|
82
|
+
}}
|
|
83
|
+
onPointerUp={() => {
|
|
84
|
+
drag.current = null;
|
|
85
|
+
}}
|
|
86
|
+
onPointerCancel={() => {
|
|
87
|
+
if (drag.current) setHeight(drag.current.height);
|
|
88
|
+
drag.current = null;
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
class="home-button home-memory-inspector-toggle"
|
|
95
|
+
aria-expanded={!collapsed}
|
|
96
|
+
aria-controls="home-memory-details"
|
|
97
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
98
|
+
>
|
|
99
|
+
{collapsed ? 'Expand details' : 'Collapse details'}
|
|
100
|
+
</button>
|
|
101
|
+
<div id="home-memory-details">{children}</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
agentPresences,
|
|
7
7
|
mutatingNodeIds,
|
|
8
8
|
presenceWorldPosition,
|
|
9
|
-
|
|
9
|
+
sessionChromeActive,
|
|
10
10
|
writerColor,
|
|
11
11
|
} from '../state/presence-store';
|
|
12
12
|
import { scopeFence } from '../state/session-store';
|
|
@@ -146,7 +146,7 @@ export function Minimap({ viewport, nodes, onNavigate, containerWidth, container
|
|
|
146
146
|
|
|
147
147
|
const selected = selectedNodeIds.value;
|
|
148
148
|
const pinned = contextPinnedNodeIds.value;
|
|
149
|
-
const agentAtWork =
|
|
149
|
+
const agentAtWork = sessionChromeActive.value ? mutatingNodeIds.value : new Set<string>();
|
|
150
150
|
const fence = scopeFence.value;
|
|
151
151
|
const fenceRect = fence
|
|
152
152
|
? fenceRectFromNodes(
|
|
@@ -175,7 +175,7 @@ export function Minimap({ viewport, nodes, onNavigate, containerWidth, container
|
|
|
175
175
|
ref={mapRef}
|
|
176
176
|
class="minimap"
|
|
177
177
|
data-testid="minimap"
|
|
178
|
-
|
|
178
|
+
aria-label="Minimap — click to jump, drag the frame to pan"
|
|
179
179
|
onPointerDown={handlePointerDown}
|
|
180
180
|
>
|
|
181
181
|
{Array.from(nodeMap.values()).map((node) => {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { gapDistance } from '../../server/spatial-analysis.js';
|
|
2
|
+
import { nodes, toggleContextPin, viewport } from '../state/canvas-store';
|
|
3
|
+
import {
|
|
4
|
+
closeNearCardSoon,
|
|
5
|
+
keepNearCard,
|
|
6
|
+
nearCard,
|
|
7
|
+
nearPins,
|
|
8
|
+
neighboursByPin,
|
|
9
|
+
shownPinsFor,
|
|
10
|
+
tetherFocus,
|
|
11
|
+
} from '../state/near-pin-store';
|
|
12
|
+
import type { CanvasNodeState } from '../types';
|
|
13
|
+
import { viewKey } from './NodeContextMark';
|
|
14
|
+
|
|
15
|
+
/** The brief's "near" edge gap (findNeighborhoods), in world px. */
|
|
16
|
+
const NEAR_RADIUS = 600;
|
|
17
|
+
|
|
18
|
+
interface Point {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Where the segment from `from` to the box's centre crosses the box's edge. */
|
|
24
|
+
function edgePoint(from: Point, box: { left: number; top: number; width: number; height: number }): Point {
|
|
25
|
+
const to = { x: box.left + box.width / 2, y: box.top + box.height / 2 };
|
|
26
|
+
const dx = from.x - to.x;
|
|
27
|
+
const dy = from.y - to.y;
|
|
28
|
+
const t = Math.min(
|
|
29
|
+
dx === 0 ? Number.POSITIVE_INFINITY : box.width / 2 / Math.abs(dx),
|
|
30
|
+
dy === 0 ? Number.POSITIVE_INFINITY : box.height / 2 / Math.abs(dy),
|
|
31
|
+
);
|
|
32
|
+
return t >= 1 ? to : { x: to.x + dx * t, y: to.y + dy * t };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Which pin? (docs/design/NearPin.dc.html §2): for a hovered, selected or
|
|
37
|
+
* dragged pin — or the pins a near chip asked to show — dotted tethers to its
|
|
38
|
+
* neighbours, numbered nearest first, and an unlabeled boundary only when
|
|
39
|
+
* another node is within range. Drawn in screen
|
|
40
|
+
* space (architecture rule 9) so they stay legible at fit zoom; while an
|
|
41
|
+
* unpinned node is dragged only the nearest pin's boundary shows (§3).
|
|
42
|
+
*/
|
|
43
|
+
export function NearPinLayer() {
|
|
44
|
+
const focus = tetherFocus.value;
|
|
45
|
+
if (focus.pinIds.length === 0) return null;
|
|
46
|
+
const v = viewport.value;
|
|
47
|
+
const all = nodes.value;
|
|
48
|
+
const byPin = neighboursByPin.value;
|
|
49
|
+
const toScreen = (x: number, y: number): Point => ({ x: x * v.scale + v.x, y: y * v.scale + v.y });
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<svg class="near-pin-layer" aria-hidden="true">
|
|
53
|
+
{focus.pinIds.map((pinId) => {
|
|
54
|
+
const pin = all.get(pinId);
|
|
55
|
+
if (!pin) return null;
|
|
56
|
+
if (![...all.values()].some((node) => node.id !== pinId && gapDistance(node, pin) <= NEAR_RADIUS)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const pinTopLeft = toScreen(pin.position.x, pin.position.y);
|
|
60
|
+
const pinBox = {
|
|
61
|
+
left: pinTopLeft.x,
|
|
62
|
+
top: pinTopLeft.y,
|
|
63
|
+
width: pin.size.width * v.scale,
|
|
64
|
+
height: pin.size.height * v.scale,
|
|
65
|
+
};
|
|
66
|
+
const c = { x: pinBox.left + pinBox.width / 2, y: pinBox.top + pinBox.height / 2 };
|
|
67
|
+
const r = NEAR_RADIUS * v.scale;
|
|
68
|
+
const neighbours = focus.ringOnly
|
|
69
|
+
? []
|
|
70
|
+
: (byPin.get(pinId) ?? [])
|
|
71
|
+
.map((id) => all.get(id))
|
|
72
|
+
.filter((node): node is CanvasNodeState => node !== undefined)
|
|
73
|
+
.sort((a, b) => gapDistance(a, pin) - gapDistance(b, pin));
|
|
74
|
+
return (
|
|
75
|
+
<g key={pinId} data-pin-id={pinId}>
|
|
76
|
+
<rect
|
|
77
|
+
class="near-pin-radius"
|
|
78
|
+
x={pinBox.left - r}
|
|
79
|
+
y={pinBox.top - r}
|
|
80
|
+
width={pinBox.width + 2 * r}
|
|
81
|
+
height={pinBox.height + 2 * r}
|
|
82
|
+
rx={r}
|
|
83
|
+
/>
|
|
84
|
+
{neighbours.map((node, index) => {
|
|
85
|
+
const topLeft = toScreen(node.position.x, node.position.y);
|
|
86
|
+
const box = {
|
|
87
|
+
left: topLeft.x,
|
|
88
|
+
top: topLeft.y,
|
|
89
|
+
width: node.size.width * v.scale,
|
|
90
|
+
height: node.size.height * v.scale,
|
|
91
|
+
};
|
|
92
|
+
// Edge to edge: the line never crosses either card's content.
|
|
93
|
+
const end = edgePoint(c, box);
|
|
94
|
+
const start = edgePoint({ x: box.left + box.width / 2, y: box.top + box.height / 2 }, pinBox);
|
|
95
|
+
return (
|
|
96
|
+
<g key={node.id} class="near-pin-tether" data-near-node={node.id}>
|
|
97
|
+
<path d={`M ${start.x} ${start.y} L ${end.x} ${end.y}`} />
|
|
98
|
+
<circle class="near-pin-number" cx={end.x} cy={end.y} r={10} />
|
|
99
|
+
<text class="near-pin-number-label" x={end.x} y={end.y}>
|
|
100
|
+
{index + 1}
|
|
101
|
+
</text>
|
|
102
|
+
</g>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</g>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</svg>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Hover a near chip (docs/design/NearPin.dc.html §2): which pins, how far, and
|
|
114
|
+
* the two ways on — pin it to put it first, or show the pins' tethers.
|
|
115
|
+
*/
|
|
116
|
+
export function NearPinCard() {
|
|
117
|
+
const card = nearCard.value;
|
|
118
|
+
if (!card || card.view !== viewKey()) return null;
|
|
119
|
+
const node = nodes.value.get(card.nodeId);
|
|
120
|
+
const near = nearPins.value.get(card.nodeId);
|
|
121
|
+
if (!node || !near) return null;
|
|
122
|
+
const showing = shownPinsFor.value === node.id;
|
|
123
|
+
const left = Math.max(8, Math.min(card.at.right - 240, window.innerWidth - 248));
|
|
124
|
+
// Opens below the chip, or above it when the chip sits in the lower half; scrolls if still too tall.
|
|
125
|
+
const below = card.at.bottom < window.innerHeight / 2;
|
|
126
|
+
const room = below ? window.innerHeight - card.at.bottom - 16 : card.at.top - 16;
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
class="near-hint"
|
|
130
|
+
role="dialog"
|
|
131
|
+
aria-label={near.length > 1 ? `Near ${near.length} pins` : 'Near a pin'}
|
|
132
|
+
style={{
|
|
133
|
+
left: `${left}px`,
|
|
134
|
+
maxHeight: `${Math.max(120, room)}px`,
|
|
135
|
+
...(below ? { top: `${card.at.bottom + 8}px` } : { bottom: `${window.innerHeight - card.at.top + 8}px` }),
|
|
136
|
+
}}
|
|
137
|
+
onPointerEnter={keepNearCard}
|
|
138
|
+
onPointerLeave={closeNearCardSoon}
|
|
139
|
+
onFocusIn={keepNearCard}
|
|
140
|
+
onFocusOut={closeNearCardSoon}
|
|
141
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
142
|
+
onKeyDown={(event) => {
|
|
143
|
+
if (event.key !== 'Escape') return;
|
|
144
|
+
event.stopPropagation();
|
|
145
|
+
nearCard.value = null;
|
|
146
|
+
document.querySelector<HTMLElement>(`[data-node-id="${CSS.escape(node.id)}"] .node-near-mark`)?.focus();
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
<span class="near-hint-label">{near.length > 1 ? `Near ${near.length} pins` : 'Near a pin'}</span>
|
|
150
|
+
{near.map((pin) => {
|
|
151
|
+
const pinNode = nodes.value.get(pin.pinNodeId);
|
|
152
|
+
return (
|
|
153
|
+
<span key={pin.pinNodeId} class="near-hint-row">
|
|
154
|
+
<span class="near-hint-title">{pin.pinTitle}</span>
|
|
155
|
+
{pinNode && <span class="near-hint-distance">{Math.round(gapDistance(node, pinNode))} px</span>}
|
|
156
|
+
</span>
|
|
157
|
+
);
|
|
158
|
+
})}
|
|
159
|
+
<span class="near-hint-body">
|
|
160
|
+
Distance is measured between card edges. The agent gets its title and a short summary because a pin is nearby.
|
|
161
|
+
Pin it to put it first.
|
|
162
|
+
</span>
|
|
163
|
+
<span class="near-hint-actions">
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
class="near-hint-pin"
|
|
167
|
+
onClick={() => {
|
|
168
|
+
nearCard.value = null;
|
|
169
|
+
toggleContextPin(node.id);
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
Pin to put it first
|
|
173
|
+
</button>
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
class="near-hint-show"
|
|
177
|
+
aria-pressed={showing}
|
|
178
|
+
onClick={() => {
|
|
179
|
+
shownPinsFor.value = showing ? null : node.id;
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{showing ? 'Hide pins' : 'Show pins'}
|
|
183
|
+
</button>
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { contextPinMeta, pinnedReadState, writerName } from '../state/context-status-store';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
1
|
+
import type { ComponentChildren } from 'preact';
|
|
2
|
+
import { contextPinMeta, linksChanged, pinnedReadState, writerName } from '../state/context-status-store';
|
|
3
|
+
import { viewport } from '../state/canvas-store';
|
|
4
|
+
import { grabbingNodeId } from '../state/human-store';
|
|
5
|
+
import {
|
|
6
|
+
clearNearChange,
|
|
7
|
+
closeNearCardSoon,
|
|
8
|
+
nearCard,
|
|
9
|
+
nearChange,
|
|
10
|
+
nearPins,
|
|
11
|
+
openNearCard,
|
|
12
|
+
} from '../state/near-pin-store';
|
|
13
|
+
import { TYPE_LABELS, type CanvasNodeState } from '../types';
|
|
7
14
|
import { BarHint } from './BarHint';
|
|
8
15
|
|
|
9
16
|
// Glyphs from docs/design/AgentContext.dc.html (24-unit paths).
|
|
@@ -30,75 +37,123 @@ function clock(iso: string): string {
|
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
function markFor(node: CanvasNodeState, pinned: boolean): Mark | null {
|
|
33
|
-
if (pinned)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
tone: 'muted',
|
|
43
|
-
glyph: 'eyeOff',
|
|
44
|
-
word: 'not read',
|
|
45
|
-
label: 'Not read yet',
|
|
46
|
-
body: `In context, but no agent has loaded it since it was pinned.${pinnedBy}`,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
if (state.kind === 'changed') {
|
|
50
|
-
return {
|
|
51
|
-
tone: 'warn',
|
|
52
|
-
glyph: 'warn',
|
|
53
|
-
word: 'changed since read',
|
|
54
|
-
label: "The agent's copy is out of date",
|
|
55
|
-
body: `Changed after ${state.by} read it at ${clock(state.at)}.${pinnedBy}`,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
tone: 'agent',
|
|
60
|
-
glyph: 'eye',
|
|
61
|
-
word: 'read',
|
|
62
|
-
label: 'Read by the agent',
|
|
63
|
-
body: `${state.count > 1 ? `Read ${state.count} times · last` : 'Read'} ${clock(state.at)} by ${state.by}.${pinnedBy}`,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
// A person's edit ends the agent's byline (decided 2026-10-05).
|
|
67
|
-
if (node.lastEditedBy?.actor === 'human') return null;
|
|
68
|
-
if (node.createdBy?.actor === 'agent') {
|
|
69
|
-
const name = writerName(node.createdBy);
|
|
40
|
+
if (!pinned) return null;
|
|
41
|
+
const pin = contextPinMeta.value[node.id];
|
|
42
|
+
const pinnedBy =
|
|
43
|
+
pin?.pinnedBy.actor === 'agent'
|
|
44
|
+
? ` Pinned by ${writerName(pin.pinnedBy)}${pin.reason ? `: ${pin.reason}` : ''}.`
|
|
45
|
+
: '';
|
|
46
|
+
const state = pinnedReadState(node.id, node.contentRevision ?? 0);
|
|
47
|
+
if (state.kind === 'not-read') {
|
|
70
48
|
return {
|
|
71
|
-
tone: '
|
|
72
|
-
glyph: '
|
|
73
|
-
word:
|
|
74
|
-
label:
|
|
75
|
-
body:
|
|
49
|
+
tone: 'muted',
|
|
50
|
+
glyph: 'eyeOff',
|
|
51
|
+
word: 'not read',
|
|
52
|
+
label: 'Not read yet',
|
|
53
|
+
body: `In context, but no agent has loaded it since it was pinned.${pinnedBy}`,
|
|
76
54
|
};
|
|
77
55
|
}
|
|
78
|
-
if (
|
|
79
|
-
const name = writerName(node.lastEditedBy);
|
|
56
|
+
if (state.kind === 'changed') {
|
|
80
57
|
return {
|
|
81
|
-
tone: '
|
|
82
|
-
glyph: '
|
|
83
|
-
word: '
|
|
84
|
-
label:
|
|
85
|
-
body:
|
|
58
|
+
tone: 'warn',
|
|
59
|
+
glyph: 'warn',
|
|
60
|
+
word: 'changed',
|
|
61
|
+
label: "The agent's copy is out of date",
|
|
62
|
+
body: `Text changed after ${state.by} read it at ${clock(state.at)}${
|
|
63
|
+
linksChanged(node.id, node.linksRevision ?? 0) ? ' · links changed too' : ''
|
|
64
|
+
}.${pinnedBy}`,
|
|
86
65
|
};
|
|
87
66
|
}
|
|
67
|
+
return {
|
|
68
|
+
tone: 'agent',
|
|
69
|
+
glyph: 'eye',
|
|
70
|
+
word: 'read',
|
|
71
|
+
label: 'Read by the agent',
|
|
72
|
+
body: `${state.count > 1 ? `Read ${state.count} times · last` : 'Read'} ${clock(state.at)} by ${state.by}.${pinnedBy}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Authorship lives only in the type icon's hint, not a star or state chip.
|
|
78
|
+
* A person's edit ends the agent byline.
|
|
79
|
+
*/
|
|
80
|
+
export function bylineFor(node: CanvasNodeState): string | null {
|
|
81
|
+
if (node.lastEditedBy?.actor === 'human') return null;
|
|
82
|
+
if (node.createdBy?.actor === 'agent') return `Written by ${writerName(node.createdBy)}`;
|
|
83
|
+
if (node.lastEditedBy?.actor === 'agent') return `Edited by ${writerName(node.lastEditedBy)}`;
|
|
88
84
|
return null;
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
/**
|
|
92
|
-
export
|
|
87
|
+
/** How a header chip shows: its word, only its glyph, or folded into the type icon's hint. */
|
|
88
|
+
export type ChipFold = 'word' | 'glyph' | 'hidden';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The one state chip a header carries — near a pin (unpinned) or the read
|
|
92
|
+
* state (pinned) — with what it says when folded into the icon's hint. Never
|
|
93
|
+
* other cards' titles: at rest a node must not contain them.
|
|
94
|
+
*/
|
|
95
|
+
export function headerChip(node: CanvasNodeState, pinned: boolean): { amber: boolean; folded: string } | null {
|
|
93
96
|
const mark = markFor(node, pinned);
|
|
94
|
-
if (
|
|
97
|
+
if (mark) return { amber: mark.tone === 'warn', folded: mark.label };
|
|
98
|
+
const near = pinned ? undefined : nearPins.value.get(node.id);
|
|
99
|
+
if (near && near.length > 0)
|
|
100
|
+
return { amber: false, folded: near.length > 1 ? `near ${near.length} pins` : 'near a pin' };
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A pinned node's read-state chip: glyph + word, or only the glyph when the title needs the room. */
|
|
105
|
+
export function NodeContextMark({
|
|
106
|
+
node,
|
|
107
|
+
pinned,
|
|
108
|
+
fold = 'word',
|
|
109
|
+
}: {
|
|
110
|
+
node: CanvasNodeState;
|
|
111
|
+
pinned: boolean;
|
|
112
|
+
fold?: ChipFold;
|
|
113
|
+
}) {
|
|
114
|
+
const mark = markFor(node, pinned);
|
|
115
|
+
// Amber always carries its word (HeaderMarks.dc.html).
|
|
116
|
+
const shown = mark?.tone === 'warn' ? 'word' : fold;
|
|
117
|
+
if (!mark || shown === 'hidden') return null;
|
|
95
118
|
return (
|
|
96
|
-
<BarHint label={mark.label} body={mark.body} align="end" tapToOpen>
|
|
97
|
-
<span
|
|
119
|
+
<BarHint label={mark.label} body={mark.body} align="end" fitWithin=".node-content" tapToOpen>
|
|
120
|
+
<span
|
|
121
|
+
class={`node-context-mark is-${mark.tone}${shown === 'glyph' ? ' is-glyph' : ''}`}
|
|
122
|
+
data-mark={mark.word}
|
|
123
|
+
aria-label={shown === 'glyph' ? mark.word : undefined}
|
|
124
|
+
>
|
|
98
125
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
99
126
|
<path d={GLYPHS[mark.glyph]} />
|
|
100
127
|
</svg>
|
|
101
|
-
{mark.word}
|
|
128
|
+
{shown === 'word' && mark.word}
|
|
129
|
+
</span>
|
|
130
|
+
</BarHint>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The type icon's hint holds the full title, the byline and any folded chip.
|
|
136
|
+
*/
|
|
137
|
+
export function NodeTypeIcon({
|
|
138
|
+
node,
|
|
139
|
+
pinned,
|
|
140
|
+
title,
|
|
141
|
+
fold,
|
|
142
|
+
children,
|
|
143
|
+
}: {
|
|
144
|
+
node: CanvasNodeState;
|
|
145
|
+
pinned: boolean;
|
|
146
|
+
title: string;
|
|
147
|
+
fold: ChipFold;
|
|
148
|
+
children: ComponentChildren;
|
|
149
|
+
}) {
|
|
150
|
+
const byline = bylineFor(node);
|
|
151
|
+
const chip = fold === 'hidden' ? headerChip(node, pinned) : null;
|
|
152
|
+
const body = [byline, chip && !chip.amber ? chip.folded : '', TYPE_LABELS[node.type]].filter(Boolean).join(' · ');
|
|
153
|
+
return (
|
|
154
|
+
<BarHint label={title} body={body} align="start" fitWithin=".node-content" tapToOpen>
|
|
155
|
+
<span class="node-type-icon" data-byline={byline ?? undefined}>
|
|
156
|
+
{children}
|
|
102
157
|
</span>
|
|
103
158
|
</BarHint>
|
|
104
159
|
);
|
|
@@ -112,45 +167,68 @@ export function isAgentPin(nodeId: string): boolean {
|
|
|
112
167
|
/**
|
|
113
168
|
* Near a pin (docs/design/NearPin.dc.html): a dotted pin-blue chip on an
|
|
114
169
|
* unpinned node the brief carries as title + short summary. Weaker than "in
|
|
115
|
-
* context" on purpose: the agent
|
|
170
|
+
* context" on purpose: the agent sees it only because a pin is nearby. Hover,
|
|
171
|
+
* focus or tap opens its card (`NearPinCard`, §2); it previews while dragged,
|
|
172
|
+
* pulses once on a drop inside and fades on a drop outside (§3).
|
|
116
173
|
*/
|
|
117
|
-
export function NearPinMark({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
174
|
+
export function NearPinMark({
|
|
175
|
+
node,
|
|
176
|
+
pinned,
|
|
177
|
+
fold = 'word',
|
|
178
|
+
}: {
|
|
179
|
+
node: CanvasNodeState;
|
|
180
|
+
pinned: boolean;
|
|
181
|
+
fold?: ChipFold;
|
|
182
|
+
}) {
|
|
183
|
+
const live = pinned ? undefined : nearPins.value.get(node.id);
|
|
184
|
+
const change = nearChange.value?.nodeId === node.id ? nearChange.value : null;
|
|
185
|
+
const fading = !live && !pinned && change?.kind === 'out';
|
|
186
|
+
const near = live ?? (fading ? change.pins : undefined);
|
|
187
|
+
if (!near || near.length === 0 || fold === 'hidden') return null;
|
|
123
188
|
const word = near.length > 1 ? `near ${near.length}` : 'near';
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
189
|
+
const moment = fading
|
|
190
|
+
? ' is-fading'
|
|
191
|
+
: change?.kind === 'in'
|
|
192
|
+
? ' is-pulse'
|
|
193
|
+
: grabbingNodeId.value === node.id
|
|
194
|
+
? ' is-preview'
|
|
195
|
+
: '';
|
|
196
|
+
const open = (event: Event) => {
|
|
197
|
+
if (!fading) openNearCard(node.id, event.currentTarget as Element, viewKey());
|
|
198
|
+
};
|
|
133
199
|
return (
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
class={`node-near-mark${fold === 'glyph' ? ' is-glyph' : ''}${moment}`}
|
|
203
|
+
aria-label={fold === 'glyph' ? word : undefined}
|
|
204
|
+
aria-expanded={nearCard.value?.nodeId === node.id}
|
|
205
|
+
data-near={near.map((pin) => pin.pinNodeId).join(' ')}
|
|
206
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
207
|
+
onPointerEnter={open}
|
|
208
|
+
onPointerLeave={closeNearCardSoon}
|
|
209
|
+
onFocus={open}
|
|
210
|
+
onBlur={closeNearCardSoon}
|
|
211
|
+
onClick={open}
|
|
212
|
+
onKeyDown={(event) => {
|
|
213
|
+
// The card draws over the canvas, outside the tab order: Enter or ↓ moves focus into it.
|
|
214
|
+
if (event.key !== 'Enter' && event.key !== 'ArrowDown') return;
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
open(event);
|
|
217
|
+
requestAnimationFrame(() => document.querySelector<HTMLElement>('.near-hint button')?.focus());
|
|
218
|
+
}}
|
|
219
|
+
onAnimationEnd={() => clearNearChange(node.id)}
|
|
139
220
|
>
|
|
140
|
-
<
|
|
141
|
-
class="node-near-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
onFocusOut={() => setOpen(false)}
|
|
147
|
-
>
|
|
148
|
-
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
149
|
-
<circle cx="12" cy="12" r="8" class="node-near-ring" />
|
|
150
|
-
<circle cx="12" cy="12" r="2.2" class="node-near-dot" />
|
|
151
|
-
</svg>
|
|
152
|
-
{word}
|
|
153
|
-
</span>
|
|
154
|
-
</BarHint>
|
|
221
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
222
|
+
<circle cx="12" cy="12" r="8" class="node-near-ring" />
|
|
223
|
+
<circle cx="12" cy="12" r="2.2" class="node-near-dot" />
|
|
224
|
+
</svg>
|
|
225
|
+
{fold === 'word' && word}
|
|
226
|
+
</button>
|
|
155
227
|
);
|
|
156
228
|
}
|
|
229
|
+
|
|
230
|
+
/** The viewport as a key: the card closes when it changes. */
|
|
231
|
+
export function viewKey(): string {
|
|
232
|
+
const v = viewport.value;
|
|
233
|
+
return `${v.x},${v.y},${v.scale}`;
|
|
234
|
+
}
|