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.
Files changed (335) hide show
  1. package/.github/extensions/pmx-canvas/runtime.json +1 -1
  2. package/.github/extensions/pmx-canvas/tools.json +601 -22
  3. package/CHANGELOG.md +49 -0
  4. package/Readme.md +131 -532
  5. package/dist/canvas/global.css +2724 -476
  6. package/dist/canvas/index.js +168 -107
  7. package/dist/canvas/mcp-app.html +2899 -596
  8. package/dist/json-render/index.js +20 -20
  9. package/dist/types/client/canvas/BarHint.d.ts +12 -2
  10. package/dist/types/client/canvas/BoardLinkDialog.d.ts +3 -1
  11. package/dist/types/client/canvas/BoardPinButton.d.ts +10 -0
  12. package/dist/types/client/canvas/BoardSwitcher.d.ts +1 -1
  13. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  14. package/dist/types/client/canvas/CommandBar.d.ts +3 -4
  15. package/dist/types/client/canvas/ConnectionBanner.d.ts +2 -1
  16. package/dist/types/client/canvas/ContextChip.d.ts +3 -0
  17. package/dist/types/client/canvas/CrossBoardReferences.d.ts +5 -0
  18. package/dist/types/client/canvas/EdgeLayer.d.ts +14 -3
  19. package/dist/types/client/canvas/EmptyState.d.ts +4 -4
  20. package/dist/types/client/canvas/HomeGraphView.d.ts +2 -0
  21. package/dist/types/client/canvas/HomeMapView.d.ts +1 -0
  22. package/dist/types/client/canvas/HomeReviewView.d.ts +6 -0
  23. package/dist/types/client/canvas/HomeView.d.ts +9 -7
  24. package/dist/types/client/canvas/KnowledgeDialog.d.ts +4 -0
  25. package/dist/types/client/canvas/LifecycleChrome.d.ts +5 -0
  26. package/dist/types/client/canvas/LifecycleFields.d.ts +7 -0
  27. package/dist/types/client/canvas/LifecyclePanel.d.ts +1 -0
  28. package/dist/types/client/canvas/MemoryInspector.d.ts +7 -0
  29. package/dist/types/client/canvas/NearPinLayer.d.ts +14 -0
  30. package/dist/types/client/canvas/NodeContextMark.d.ts +40 -7
  31. package/dist/types/client/canvas/RelationPresetsDialog.d.ts +4 -0
  32. package/dist/types/client/canvas/RelationsPanel.d.ts +4 -0
  33. package/dist/types/client/canvas/SessionPanel.d.ts +2 -0
  34. package/dist/types/client/canvas/canvas-area.d.ts +1 -1
  35. package/dist/types/client/canvas/graph-geometry.d.ts +27 -0
  36. package/dist/types/client/canvas/graph-model.d.ts +37 -0
  37. package/dist/types/client/canvas/memory-layout.d.ts +23 -0
  38. package/dist/types/client/canvas/use-chrome-height.d.ts +2 -0
  39. package/dist/types/client/canvas/use-focus-trap.d.ts +1 -0
  40. package/dist/types/client/canvas/use-memory-graph.d.ts +8 -0
  41. package/dist/types/client/canvas/use-pan-zoom.d.ts +4 -1
  42. package/dist/types/client/icons.d.ts +0 -10
  43. package/dist/types/client/nodes/AxStepControls.d.ts +1 -1
  44. package/dist/types/client/nodes/InlineMarkdownEditor.d.ts +4 -4
  45. package/dist/types/client/nodes/MarkdownNode.d.ts +5 -1
  46. package/dist/types/client/nodes/McpAppNode.d.ts +4 -4
  47. package/dist/types/client/nodes/viewer-runtime-state.d.ts +1 -1
  48. package/dist/types/client/state/boards-store.d.ts +81 -9
  49. package/dist/types/client/state/canvas-store.d.ts +14 -1
  50. package/dist/types/client/state/context-chip-store.d.ts +22 -0
  51. package/dist/types/client/state/context-status-store.d.ts +35 -0
  52. package/dist/types/client/state/intent-bridge.d.ts +11 -8
  53. package/dist/types/client/state/knowledge-store.d.ts +22 -0
  54. package/dist/types/client/state/near-pin-store.d.ts +44 -1
  55. package/dist/types/client/state/presence-store.d.ts +4 -4
  56. package/dist/types/client/state/presentation.d.ts +28 -0
  57. package/dist/types/client/state/relation-store.d.ts +10 -0
  58. package/dist/types/client/state/session-store.d.ts +41 -2
  59. package/dist/types/client/state/workspace-settings.d.ts +7 -0
  60. package/dist/types/client/types.d.ts +13 -1
  61. package/dist/types/client/utils/keyboard.d.ts +2 -0
  62. package/dist/types/client/utils/time-ago.d.ts +4 -0
  63. package/dist/types/mcp/context-uri-template.d.ts +3 -1
  64. package/dist/types/server/agent-presence.d.ts +10 -3
  65. package/dist/types/server/ax-context.d.ts +1 -1
  66. package/dist/types/server/canvas-db.d.ts +30 -7
  67. package/dist/types/server/canvas-operations.d.ts +24 -2
  68. package/dist/types/server/canvas-provenance.d.ts +22 -2
  69. package/dist/types/server/canvas-serialization.d.ts +2 -0
  70. package/dist/types/server/canvas-state.d.ts +93 -15
  71. package/dist/types/server/canvas-validation.d.ts +1 -1
  72. package/dist/types/server/context-brief.d.ts +94 -6
  73. package/dist/types/server/context-reads.d.ts +33 -7
  74. package/dist/types/server/edit-summary.d.ts +20 -0
  75. package/dist/types/server/index.d.ts +71 -10
  76. package/dist/types/server/knowledge-lifecycle.d.ts +221 -0
  77. package/dist/types/server/lifecycle-status.d.ts +10 -0
  78. package/dist/types/server/memory-graph.d.ts +9 -0
  79. package/dist/types/server/mutation-history.d.ts +2 -0
  80. package/dist/types/server/operations/ops/ax-shared.d.ts +1 -1
  81. package/dist/types/server/operations/ops/boards.d.ts +2 -1
  82. package/dist/types/server/operations/ops/lifecycle.d.ts +2 -0
  83. package/dist/types/server/operations/ops/memory-graph.d.ts +2 -0
  84. package/dist/types/server/operations/ops/nodes.d.ts +5 -0
  85. package/dist/types/server/operations/ops/query.d.ts +23 -0
  86. package/dist/types/server/operations/registry.d.ts +1 -0
  87. package/dist/types/server/placement.d.ts +7 -0
  88. package/dist/types/server/scope-fence.d.ts +2 -1
  89. package/dist/types/server/spatial-analysis.d.ts +6 -3
  90. package/dist/types/server/tour-control.d.ts +32 -0
  91. package/dist/types/server/wiki-links.d.ts +14 -0
  92. package/dist/types/server/workbench-auth.d.ts +2 -0
  93. package/dist/types/shared/agent-context.d.ts +2 -0
  94. package/dist/types/shared/agent-presence.d.ts +15 -12
  95. package/dist/types/shared/auto-arrange.d.ts +1 -0
  96. package/dist/types/shared/boards.d.ts +4 -4
  97. package/dist/types/shared/knowledge-lifecycle.d.ts +41 -0
  98. package/dist/types/shared/knowledge-presets.d.ts +6 -0
  99. package/dist/types/shared/lifecycle-status.d.ts +24 -0
  100. package/dist/types/shared/memory-graph.d.ts +53 -0
  101. package/dist/types/shared/relations.d.ts +68 -0
  102. package/dist/types/shared/semantic-attention.d.ts +2 -0
  103. package/dist/types/shared/tour.d.ts +38 -0
  104. package/dist/types/shared/workspace-settings.d.ts +11 -0
  105. package/docs/RELEASE.md +21 -3
  106. package/docs/adr-001-bun-only-runtime.md +7 -1
  107. package/docs/adr-002-shared-product-separate-runtimes.md +93 -0
  108. package/docs/api-reference.md +109 -0
  109. package/docs/ax-host-adapter-contract.md +1 -1
  110. package/docs/ax-state-contract.md +5 -4
  111. package/docs/cli.md +153 -15
  112. package/docs/demos/pmx-canvas-demo.mp4 +0 -0
  113. package/docs/design/AgentContext.dc.html +3 -3
  114. package/docs/design/BoardPins.dc.html +37 -22
  115. package/docs/design/Categories.dc.html +208 -0
  116. package/docs/design/CategoryEditor.dc.html +33 -0
  117. package/docs/design/Chrome.dc.html +52 -9
  118. package/docs/design/ContextChip.dc.html +224 -0
  119. package/docs/design/ContextChip600.dc.html +136 -0
  120. package/docs/design/EmptyState.dc.html +45 -0
  121. package/docs/design/Flows.dc.html +23 -1
  122. package/docs/design/HeaderMarks.dc.html +183 -0
  123. package/docs/design/Home.dc.html +19 -1
  124. package/docs/design/HomeGraph.dc.html +86 -202
  125. package/docs/design/HomeMap.dc.html +23 -1
  126. package/docs/design/HomeTeam.dc.html +32 -14
  127. package/docs/design/Lifecycle.dc.html +22 -0
  128. package/docs/design/Lifecycle600.dc.html +18 -0
  129. package/docs/design/LifecycleVariants.dc.html +157 -0
  130. package/docs/design/LinkAuthorship.dc.html +139 -0
  131. package/docs/design/LinksChanged.dc.html +203 -0
  132. package/docs/design/LinksOptions.dc.html +137 -0
  133. package/docs/design/LinksOptions600.dc.html +134 -0
  134. package/docs/design/NearPin.dc.html +9 -10
  135. package/docs/design/Pane600.dc.html +4 -3
  136. package/docs/design/Relations.dc.html +3 -2
  137. package/docs/design/Restore.dc.html +152 -0
  138. package/docs/design/SeeChange.dc.html +267 -0
  139. package/docs/design/Uses.dc.html +12 -6
  140. package/docs/design/canvas.json +146 -19
  141. package/docs/design.md +449 -16
  142. package/docs/environment.md +0 -1
  143. package/docs/evals/curation-effect.md +128 -0
  144. package/docs/http-api.md +459 -37
  145. package/docs/mcp.md +146 -18
  146. package/docs/node-types.md +58 -3
  147. package/docs/plans/design-gaps-2026-10-08.md +53 -0
  148. package/docs/plans/plan-009-tech-debt-backlog.md +379 -0
  149. package/docs/plans/plan-016-context-made-visible-wave-1.md +150 -5
  150. package/docs/plans/plan-017-knowledge-categories-and-lifecycle.md +175 -0
  151. package/docs/plans/plan-018-connected-memory-wave-2.md +594 -0
  152. package/docs/plans/plan-019-post-0.9-roadmap.md +198 -0
  153. package/docs/product-context-vision-2026-09.md +17 -1
  154. package/docs/product-vision-2026-09.md +1082 -43
  155. package/docs/product-vision-review-2026-09.md +13 -1
  156. package/docs/screenshots/workbench-live-dark.png +0 -0
  157. package/docs/screenshots/workbench-live-light.png +0 -0
  158. package/docs/sdk.md +62 -2
  159. package/package.json +1 -1
  160. package/plugins/pmx-canvas/com.github.copilot/extensions/pmx-canvas/runtime.json +1 -1
  161. package/plugins/pmx-canvas/plugin.json +1 -1
  162. package/plugins/pmx-canvas/runtime.json +1 -1
  163. package/plugins/pmx-canvas/tools.json +601 -22
  164. package/skills/pmx-canvas/SKILL.md +216 -42
  165. package/skills/pmx-canvas/references/full-reference.md +120 -21
  166. package/skills/pmx-canvas/references/github-copilot-app-adapter.md +2 -2
  167. package/skills/pmx-canvas/references/importing-context.md +132 -0
  168. package/skills/pmx-canvas/references/installing-pmx-canvas.md +6 -6
  169. package/skills/pmx-canvas/references/tours-and-recording.md +16 -1
  170. package/skills/pmx-canvas-testing/SKILL.md +20 -0
  171. package/src/cli/agent.ts +2 -0
  172. package/src/cli/commands/ax.ts +12 -0
  173. package/src/cli/commands/boards.ts +87 -11
  174. package/src/cli/commands/edges.ts +28 -0
  175. package/src/cli/commands/groups.ts +7 -0
  176. package/src/cli/commands/nodes.ts +1 -0
  177. package/src/cli/commands/query.ts +34 -2
  178. package/src/cli/commands/record.ts +199 -48
  179. package/src/cli/index.ts +6 -1
  180. package/src/client/App.tsx +177 -70
  181. package/src/client/canvas/AttentionHistory.tsx +0 -2
  182. package/src/client/canvas/BarHint.tsx +40 -2
  183. package/src/client/canvas/BoardCopyDialog.tsx +1 -1
  184. package/src/client/canvas/BoardFolderDialog.tsx +7 -7
  185. package/src/client/canvas/BoardLinkDialog.tsx +138 -33
  186. package/src/client/canvas/BoardPinButton.tsx +33 -0
  187. package/src/client/canvas/BoardSwitcher.tsx +26 -40
  188. package/src/client/canvas/CanvasNode.tsx +360 -237
  189. package/src/client/canvas/CanvasViewport.tsx +45 -7
  190. package/src/client/canvas/CommandBar.tsx +29 -23
  191. package/src/client/canvas/CommandPalette.tsx +5 -12
  192. package/src/client/canvas/ConnectionBanner.tsx +3 -3
  193. package/src/client/canvas/ContextChip.tsx +296 -0
  194. package/src/client/canvas/ContextMenu.tsx +63 -5
  195. package/src/client/canvas/CrossBoardReferences.tsx +60 -0
  196. package/src/client/canvas/EdgeLayer.tsx +339 -76
  197. package/src/client/canvas/EmptyState.tsx +33 -18
  198. package/src/client/canvas/ExpandedNodeOverlay.tsx +72 -40
  199. package/src/client/canvas/ExternalWriters.tsx +57 -42
  200. package/src/client/canvas/HomeGraphView.tsx +942 -0
  201. package/src/client/canvas/HomeMapView.tsx +641 -0
  202. package/src/client/canvas/HomeReviewView.tsx +167 -0
  203. package/src/client/canvas/HomeView.tsx +687 -160
  204. package/src/client/canvas/IntentLayer.tsx +19 -12
  205. package/src/client/canvas/KnowledgeDialog.tsx +488 -0
  206. package/src/client/canvas/LifecycleChrome.tsx +133 -0
  207. package/src/client/canvas/LifecycleFields.tsx +91 -0
  208. package/src/client/canvas/LifecyclePanel.tsx +533 -0
  209. package/src/client/canvas/MemoryInspector.tsx +104 -0
  210. package/src/client/canvas/Minimap.tsx +3 -3
  211. package/src/client/canvas/NearPinLayer.tsx +187 -0
  212. package/src/client/canvas/NodeContextMark.tsx +175 -97
  213. package/src/client/canvas/Presentation.tsx +23 -57
  214. package/src/client/canvas/RelationPresetsDialog.tsx +413 -0
  215. package/src/client/canvas/RelationsPanel.tsx +82 -0
  216. package/src/client/canvas/SelectionBar.tsx +3 -6
  217. package/src/client/canvas/SessionPanel.tsx +90 -50
  218. package/src/client/canvas/SessionReceipt.tsx +311 -39
  219. package/src/client/canvas/ShortcutOverlay.tsx +1 -1
  220. package/src/client/canvas/SnapshotPanel.tsx +27 -36
  221. package/src/client/canvas/ToolRail.tsx +367 -192
  222. package/src/client/canvas/TopBar.tsx +354 -124
  223. package/src/client/canvas/canvas-area.ts +10 -3
  224. package/src/client/canvas/graph-geometry.ts +195 -0
  225. package/src/client/canvas/graph-model.ts +254 -0
  226. package/src/client/canvas/memory-layout.ts +106 -0
  227. package/src/client/canvas/use-chrome-height.ts +20 -0
  228. package/src/client/canvas/use-focus-trap.ts +31 -5
  229. package/src/client/canvas/use-memory-graph.ts +58 -0
  230. package/src/client/canvas/use-node-drag.ts +2 -1
  231. package/src/client/canvas/use-pan-zoom.ts +64 -7
  232. package/src/client/icons.tsx +0 -63
  233. package/src/client/mcp-app/style.css +0 -6
  234. package/src/client/nodes/AxStepControls.tsx +74 -50
  235. package/src/client/nodes/BoardNode.tsx +5 -1
  236. package/src/client/nodes/ContextNode.tsx +18 -11
  237. package/src/client/nodes/ExtAppFrame.tsx +0 -1
  238. package/src/client/nodes/FileNode.tsx +46 -42
  239. package/src/client/nodes/HtmlNode.tsx +32 -0
  240. package/src/client/nodes/ImageNode.tsx +1 -1
  241. package/src/client/nodes/InlineFormatBar.tsx +6 -9
  242. package/src/client/nodes/InlineMarkdownEditor.tsx +56 -17
  243. package/src/client/nodes/MarkdownNode.tsx +377 -109
  244. package/src/client/nodes/McpAppNode.tsx +50 -20
  245. package/src/client/nodes/MdFormatBar.tsx +21 -18
  246. package/src/client/nodes/PromptNode.tsx +1 -1
  247. package/src/client/nodes/ResponseNode.tsx +1 -1
  248. package/src/client/nodes/viewer-runtime-state.ts +54 -22
  249. package/src/client/state/attention-bridge.ts +64 -15
  250. package/src/client/state/boards-store.ts +193 -39
  251. package/src/client/state/canvas-store.ts +99 -12
  252. package/src/client/state/context-chip-store.ts +70 -0
  253. package/src/client/state/context-status-store.ts +53 -0
  254. package/src/client/state/intent-bridge.ts +51 -30
  255. package/src/client/state/knowledge-store.ts +43 -0
  256. package/src/client/state/near-pin-store.ts +115 -6
  257. package/src/client/state/presence-store.ts +7 -10
  258. package/src/client/state/presentation.ts +91 -0
  259. package/src/client/state/relation-store.ts +81 -0
  260. package/src/client/state/session-store.ts +144 -18
  261. package/src/client/state/sse-bridge.ts +206 -29
  262. package/src/client/state/workspace-settings.ts +11 -0
  263. package/src/client/theme/global.css +2724 -476
  264. package/src/client/types.ts +12 -0
  265. package/src/client/utils/keyboard.ts +10 -0
  266. package/src/client/utils/time-ago.ts +14 -0
  267. package/src/json-render/renderer/index.tsx +31 -0
  268. package/src/mcp/canvas-app.ts +12 -2
  269. package/src/mcp/context-uri-template.ts +7 -4
  270. package/src/mcp/server.ts +63 -41
  271. package/src/mcp/workbench-app.ts +39 -6
  272. package/src/server/agent-presence.ts +128 -28
  273. package/src/server/ax-context.ts +27 -13
  274. package/src/server/board-export.ts +12 -1
  275. package/src/server/canvas-db.ts +331 -42
  276. package/src/server/canvas-operations.ts +195 -83
  277. package/src/server/canvas-provenance.ts +53 -22
  278. package/src/server/canvas-serialization.ts +2 -0
  279. package/src/server/canvas-state.ts +950 -122
  280. package/src/server/canvas-validation.ts +9 -2
  281. package/src/server/context-brief.ts +345 -38
  282. package/src/server/context-reads.ts +178 -30
  283. package/src/server/demo-state.json +355 -24
  284. package/src/server/edit-summary.ts +74 -0
  285. package/src/server/html-surface.ts +8 -1
  286. package/src/server/index.ts +181 -11
  287. package/src/server/knowledge-lifecycle.ts +304 -0
  288. package/src/server/lifecycle-status.ts +232 -0
  289. package/src/server/memory-graph.ts +166 -0
  290. package/src/server/mutation-history.ts +18 -6
  291. package/src/server/operations/composites.ts +32 -11
  292. package/src/server/operations/http.ts +7 -2
  293. package/src/server/operations/index.ts +4 -0
  294. package/src/server/operations/mcp.ts +26 -1
  295. package/src/server/operations/ops/ax-presence.ts +1 -1
  296. package/src/server/operations/ops/ax-read.ts +57 -6
  297. package/src/server/operations/ops/ax-timeline.ts +28 -2
  298. package/src/server/operations/ops/backup.ts +5 -3
  299. package/src/server/operations/ops/batch.ts +35 -4
  300. package/src/server/operations/ops/boards.ts +188 -47
  301. package/src/server/operations/ops/canvas-wire.ts +31 -1
  302. package/src/server/operations/ops/edges.ts +36 -2
  303. package/src/server/operations/ops/groups.ts +6 -1
  304. package/src/server/operations/ops/lifecycle.ts +262 -0
  305. package/src/server/operations/ops/memory-graph.ts +45 -0
  306. package/src/server/operations/ops/nodes.ts +107 -24
  307. package/src/server/operations/ops/query.ts +281 -68
  308. package/src/server/operations/ops/snapshots.ts +73 -1
  309. package/src/server/operations/ops/validate.ts +5 -1
  310. package/src/server/operations/ops/viewport.ts +97 -3
  311. package/src/server/operations/registry.ts +74 -25
  312. package/src/server/placement.ts +29 -0
  313. package/src/server/scope-fence.ts +78 -6
  314. package/src/server/server.ts +115 -16
  315. package/src/server/spatial-analysis.ts +48 -23
  316. package/src/server/tour-control.ts +82 -0
  317. package/src/server/wiki-links.ts +137 -0
  318. package/src/server/workbench-auth.ts +11 -0
  319. package/src/shared/agent-context.ts +3 -0
  320. package/src/shared/agent-presence.ts +15 -15
  321. package/src/shared/auto-arrange.ts +2 -0
  322. package/src/shared/boards.ts +4 -4
  323. package/src/shared/knowledge-lifecycle.ts +22 -0
  324. package/src/shared/knowledge-presets.ts +80 -0
  325. package/src/shared/lifecycle-status.ts +26 -0
  326. package/src/shared/memory-graph.ts +74 -0
  327. package/src/shared/relations.ts +98 -0
  328. package/src/shared/semantic-attention.ts +15 -1
  329. package/src/shared/tour.ts +23 -5
  330. package/src/shared/workspace-settings.ts +13 -0
  331. package/dist/types/client/canvas/ContextPinBar.d.ts +0 -1
  332. package/dist/types/client/canvas/FocusFieldLayer.d.ts +0 -1
  333. package/dist/types/client/canvas/kind-colors.d.ts +0 -3
  334. package/dist/types/server/agent-context.d.ts +0 -22
  335. 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
