nudge-ui 0.2.1 → 0.2.3

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 (258) hide show
  1. package/README.md +164 -46
  2. package/dist/client.mjs +252 -54
  3. package/dist/css/value-semantics/colorSemantics.d.ts +4 -0
  4. package/dist/css/value-semantics/colorSemantics.js +17 -0
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -0
  6. package/dist/hosts/astro/clientTransport.js +41 -32
  7. package/dist/hosts/astro/integration.d.ts +2 -2
  8. package/dist/hosts/astro/integration.js +9 -4
  9. package/dist/hosts/astro/markdownIdentity.d.ts +28 -0
  10. package/dist/hosts/astro/markdownIdentity.js +81 -0
  11. package/dist/hosts/environment.d.ts +15 -0
  12. package/dist/hosts/environment.js +16 -0
  13. package/dist/hosts/next/manifest.js +1 -1
  14. package/dist/hosts/next/sidecar.js +12 -2
  15. package/dist/hosts/next/wrapper.d.ts +2 -0
  16. package/dist/hosts/next/wrapper.js +2 -1
  17. package/dist/hosts/projectBridge.d.ts +1 -0
  18. package/dist/hosts/projectBridge.js +1 -0
  19. package/dist/hosts/static/manifest.js +3 -1
  20. package/dist/hosts/static/server.js +4 -0
  21. package/dist/hosts/vite/vite.d.ts +4 -0
  22. package/dist/hosts/vite/vite.js +28 -5
  23. package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
  24. package/dist/inspector/agent/McpConnectionDialog.js +23 -22
  25. package/dist/inspector/agent/activity.d.ts +18 -0
  26. package/dist/inspector/agent/activity.js +70 -0
  27. package/dist/inspector/agent/client.d.ts +8 -0
  28. package/dist/inspector/agent/client.js +65 -5
  29. package/dist/inspector/agent/connectionStatus.d.ts +3 -3
  30. package/dist/inspector/agent/connectionStatus.js +9 -2
  31. package/dist/inspector/agent/httpTransport.d.ts +6 -0
  32. package/dist/inspector/agent/httpTransport.js +50 -3
  33. package/dist/inspector/agent/protocol.d.ts +9 -3
  34. package/dist/inspector/agent/protocol.js +1 -1
  35. package/dist/inspector/agent/verification.d.ts +11 -1
  36. package/dist/inspector/agent/verification.js +53 -25
  37. package/dist/inspector/canvas/AgentActivityOverlay.d.ts +13 -0
  38. package/dist/inspector/canvas/AgentActivityOverlay.js +111 -0
  39. package/dist/inspector/canvas/CanvasCard.d.ts +9 -2
  40. package/dist/inspector/canvas/CanvasCard.js +133 -38
  41. package/dist/inspector/canvas/CanvasElementOverlay.js +200 -22
  42. package/dist/inspector/canvas/CanvasToolIcons.d.ts +10 -0
  43. package/dist/inspector/canvas/CanvasToolIcons.js +20 -0
  44. package/dist/inspector/canvas/CanvasToolbar.js +8 -4
  45. package/dist/inspector/canvas/CanvasWorkspace.js +243 -56
  46. package/dist/inspector/canvas/StaleChangeIndicator.js +3 -13
  47. package/dist/inspector/canvas/agentPresentation.d.ts +8 -8
  48. package/dist/inspector/canvas/agentPresentation.js +64 -52
  49. package/dist/inspector/canvas/canvasGestures.d.ts +1 -1
  50. package/dist/inspector/canvas/canvasGestures.js +1 -1
  51. package/dist/inspector/canvas/canvasStore.d.ts +46 -82
  52. package/dist/inspector/canvas/canvasStore.js +251 -284
  53. package/dist/inspector/canvas/frameContent.d.ts +21 -0
  54. package/dist/inspector/canvas/frameContent.js +44 -0
  55. package/dist/inspector/canvas/frameProtocol.d.ts +34 -4
  56. package/dist/inspector/canvas/frameProtocol.js +9 -2
  57. package/dist/inspector/canvas/keyboardShortcuts.d.ts +4 -1
  58. package/dist/inspector/canvas/keyboardShortcuts.js +5 -3
  59. package/dist/inspector/canvas/layout.d.ts +52 -0
  60. package/dist/inspector/canvas/layout.js +106 -0
  61. package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
  62. package/dist/inspector/canvas/linkEligibility.js +0 -29
  63. package/dist/inspector/canvas/projection.d.ts +15 -5
  64. package/dist/inspector/canvas/projection.js +69 -13
  65. package/dist/inspector/canvas/rendererBootstrap.js +45 -4
  66. package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
  67. package/dist/inspector/canvas/rendererElementSelector.js +102 -45
  68. package/dist/inspector/canvas/rendererStylesheet.js +2 -2
  69. package/dist/inspector/canvas/resizeHandleScale.js +1 -1
  70. package/dist/inspector/canvas/sessionStore.d.ts +11 -26
  71. package/dist/inspector/canvas/sessionStore.js +43 -183
  72. package/dist/inspector/canvas/staleChangeDetector.js +9 -3
  73. package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
  74. package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
  75. package/dist/inspector/canvas/viewStore.d.ts +38 -0
  76. package/dist/inspector/canvas/viewStore.js +104 -0
  77. package/dist/inspector/canvas/wheelPan.d.ts +8 -0
  78. package/dist/inspector/canvas/wheelPan.js +8 -0
  79. package/dist/inspector/changes/changesLog.d.ts +6 -5
  80. package/dist/inspector/changes/changesLog.js +40 -32
  81. package/dist/inspector/changes/draftChanges.d.ts +56 -0
  82. package/dist/inspector/changes/draftChanges.js +300 -0
  83. package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
  84. package/dist/inspector/changes/previewDiagnostics.js +11 -25
  85. package/dist/inspector/clientManifest.d.ts +3 -1
  86. package/dist/inspector/clientManifest.js +1 -0
  87. package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
  88. package/dist/inspector/comments/CommentsOverlay.js +250 -0
  89. package/dist/inspector/comments/element.d.ts +6 -0
  90. package/dist/inspector/comments/element.js +47 -0
  91. package/dist/inspector/comments/store.d.ts +29 -0
  92. package/dist/inspector/comments/store.js +121 -0
  93. package/dist/inspector/comments/verification.d.ts +5 -0
  94. package/dist/inspector/comments/verification.js +52 -0
  95. package/dist/inspector/drafts/model.d.ts +35 -0
  96. package/dist/inspector/drafts/model.js +45 -0
  97. package/dist/inspector/drafts/store.d.ts +28 -0
  98. package/dist/inspector/drafts/store.js +231 -0
  99. package/dist/inspector/index.js +26 -35
  100. package/dist/inspector/inline-text/inlineTextEditor.js +7 -0
  101. package/dist/inspector/inspection/useBrowserCssInspection.js +15 -1
  102. package/dist/inspector/iterations/capture.d.ts +2 -0
  103. package/dist/inspector/iterations/capture.js +173 -0
  104. package/dist/inspector/iterations/client.d.ts +7 -0
  105. package/dist/inspector/iterations/client.js +52 -0
  106. package/dist/inspector/iterations/coordinator.d.ts +15 -0
  107. package/dist/inspector/iterations/coordinator.js +130 -0
  108. package/dist/inspector/iterations/prompt.d.ts +3 -0
  109. package/dist/inspector/iterations/prompt.js +26 -0
  110. package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
  111. package/dist/inspector/overlay/DomNavigator.js +46 -0
  112. package/dist/inspector/overlay/InspectorOverlay.js +6 -2
  113. package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
  114. package/dist/inspector/overlay/clickPolicy.js +34 -3
  115. package/dist/inspector/overlay/elementSelector.d.ts +1 -1
  116. package/dist/inspector/overlay/elementSelector.js +18 -7
  117. package/dist/inspector/overlay/interactionStyles.js +3 -1
  118. package/dist/inspector/overlay/spacingGestures.d.ts +15 -3
  119. package/dist/inspector/overlay/spacingGestures.js +88 -13
  120. package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
  121. package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
  122. package/dist/inspector/projection/renderedInstance.d.ts +2 -1
  123. package/dist/inspector/projection/renderedInstance.js +9 -5
  124. package/dist/inspector/projection/structuralProjection.d.ts +2 -1
  125. package/dist/inspector/projection/structuralProjection.js +19 -15
  126. package/dist/inspector/projection/textProjection.d.ts +2 -1
  127. package/dist/inspector/projection/textProjection.js +8 -4
  128. package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
  129. package/dist/inspector/prompt/clipboardHandoff.d.ts +10 -18
  130. package/dist/inspector/prompt/clipboardHandoff.js +92 -143
  131. package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
  132. package/dist/inspector/prompt/generatePrompt.js +17 -16
  133. package/dist/inspector/runtime/runtimeConfig.d.ts +10 -1
  134. package/dist/inspector/runtime/runtimeConfig.js +24 -0
  135. package/dist/inspector/selection/hierarchy.d.ts +6 -0
  136. package/dist/inspector/selection/hierarchy.js +8 -0
  137. package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
  138. package/dist/inspector/settings/SettingsDialog.js +22 -9
  139. package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
  140. package/dist/inspector/shell/AgentStatusButton.js +36 -0
  141. package/dist/inspector/shell/AppShell.js +4 -2
  142. package/dist/inspector/shell/ChangesLog.js +47 -27
  143. package/dist/inspector/shell/CopyPromptButton.js +146 -90
  144. package/dist/inspector/shell/EmptyState.js +9 -8
  145. package/dist/inspector/shell/InspectorShell.js +14 -25
  146. package/dist/inspector/shell/PeekOriginalButton.js +24 -23
  147. package/dist/inspector/shell/originalPreview.js +5 -5
  148. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  149. package/dist/inspector/shell/shortcuts.js +11 -0
  150. package/dist/inspector/sketch/SketchEditor.js +14 -14
  151. package/dist/inspector/sketch/SketchEntryButton.js +1 -1
  152. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
  153. package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
  154. package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
  155. package/dist/inspector/sketch/SketchOverlay.js +7 -7
  156. package/dist/inspector/sketch/SketchWorkspace.js +18 -12
  157. package/dist/inspector/sketch/interaction.d.ts +1 -0
  158. package/dist/inspector/sketch/interaction.js +8 -4
  159. package/dist/inspector/sketch/model.d.ts +5 -0
  160. package/dist/inspector/sketch/model.js +6 -2
  161. package/dist/inspector/sketch/raster.d.ts +1 -2
  162. package/dist/inspector/sketch/raster.js +68 -4
  163. package/dist/inspector/sketch/store.js +48 -23
  164. package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
  165. package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
  166. package/dist/inspector/styleEditors/BorderEditor.js +49 -78
  167. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
  168. package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
  169. package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
  170. package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
  171. package/dist/inspector/styleEditors/ColorPicker.js +10 -4
  172. package/dist/inspector/styleEditors/GapField.js +3 -5
  173. package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
  174. package/dist/inspector/styleEditors/GridChildSection.js +18 -9
  175. package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
  176. package/dist/inspector/styleEditors/GridPicker.js +8 -2
  177. package/dist/inspector/styleEditors/GridSection.js +1 -1
  178. package/dist/inspector/styleEditors/GridValueField.js +19 -14
  179. package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
  180. package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
  181. package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
  182. package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
  183. package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
  184. package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
  185. package/dist/inspector/styleEditors/LayoutSection.js +32 -11
  186. package/dist/inspector/styleEditors/SpacingBox.js +16 -9
  187. package/dist/inspector/styleEditors/Typography.js +11 -3
  188. package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
  189. package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
  190. package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
  191. package/dist/inspector/styleEditors/gradientValue.js +114 -0
  192. package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
  193. package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
  194. package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
  195. package/dist/inspector/styleEditors/layoutValue.js +0 -42
  196. package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
  197. package/dist/inspector/styleEditors/nudgeValue.js +41 -11
  198. package/dist/inspector/tokens/TokenField.d.ts +7 -0
  199. package/dist/inspector/tokens/TokenField.js +49 -22
  200. package/dist/inspector/tokens/TokenLabel.js +5 -14
  201. package/dist/inspector/tokens/TokensPanel.js +2 -1
  202. package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
  203. package/dist/inspector/tokens/dragPointerLock.js +70 -0
  204. package/dist/inspector/tokens/editActions.d.ts +1 -1
  205. package/dist/inspector/tokens/editActions.js +1 -1
  206. package/dist/inspector/ui/AtRuleContext.js +4 -6
  207. package/dist/inspector/ui/Button.d.ts +6 -2
  208. package/dist/inspector/ui/Button.js +4 -3
  209. package/dist/inspector/ui/ControlSurface.d.ts +2 -2
  210. package/dist/inspector/ui/ControlSurface.js +4 -3
  211. package/dist/inspector/ui/Disclosure.d.ts +22 -0
  212. package/dist/inspector/ui/Disclosure.js +13 -0
  213. package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
  214. package/dist/inspector/ui/InlineStyleWarning.js +13 -0
  215. package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
  216. package/dist/inspector/ui/InspectorPopover.js +4 -5
  217. package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
  218. package/dist/inspector/ui/PopoverListbox.js +8 -7
  219. package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
  220. package/dist/inspector/ui/SegmentedControl.js +2 -2
  221. package/dist/inspector/ui/Select.d.ts +6 -1
  222. package/dist/inspector/ui/Select.js +7 -11
  223. package/dist/inspector/ui/SideValuesField.js +1 -1
  224. package/dist/inspector/ui/Tooltip.d.ts +26 -0
  225. package/dist/inspector/ui/Tooltip.js +32 -0
  226. package/dist/inspector/ui/classNames.d.ts +2 -0
  227. package/dist/inspector/ui/classNames.js +4 -0
  228. package/dist/inspector/ui/customCursors.d.ts +23 -0
  229. package/dist/inspector/ui/customCursors.js +122 -0
  230. package/dist/inspector/ui/dragCursor.d.ts +6 -0
  231. package/dist/inspector/ui/dragCursor.js +13 -0
  232. package/dist/inspector/ui/portalContainer.d.ts +2 -0
  233. package/dist/inspector/ui/portalContainer.js +6 -0
  234. package/dist/inspector/ui/styles.js +34 -24
  235. package/dist/inspector/workspace/commands.d.ts +9 -0
  236. package/dist/inspector/workspace/commands.js +82 -0
  237. package/dist/inspector/workspace/controller.d.ts +4 -0
  238. package/dist/inspector/workspace/controller.js +66 -0
  239. package/dist/inspector/workspace/handoff.d.ts +42 -0
  240. package/dist/inspector/workspace/handoff.js +115 -0
  241. package/dist/inspector/workspace/timeline.d.ts +30 -0
  242. package/dist/inspector/workspace/timeline.js +108 -0
  243. package/dist/internal/agent-protocol.d.ts +19 -2
  244. package/dist/internal/agent-protocol.js +13 -0
  245. package/dist/nudge-ui.mjs +560 -299
  246. package/dist/project/artifacts.d.ts +13 -0
  247. package/dist/project/artifacts.js +253 -0
  248. package/dist/transport/artifacts.d.ts +8 -0
  249. package/dist/transport/artifacts.js +9 -0
  250. package/dist/transport/editor.d.ts +20 -1
  251. package/dist/transport/editor.js +44 -11
  252. package/dist/transport/routes.d.ts +6 -1
  253. package/dist/transport/routes.js +6 -1
  254. package/package.json +6 -2
  255. package/dist/inspector/changes/workspaceChanges.d.ts +0 -43
  256. package/dist/inspector/changes/workspaceChanges.js +0 -239
  257. package/dist/inspector/shell/DomNavigation.d.ts +0 -8
  258. package/dist/inspector/shell/DomNavigation.js +0 -30