- sessionActive,
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 = sessionActive.value ? mutatingNodeIds.value : new Set<string>();
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
- title="Click to jump · drag the frame to pan"
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 { useState } from 'preact/hooks';
2
- import { contextPinMeta, pinnedReadState, writerName } from '../state/context-status-store';
3
- import { centerDistance } from '../../server/spatial-analysis.js';
4
- import { nodes } from '../state/canvas-store';
5
- import { nearPins } from '../state/near-pin-store';
6
- import type { CanvasNodeState } from '../types';
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
- const pin = contextPinMeta.value[node.id];
35
- const pinnedBy =
36
- pin?.pinnedBy.actor === 'agent'
37
- ? ` Pinned by ${writerName(pin.pinnedBy)}${pin.reason ? `: ${pin.reason}` : ''}.`
38
- : '';
39
- const state = pinnedReadState(node.id, node.contentRevision ?? 0);
40
- if (state.kind === 'not-read') {
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: 'agent',
72
- glyph: 'spark',
73
- word: `by ${name}`,
74
- label: `Created by ${name}`,
75
- body: 'The byline stays until you edit the node.',
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 (node.lastEditedBy?.actor === 'agent') {
79
- const name = writerName(node.lastEditedBy);
56
+ if (state.kind === 'changed') {
80
57
  return {
81
- tone: 'agent',
82
- glyph: 'pen',
83
- word: 'edited',
84
- label: `Edited by ${name}`,
85
- body: 'The mark stays until you edit the node.',
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
- /** The header chip saying what agents did with this node (one per node; glyph + word). */
92
- export function NodeContextMark({ node, pinned }: { node: CanvasNodeState; pinned: boolean }) {
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 (!mark) return null;
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 class={`node-context-mark is-${mark.tone}`} data-mark={mark.word}>
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 never gets this node's full content.
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({ node, pinned }: { node: CanvasNodeState; pinned: boolean }) {
118
- // The pins' titles join this node's DOM only while the chip is hovered or
119
- // focused: at rest the node must not contain other nodes' titles.
120
- const [open, setOpen] = useState(false);
121
- const near = pinned ? undefined : nearPins.value.get(node.id);
122
- if (!near || near.length === 0) return null;
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
- // Distances change every drag frame; read them only while the hint is open.
125
- const where = open
126
- ? near
127
- .map((pin) => {
128
- const pinNode = nodes.value.get(pin.pinNodeId);
129
- return pinNode ? `“${pin.pinTitle}” (${Math.round(centerDistance(node, pinNode))} px)` : `“${pin.pinTitle}”`;
130
- })
131
- .join(', ')
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
- <BarHint
135
- label={open ? `Near ${where}` : 'Near a pin'}
136
- body="The agent gets its title and a short summary, not its full content. Pin it to send its content."
137
- align="end"
138
- tapToOpen
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
- <span
141
- class="node-near-mark"
142
- data-near={near.map((pin) => pin.pinNodeId).join(' ')}
143
- onMouseEnter={() => setOpen(true)}
144
- onMouseLeave={() => setOpen(false)}
145
- onFocusIn={() => setOpen(true)}
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
+ }