@@ -1,29 +1,48 @@
1
+ import { iterationId, contentSourceUrl, contentEditTarget } from "./frameContent.js";
2
+ import { recordCanvasChange, discardCanvasHistory, isDraftLocked, forgetDraft } from "../changes/draftChanges.js";
3
+ import { targetKey } from "../drafts/model.js";
1
4
  import { useSyncExternalStore } from "react";
5
+ import { DEFAULT_CAMERA, getBoardCamera, placeBoardCamera, restoreCanvasView, setCanvasMode, } from "./viewStore.js";
2
6
  import { normalizeUrl, normalizedUrlKey } from "./normalizeUrl.js";
3
- export const MIN_CAMERA_ZOOM = 0.25;
4
- export const MAX_CAMERA_ZOOM = 3;
7
+ import { removeHtmlArtifact } from "../iterations/client.js";
8
+ import { applyLayoutShifts, boundsOf, centerCamera, fitCamera, layoutShifts, nextCardPosition, pushOverlappingCards, reinsertInRecordedOrder, iterationPosition, } from "./layout.js";
9
+ const frameGroups = new Map();
10
+ export function setFrameGroup(group) { frameGroups.set(group.id, group); }
11
+ export function getFrameGroup(id) { return frameGroups.get(id); }
12
+ export function removeFrameGroup(id) { frameGroups.delete(id); }
13
+ export function getFrameGroups() {
14
+ return [...frameGroups.values()].filter((group) => cards.some((card) => card.groupId === group.id));
15
+ }
16
+ export function isFrameGroup(value) {
17
+ if (!value || typeof value !== "object")
18
+ return false;
19
+ const group = value;
20
+ if (typeof group.id !== "string" || group.id.length > 256)
21
+ return false;
22
+ return group.kind === "agent" && typeof group.label === "string" && typeof group.agentId === "string"
23
+ && Array.isArray(group.routes) && group.routes.every((route) => {
24
+ try {
25
+ return typeof route.url === "string" && new URL(route.url).origin === window.location.origin;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ });
31
+ }
5
32
  export const CARD_GAP = 40;
6
33
  export const FIT_ALL_PADDING = 80;
7
34
  export const PRIMARY_CARD_INSET = 40;
8
- const DEFAULT_CAMERA = { x: 0, y: 0, zoom: 1 };
9
35
  function defaultViewportSize() {
10
36
  return {
11
37
  width: window.innerWidth || 1024,
12
38
  height: window.innerHeight || 768,
13
39
  };
14
40
  }
15
- let mode = "inspect";
16
- let presentation = "focus";
17
- let presentationTransitioning = false;
18
- let presentationTransitionTimer = null;
19
41
  let cards = [];
20
- let comparisonGroups = [];
21
42
  let focusedCardId = null;
22
43
  let selectedCardId = null;
23
44
  let cardIdCounter = 0;
24
- let cachedBoardCamera = { ...DEFAULT_CAMERA };
25
45
  let lastUsedCardSize = null;
26
- let fitAllRan = false;
27
46
  const listeners = new Set();
28
47
  function subscribe(cb) {
29
48
  listeners.add(cb);
@@ -31,102 +50,39 @@ function subscribe(cb) {
31
50
  listeners.delete(cb);
32
51
  };
33
52
  }
34
- function getMode() {
35
- return mode;
36
- }
37
- function getPresentation() {
38
- return presentation;
39
- }
40
- function getPresentationTransitioning() {
41
- return presentationTransitioning;
42
- }
43
53
  function getCards() {
44
54
  return cards;
45
55
  }
46
- function getComparisonGroups() {
47
- return comparisonGroups;
48
- }
49
- function notify() {
50
- listeners.forEach((l) => l());
51
- }
52
- function computeNewCardPosition(existingCards, gap) {
53
- if (existingCards.length === 0)
54
- return { x: 0, y: 0 };
55
- let rightmostEdge = -Infinity;
56
- for (const c of existingCards) {
57
- const edge = c.x + c.width + gap;
58
- if (edge > rightmostEdge)
59
- rightmostEdge = edge;
60
- }
61
- const rowY = existingCards[0].y;
62
- return { x: rightmostEdge, y: rowY };
63
- }
64
- function boundsOf(cards) {
65
- let minX = Infinity;
66
- let minY = Infinity;
67
- let maxX = -Infinity;
68
- let maxY = -Infinity;
69
- for (const card of cards) {
70
- minX = Math.min(minX, card.x);
71
- minY = Math.min(minY, card.y);
72
- maxX = Math.max(maxX, card.x + card.width);
73
- maxY = Math.max(maxY, card.y + card.height);
56
+ let notificationDepth = 0;
57
+ let notificationPending = false;
58
+ export function batchCanvasChanges(operation) {
59
+ notificationDepth += 1;
60
+ try {
61
+ return operation();
74
62
  }
75
- return { minX, minY, maxX, maxY };
76
- }
77
- export function setCanvasMode(newMode) {
78
- if (mode === newMode)
79
- return;
80
- mode = newMode;
81
- notify();
82
- }
83
- /**
84
- * Changes how the mounted iframe workspace is presented.
85
- *
86
- * Canvas starts from the active card centered in the supplied viewport. Card
87
- * geometry remains durable, but the camera does not carry over from a prior
88
- * Canvas session.
89
- */
90
- export function setCanvasPresentation(next, viewport) {
91
- if (presentation === next)
92
- return;
93
- if (next === "canvas")
94
- resetCameraToActiveCard(viewport);
95
- presentation = next;
96
- presentationTransitioning = !window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
97
- if (presentationTransitionTimer !== null)
98
- window.clearTimeout(presentationTransitionTimer);
99
- presentationTransitionTimer = presentationTransitioning
100
- ? window.setTimeout(() => {
101
- presentationTransitionTimer = null;
102
- presentationTransitioning = false;
63
+ finally {
64
+ notificationDepth -= 1;
65
+ if (notificationDepth === 0 && notificationPending) {
66
+ notificationPending = false;
103
67
  notify();
104
- }, 220)
105
- : null;
106
- notify();
68
+ }
69
+ }
107
70
  }
108
- function resetCameraToActiveCard(viewport) {
109
- const activeCardId = selectedCardId ?? focusedCardId;
110
- const activeCard = cards.find((card) => card.id === activeCardId) ?? cards[0];
111
- if (!activeCard) {
112
- cachedBoardCamera = { ...DEFAULT_CAMERA };
71
+ function notify() {
72
+ if (notificationDepth > 0) {
73
+ notificationPending = true;
113
74
  return;
114
75
  }
115
- const zoom = 0.9;
116
- const view = viewport ?? defaultViewportSize();
117
- cachedBoardCamera = {
118
- x: view.width / 2 - (activeCard.x + activeCard.width / 2) * zoom,
119
- y: view.height / 2 - (activeCard.y + activeCard.height / 2) * zoom,
120
- zoom,
121
- };
76
+ listeners.forEach((l) => l());
122
77
  }
123
78
  /** Adds one card while retaining the existing placement policy. */
124
79
  export function addCanvasCard(url, title) {
80
+ const before = cards;
125
81
  const size = lastUsedCardSize ?? defaultViewportSize();
126
- const pos = computeNewCardPosition(cards, CARD_GAP);
82
+ const pos = nextCardPosition(cards, CARD_GAP);
127
83
  const card = {
128
84
  id: `card-${++cardIdCounter}`,
129
- url,
85
+ content: { kind: "route", url },
130
86
  title: title ?? null,
131
87
  x: pos.x,
132
88
  y: pos.y,
@@ -135,65 +91,90 @@ export function addCanvasCard(url, title) {
135
91
  };
136
92
  cards = [...cards, card];
137
93
  lastUsedCardSize = { width: size.width, height: size.height };
94
+ recordCreation(before, [card]);
138
95
  notify();
139
96
  return card;
140
97
  }
141
- /**
142
- * Appends an agent-created comparison group without disturbing existing cards.
143
- * Callers validate route URLs and labels at the command boundary; this store
144
- * function only enforces the durable identity and placement invariants.
145
- */
146
- export function appendCanvasComparisonGroup(input) {
147
- if (!input.id || !input.label || !input.agentId || input.routes.length === 0)
148
- return null;
149
- if (comparisonGroups.some((group) => group.id === input.id))
98
+ export function appendAgentRouteCards(groupId, routes) {
99
+ if (!groupId || routes.length === 0)
150
100
  return null;
101
+ const before = cards;
151
102
  const nextCards = [];
152
103
  let nextCardsSnapshot = cards;
153
- for (const route of input.routes) {
104
+ for (const route of routes) {
154
105
  const size = lastUsedCardSize ?? defaultViewportSize();
155
- const pos = computeNewCardPosition(nextCardsSnapshot, CARD_GAP);
106
+ const pos = nextCardPosition(nextCardsSnapshot, CARD_GAP);
156
107
  const card = {
157
108
  id: `card-${++cardIdCounter}`,
158
- url: route.url,
109
+ content: { kind: "route", url: route.url },
159
110
  title: route.title ?? null,
160
111
  x: pos.x,
161
112
  y: pos.y,
162
113
  width: size.width,
163
114
  height: size.height,
164
- comparisonGroupId: input.id,
115
+ groupId: groupId,
165
116
  };
166
117
  nextCards.push(card);
167
118
  nextCardsSnapshot = [...nextCardsSnapshot, card];
168
119
  lastUsedCardSize = { width: size.width, height: size.height };
169
120
  }
170
- const group = {
171
- id: input.id,
172
- label: input.label,
173
- owner: "agent",
174
- agentId: input.agentId,
175
- cardIds: nextCards.map((card) => card.id),
176
- routes: input.routes.map((route) => ({ ...route })),
177
- };
178
121
  cards = [...cards, ...nextCards];
179
- comparisonGroups = [...comparisonGroups, group];
180
- notify();
181
- return { group, cards: nextCards };
182
- }
183
- export function getCanvasComparisonGroup(id) {
184
- return comparisonGroups.find((group) => group.id === id);
185
- }
186
- /** Removes a group and exactly the cards owned by that group. */
187
- export function removeCanvasComparisonGroup(id) {
188
- const group = getCanvasComparisonGroup(id);
189
- if (!group)
190
- return null;
191
- const groupCardIds = new Set(group.cardIds);
192
- cards = cards.filter((card) => !groupCardIds.has(card.id));
193
- comparisonGroups = comparisonGroups.filter((candidate) => candidate.id !== id);
194
- selectFallbackAfterRemoval(groupCardIds);
122
+ recordCreation(before, nextCards);
195
123
  notify();
196
- return group;
124
+ return nextCards;
125
+ }
126
+ let activateHistoryCard = () => undefined;
127
+ /** Connects canvas replay to the draft controller without coupling the stores. */
128
+ export function setCanvasHistoryActivator(activate) {
129
+ activateHistoryCard = activate;
130
+ }
131
+ function recordCreation(before, created) {
132
+ const createdIds = new Set(created.map((card) => card.id));
133
+ const after = cards.map((card) => ({ ...card }));
134
+ const createdGroups = new Map(created.flatMap((card) => {
135
+ const group = card.groupId ? getFrameGroup(card.groupId) : undefined;
136
+ return group ? [[group.id, group]] : [];
137
+ }));
138
+ const oldFocus = selectedCardId ?? focusedCardId ?? before[0]?.id;
139
+ const shifts = layoutShifts(before, cards);
140
+ const restore = (redo) => {
141
+ if (!redo) {
142
+ // Direct creation drags finish placement after the creation was recorded.
143
+ // Redo must recover the dropped position, dimensions, and loaded title.
144
+ for (const [index, recorded] of after.entries()) {
145
+ const current = createdIds.has(recorded.id) && cards.find((card) => card.id === recorded.id);
146
+ if (current)
147
+ after[index] = { ...current };
148
+ }
149
+ }
150
+ cards = applyLayoutShifts(cards.filter((card) => !createdIds.has(card.id)), shifts, redo);
151
+ if (redo)
152
+ cards = reinsertInRecordedOrder(cards, after, createdIds);
153
+ for (const [id, group] of createdGroups) {
154
+ if (!cards.some((card) => card.groupId === id))
155
+ removeFrameGroup(id);
156
+ else if (!getFrameGroup(id))
157
+ setFrameGroup(group);
158
+ }
159
+ const focus = redo ? created[0]?.id : oldFocus;
160
+ const target = cards.find((card) => card.id === focus) ?? cards[0];
161
+ focusedCardId = target?.id ?? null;
162
+ selectedCardId = focusedCardId;
163
+ if (target) {
164
+ activateHistoryCard(target.id);
165
+ focusCanvasCards([target.id]);
166
+ }
167
+ notify();
168
+ };
169
+ recordCanvasChange({
170
+ cardIds: [...createdIds],
171
+ undo: () => restore(false),
172
+ redo: () => restore(true),
173
+ dispose: () => {
174
+ for (const card of created)
175
+ releaseUnusedIteration(card);
176
+ },
177
+ });
197
178
  }
198
179
  function selectFallbackAfterRemoval(removedIds) {
199
180
  const fallbackId = cards[0]?.id ?? null;
@@ -202,65 +183,98 @@ function selectFallbackAfterRemoval(removedIds) {
202
183
  if (removedIds.has(focusedCardId ?? ""))
203
184
  focusedCardId = fallbackId;
204
185
  }
205
- /** Clears group metadata while leaving ordinary cards intact. */
206
- export function clearCanvasComparisonGroups() {
207
- if (comparisonGroups.length === 0 && !cards.some((card) => card.comparisonGroupId))
186
+ function releaseUnusedIteration(card) {
187
+ const artifactId = iterationId(card.content);
188
+ if (!artifactId || cards.some((candidate) => iterationId(candidate.content) === artifactId))
208
189
  return;
209
- cards = cards.map(({ comparisonGroupId: _comparisonGroupId, ...card }) => card);
210
- comparisonGroups = [];
211
- notify();
190
+ forgetDraft(targetKey(contentEditTarget(card.content)));
191
+ void removeHtmlArtifact(artifactId).catch((error) => {
192
+ console.warn("Nudge UI could not remove an unused HTML iteration:", error);
193
+ });
212
194
  }
213
- /**
214
- * Finds the route paired with `cardId` inside the agent group `groupId` and
215
- * replaces that group with `patchGroup`, which receives the paired route
216
- * index.
217
- */
218
- function updateComparisonGroupRoutes(groupId, cardId, patchGroup) {
219
- comparisonGroups = comparisonGroups.map((group) => {
220
- if (group.id !== groupId)
221
- return group;
222
- const routeIndex = group.cardIds.indexOf(cardId);
223
- if (routeIndex < 0)
224
- return group;
225
- return patchGroup(group, routeIndex);
195
+ function recordRemoval(before, removed) {
196
+ let snapshot = removed;
197
+ const group = removed.groupId ? getFrameGroup(removed.groupId) : undefined;
198
+ recordCanvasChange({
199
+ cardIds: [removed.id],
200
+ undo: () => {
201
+ cards = reinsertInRecordedOrder(cards, before.map((card) => card.id === removed.id ? snapshot : card), new Set([removed.id]));
202
+ if (group)
203
+ setFrameGroup(group);
204
+ selectCard(removed.id);
205
+ activateHistoryCard(removed.id);
206
+ focusCanvasCards([removed.id]);
207
+ },
208
+ redo: () => {
209
+ snapshot = cards.find((card) => card.id === removed.id) ?? snapshot;
210
+ cards = cards.filter((card) => card.id !== removed.id);
211
+ selectFallbackAfterRemoval(new Set([removed.id]));
212
+ notify();
213
+ },
214
+ dispose: () => releaseUnusedIteration(snapshot),
226
215
  });
227
216
  }
228
- export function removeCanvasCard(id) {
217
+ export function removeCanvasCard(id, { undoable = false } = {}) {
218
+ if (isDraftLocked())
219
+ return;
229
220
  const removed = cards.find((card) => card.id === id);
230
221
  if (!removed)
231
222
  return;
223
+ const before = cards;
224
+ if (!undoable)
225
+ discardCanvasHistory(id);
232
226
  cards = cards.filter((c) => c.id !== id);
233
227
  selectFallbackAfterRemoval(new Set([id]));
234
- if (removed?.comparisonGroupId) {
235
- updateComparisonGroupRoutes(removed.comparisonGroupId, id, (group, routeIndex) => ({
236
- ...group,
237
- cardIds: group.cardIds.filter((_cardId, index) => index !== routeIndex),
238
- routes: group.routes.filter((_route, index) => index !== routeIndex),
239
- }));
240
- comparisonGroups = comparisonGroups.filter((group) => group.cardIds.length > 0);
241
- }
228
+ if (undoable)
229
+ recordRemoval(before, removed);
242
230
  notify();
231
+ if (!undoable)
232
+ releaseUnusedIteration(removed);
243
233
  }
244
234
  /**
245
235
  * Opens one editor target in the iframe workspace without replacing restored
246
236
  * cards or their dimensions.
247
237
  */
248
- export function activateIframeWorkspace(url, viewport) {
238
+ export function activateIframeWorkspace(url, viewport, options = {}) {
249
239
  const normalized = normalizeUrl(url);
250
240
  if (!normalized || normalized.origin !== window.location.origin)
251
241
  return null;
252
242
  const targetHref = new URL(url).href;
253
- mode = "canvas";
243
+ setCanvasMode("canvas");
244
+ const active = cards.find((card) => card.id === (selectedCardId ?? focusedCardId));
245
+ if (options.preserveIterationFocus && active?.content.kind === "iteration") {
246
+ const sourceRoute = normalizeUrl(active.content.sourceUrl);
247
+ if (sourceRoute && normalizedUrlKey(sourceRoute) === normalizedUrlKey(normalized)) {
248
+ focusedCardId = active.id;
249
+ selectedCardId = active.id;
250
+ notify();
251
+ return active;
252
+ }
253
+ }
254
+ if (options.replaceActiveCard && active && !iterationId(active.content)) {
255
+ const oldRoute = normalizeUrl(contentSourceUrl(active.content));
256
+ const changedRoute = !oldRoute || normalizedUrlKey(oldRoute) !== normalizedUrlKey(normalized);
257
+ const next = {
258
+ ...active,
259
+ content: { kind: "route", url: targetHref, ...(contentSourceUrl(active.content) !== targetHref ? { navigationUrl: targetHref } : {}) },
260
+ ...(changedRoute ? { title: null, groupId: undefined, entrance: undefined } : {}),
261
+ };
262
+ cards = cards.map((card) => card.id === active.id ? next : card);
263
+ focusedCardId = active.id;
264
+ selectedCardId = active.id;
265
+ notify();
266
+ return next;
267
+ }
254
268
  const focused = cards.find((card) => card.id === focusedCardId);
255
- const focusedUrl = focused ? normalizeUrl(focused.url) : null;
269
+ const focusedUrl = focused?.content.kind === "route" ? normalizeUrl(focused.content.url) : null;
256
270
  const existing = focused && focusedUrl
257
271
  && normalizedUrlKey(focusedUrl) === normalizedUrlKey(normalized)
258
272
  ? focused
259
- : cards.find((card) => card.url === targetHref) ?? findCardByNormalizedUrl(normalized);
273
+ : cards.find((card) => card.content.kind === "route" && card.content.url === targetHref) ?? findCardByNormalizedUrl(normalized);
260
274
  if (existing) {
261
275
  let activated = existing;
262
- if (existing.url !== targetHref) {
263
- activated = { ...existing, url: targetHref, navigationUrl: targetHref };
276
+ if (contentSourceUrl(existing.content) !== targetHref) {
277
+ activated = { ...existing, content: { kind: "route", url: targetHref, navigationUrl: targetHref } };
264
278
  cards = cards.map((card) => card.id === existing.id ? activated : card);
265
279
  }
266
280
  focusedCardId = existing.id;
@@ -273,7 +287,7 @@ export function activateIframeWorkspace(url, viewport) {
273
287
  const height = Math.max(150, viewport.height - PRIMARY_CARD_INSET * 2);
274
288
  const card = {
275
289
  id: `card-${++cardIdCounter}`,
276
- url: targetHref,
290
+ content: { kind: "route", url: targetHref },
277
291
  title: null,
278
292
  x: 0,
279
293
  y: 0,
@@ -284,8 +298,7 @@ export function activateIframeWorkspace(url, viewport) {
284
298
  lastUsedCardSize = { width, height };
285
299
  focusedCardId = card.id;
286
300
  selectedCardId = card.id;
287
- cachedBoardCamera = { x: PRIMARY_CARD_INSET, y: PRIMARY_CARD_INSET, zoom: 1 };
288
- fitAllRan = true;
301
+ placeBoardCamera({ x: PRIMARY_CARD_INSET, y: PRIMARY_CARD_INSET, zoom: 1 });
289
302
  notify();
290
303
  return card;
291
304
  }
@@ -296,46 +309,51 @@ export function activateIframeWorkspace(url, viewport) {
296
309
  return card;
297
310
  }
298
311
  export function updateCardTitle(id, title) {
299
- const card = cards.find((candidate) => candidate.id === id);
300
312
  cards = cards.map((c) => (c.id === id ? { ...c, title } : c));
301
- if (card?.comparisonGroupId) {
302
- updateComparisonGroupRoutes(card.comparisonGroupId, id, (group, routeIndex) => ({
303
- ...group,
304
- routes: group.routes.map((route, index) => index === routeIndex ? { ...route, title } : route),
305
- }));
306
- }
307
313
  notify();
308
314
  }
309
315
  export function updateCardUrl(id, url) {
310
- const card = cards.find((candidate) => candidate.id === id);
311
- cards = cards.map((c) => (c.id === id ? { ...c, url } : c));
312
- if (card?.comparisonGroupId) {
313
- updateComparisonGroupRoutes(card.comparisonGroupId, id, (group, routeIndex) => ({
314
- ...group,
315
- routes: group.routes.map((route, index) => index === routeIndex ? { ...route, url } : route),
316
- }));
317
- }
316
+ cards = cards.map((c) => (c.id === id && c.content.kind === "route" ? { ...c, content: { ...c.content, url } } : c));
318
317
  notify();
319
318
  }
320
319
  export function duplicateCard(sourceId) {
320
+ const before = cards;
321
321
  const source = cards.find((c) => c.id === sourceId);
322
- if (!source)
322
+ if (!source || source.content.kind !== "route")
323
323
  return null;
324
324
  const size = lastUsedCardSize ?? { width: source.width, height: source.height };
325
- const pos = computeNewCardPosition(cards, CARD_GAP);
326
325
  const card = {
327
326
  id: `card-${++cardIdCounter}`,
328
- url: source.url,
327
+ content: { kind: "route", url: source.content.url },
328
+ entrance: "linked",
329
329
  title: source.title,
330
- x: pos.x,
331
- y: pos.y,
330
+ x: source.x + source.width + CARD_GAP,
331
+ y: source.y,
332
332
  width: size.width,
333
333
  height: size.height,
334
334
  };
335
- cards = [...cards, card];
335
+ const index = cards.indexOf(source);
336
+ cards = [...cards.slice(0, index + 1), card, ...cards.slice(index + 1)];
337
+ cards = pushOverlappingCards(cards, new Set([source.id, card.id]), CARD_GAP);
336
338
  lastUsedCardSize = { width: size.width, height: size.height };
339
+ recordCreation(before, [card]);
337
340
  notify();
338
- return card;
341
+ return cards.find((c) => c.id === card.id);
342
+ }
343
+ export function addCanvasIteration(sourceId, artifactId, position, animateEntrance = true) {
344
+ const before = cards;
345
+ const source = cards.find((card) => card.id === sourceId);
346
+ if (!source)
347
+ return null;
348
+ const { x, y } = position ?? iterationPosition(cards, source, CARD_GAP);
349
+ const iteration = {
350
+ id: `card-${++cardIdCounter}`, content: { kind: "iteration", sourceUrl: contentSourceUrl(source.content), artifactId }, title: source.title,
351
+ width: source.width, height: source.height, x, y, entrance: "iteration", animateEntrance,
352
+ };
353
+ cards = [...cards, iteration];
354
+ recordCreation(before, [iteration]);
355
+ notify();
356
+ return iteration;
339
357
  }
340
358
  export function resizeCard(id, width, height, position) {
341
359
  cards = cards.map((c) => (c.id === id
@@ -344,88 +362,33 @@ export function resizeCard(id, width, height, position) {
344
362
  lastUsedCardSize = { width, height };
345
363
  notify();
346
364
  }
347
- export function getBoardCamera() {
348
- return cachedBoardCamera;
349
- }
350
- export function setBoardCamera(camera) {
351
- cachedBoardCamera = {
352
- x: camera.x,
353
- y: camera.y,
354
- zoom: Math.max(MIN_CAMERA_ZOOM, Math.min(MAX_CAMERA_ZOOM, camera.zoom)),
355
- };
356
- notify();
357
- }
358
- export function updateBoardCamera(partial) {
359
- const next = { ...cachedBoardCamera, ...partial };
360
- if (partial.zoom !== undefined) {
361
- next.zoom = Math.max(MIN_CAMERA_ZOOM, Math.min(MAX_CAMERA_ZOOM, partial.zoom));
362
- }
363
- cachedBoardCamera = next;
364
- notify();
365
- }
366
365
  export function fitAllCards(viewport) {
367
- fitCanvasCards(cards.map((card) => card.id), viewport);
368
- }
369
- /** Fits a selected set of cards while preserving cards outside that set. */
370
- export function fitCanvasCards(cardIds, viewport) {
371
- const selectedIds = new Set(cardIds);
372
- const selectedCards = cards.filter((card) => selectedIds.has(card.id));
373
- if (selectedCards.length === 0)
374
- return false;
375
- const { minX, minY, maxX, maxY } = boundsOf(selectedCards);
376
- const contentW = maxX - minX;
377
- const contentH = maxY - minY;
378
- if (contentW <= 0 || contentH <= 0) {
379
- cachedBoardCamera = { ...DEFAULT_CAMERA };
380
- fitAllRan = true;
381
- notify();
382
- return true;
383
- }
384
- const viewW = Math.max(1, (viewport?.width ?? window.innerWidth) - FIT_ALL_PADDING * 2);
385
- const viewH = Math.max(1, (viewport?.height ?? window.innerHeight) - FIT_ALL_PADDING * 2);
386
- const zoomX = viewW / contentW;
387
- const zoomY = viewH / contentH;
388
- const zoom = Math.max(MIN_CAMERA_ZOOM, Math.min(zoomX, zoomY, MAX_CAMERA_ZOOM));
389
- const contentCenterX = minX + contentW / 2;
390
- const contentCenterY = minY + contentH / 2;
391
- cachedBoardCamera = {
392
- x: -(contentCenterX * zoom) + (viewport?.width ?? window.innerWidth) / 2,
393
- y: -(contentCenterY * zoom) + (viewport?.height ?? window.innerHeight) / 2,
394
- zoom,
395
- };
396
- fitAllRan = true;
397
- notify();
398
- return true;
366
+ const bounds = selectedBounds(cards.map((card) => card.id));
367
+ if (!bounds)
368
+ return;
369
+ placeBoardCamera(fitCamera(bounds, viewport ?? windowViewport(), FIT_ALL_PADDING) ?? DEFAULT_CAMERA);
399
370
  }
400
371
  /** Centers a selected set of cards without changing the current zoom. */
401
372
  export function focusCanvasCards(cardIds, viewport) {
402
- const selectedIds = new Set(cardIds);
403
- const selectedCards = cards.filter((card) => selectedIds.has(card.id));
404
- if (selectedCards.length === 0)
373
+ const bounds = selectedBounds(cardIds);
374
+ if (!bounds)
405
375
  return false;
406
- const { minX, minY, maxX, maxY } = boundsOf(selectedCards);
407
- const viewW = viewport?.width ?? window.innerWidth;
408
- const viewH = viewport?.height ?? window.innerHeight;
409
- const contentCenterX = minX + (maxX - minX) / 2;
410
- const contentCenterY = minY + (maxY - minY) / 2;
411
- cachedBoardCamera = {
412
- x: -(contentCenterX * cachedBoardCamera.zoom) + viewW / 2,
413
- y: -(contentCenterY * cachedBoardCamera.zoom) + viewH / 2,
414
- zoom: cachedBoardCamera.zoom,
415
- };
416
- fitAllRan = true;
417
- notify();
376
+ placeBoardCamera(centerCamera(bounds, getBoardCamera().zoom, viewport ?? windowViewport()));
418
377
  return true;
419
378
  }
420
- export function hasFitAllRan() {
421
- return fitAllRan;
379
+ function selectedBounds(cardIds) {
380
+ const selectedIds = new Set(cardIds);
381
+ const selected = cards.filter((card) => selectedIds.has(card.id));
382
+ return selected.length ? boundsOf(selected) : null;
422
383
  }
423
- export function resetFitAllFlag() {
424
- fitAllRan = false;
384
+ function windowViewport() {
385
+ return { width: window.innerWidth, height: window.innerHeight };
425
386
  }
426
387
  export function findCardByNormalizedUrl(normalized) {
427
388
  return cards.find((c) => {
428
- const n = normalizeUrl(c.url);
389
+ if (c.content.kind !== "route")
390
+ return false;
391
+ const n = normalizeUrl(c.content.url);
429
392
  if (!n)
430
393
  return false;
431
394
  return normalizedUrlKey(n) === normalizedUrlKey(normalized);
@@ -461,20 +424,18 @@ export function setCardPosition(id, x, y) {
461
424
  cards = cards.map((c) => (c.id === id ? { ...c, x, y } : c));
462
425
  notify();
463
426
  }
464
- export function hydrateCanvasStore(newMode, newCards, newCamera, newComparisonGroups = [], restoredFocusedCardId = null) {
465
- mode = newMode;
466
- cards = newCards.map((card) => ({ ...card }));
467
- comparisonGroups = newComparisonGroups.map((group) => ({
468
- ...group,
469
- owner: "agent",
470
- cardIds: [...group.cardIds],
471
- routes: group.routes.map((route) => ({ ...route })),
427
+ export function hydrateCanvasStore(newMode, newCards, newCamera, restoredFocusedCardId = null, restoredPresentation = "focus", restoredGroups = []) {
428
+ frameGroups.clear();
429
+ for (const group of restoredGroups)
430
+ frameGroups.set(group.id, group);
431
+ cards = newCards.map((card) => ({
432
+ ...card,
433
+ groupId: card.content.kind === "route" && card.groupId && frameGroups.has(card.groupId) ? card.groupId : undefined,
472
434
  }));
473
435
  focusedCardId = newCards.some((card) => card.id === restoredFocusedCardId)
474
436
  ? restoredFocusedCardId
475
437
  : null;
476
438
  selectedCardId = null;
477
- cachedBoardCamera = { ...newCamera };
478
439
  const lastCard = newCards.at(-1);
479
440
  lastUsedCardSize = lastCard
480
441
  ? { width: lastCard.width, height: lastCard.height }
@@ -491,25 +452,31 @@ export function hydrateCanvasStore(newMode, newCards, newCamera, newComparisonGr
491
452
  }
492
453
  cardIdCounter = Math.max(cardIdCounter, maxNum);
493
454
  }
494
- fitAllRan = true;
455
+ restoreCanvasView({ mode: newMode, presentation: restoredPresentation, camera: newCamera });
495
456
  notify();
496
457
  }
497
- export { subscribe, getMode, getCards, getComparisonGroups, getMode as getCanvasMode, getCards as getCanvasCards, getComparisonGroups as getCanvasComparisonGroups, };
498
- export function useCanvasMode() {
499
- return useSyncExternalStore(subscribe, getMode, getMode);
500
- }
501
- export function useCanvasPresentation() {
502
- return useSyncExternalStore(subscribe, getPresentation, getPresentation);
503
- }
504
- export function useCanvasPresentationTransitioning() {
505
- return useSyncExternalStore(subscribe, getPresentationTransitioning, getPresentationTransitioning);
506
- }
458
+ export { subscribe, getCards, getCards as getCanvasCards, };
507
459
  export function useCanvasCards() {
508
460
  return useSyncExternalStore(subscribe, getCards, getCards);
509
461
  }
510
- export function useCanvasComparisonGroups() {
511
- return useSyncExternalStore(subscribe, getComparisonGroups, getComparisonGroups);
462
+ function frameRoute(card) {
463
+ const sourceUrl = contentSourceUrl(card.content);
464
+ const normalized = normalizeUrl(sourceUrl);
465
+ if (!normalized)
466
+ return { key: sourceUrl, label: sourceUrl };
467
+ return {
468
+ key: `${normalized.origin}${normalized.pathname}`,
469
+ label: normalized.pathname,
470
+ };
512
471
  }
513
- export function useBoardCamera() {
514
- return useSyncExternalStore(subscribe, getBoardCamera, getBoardCamera);
472
+ /** Returns a route-based label that distinguishes copies and HTML iterations. */
473
+ export function getCanvasCardLabel(card, frameCards = cards) {
474
+ const route = frameRoute(card);
475
+ const candidates = frameCards.some((candidate) => candidate.id === card.id) ? frameCards : [...frameCards, card];
476
+ const matchingFrames = candidates.filter((candidate) => frameRoute(candidate).key === route.key);
477
+ const matchingKind = matchingFrames.filter((candidate) => candidate.content.kind === card.content.kind);
478
+ const number = matchingKind.findIndex((candidate) => candidate.id === card.id) + 1;
479
+ if (card.content.kind === "iteration")
480
+ return `${route.label} · Iteration ${number}`;
481
+ return number === 1 ? route.label : `${route.label} · Copy ${number}`;
515
482
  }