nudge-ui 0.2.2 → 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 (226) hide show
  1. package/README.md +164 -46
  2. package/dist/client.mjs +252 -61
  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 +38 -32
  7. package/dist/hosts/astro/integration.js +5 -1
  8. package/dist/hosts/astro/markdownIdentity.d.ts +28 -0
  9. package/dist/hosts/astro/markdownIdentity.js +81 -0
  10. package/dist/hosts/next/manifest.js +1 -1
  11. package/dist/hosts/next/sidecar.js +5 -1
  12. package/dist/hosts/static/manifest.js +1 -1
  13. package/dist/hosts/static/server.js +3 -0
  14. package/dist/hosts/vite/vite.d.ts +3 -0
  15. package/dist/hosts/vite/vite.js +24 -4
  16. package/dist/inspector/agent/McpConnectionDialog.js +13 -19
  17. package/dist/inspector/agent/activity.d.ts +18 -0
  18. package/dist/inspector/agent/activity.js +70 -0
  19. package/dist/inspector/agent/client.d.ts +2 -0
  20. package/dist/inspector/agent/client.js +29 -1
  21. package/dist/inspector/agent/connectionStatus.js +1 -1
  22. package/dist/inspector/agent/httpTransport.js +9 -2
  23. package/dist/inspector/agent/protocol.d.ts +6 -3
  24. package/dist/inspector/agent/protocol.js +1 -1
  25. package/dist/inspector/agent/verification.d.ts +11 -1
  26. package/dist/inspector/agent/verification.js +53 -25
  27. package/dist/inspector/canvas/AgentActivityOverlay.d.ts +13 -0
  28. package/dist/inspector/canvas/AgentActivityOverlay.js +111 -0
  29. package/dist/inspector/canvas/CanvasCard.d.ts +9 -2
  30. package/dist/inspector/canvas/CanvasCard.js +133 -38
  31. package/dist/inspector/canvas/CanvasElementOverlay.js +152 -16
  32. package/dist/inspector/canvas/CanvasToolIcons.d.ts +10 -0
  33. package/dist/inspector/canvas/CanvasToolIcons.js +20 -0
  34. package/dist/inspector/canvas/CanvasToolbar.js +8 -4
  35. package/dist/inspector/canvas/CanvasWorkspace.js +225 -57
  36. package/dist/inspector/canvas/StaleChangeIndicator.js +3 -13
  37. package/dist/inspector/canvas/agentPresentation.d.ts +8 -8
  38. package/dist/inspector/canvas/agentPresentation.js +64 -52
  39. package/dist/inspector/canvas/canvasGestures.d.ts +1 -1
  40. package/dist/inspector/canvas/canvasGestures.js +1 -1
  41. package/dist/inspector/canvas/canvasStore.d.ts +46 -82
  42. package/dist/inspector/canvas/canvasStore.js +251 -284
  43. package/dist/inspector/canvas/frameContent.d.ts +21 -0
  44. package/dist/inspector/canvas/frameContent.js +44 -0
  45. package/dist/inspector/canvas/frameProtocol.d.ts +21 -4
  46. package/dist/inspector/canvas/frameProtocol.js +3 -3
  47. package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -1
  48. package/dist/inspector/canvas/keyboardShortcuts.js +4 -4
  49. package/dist/inspector/canvas/layout.d.ts +52 -0
  50. package/dist/inspector/canvas/layout.js +106 -0
  51. package/dist/inspector/canvas/projection.d.ts +15 -5
  52. package/dist/inspector/canvas/projection.js +69 -13
  53. package/dist/inspector/canvas/rendererBootstrap.js +32 -4
  54. package/dist/inspector/canvas/rendererElementSelector.js +47 -6
  55. package/dist/inspector/canvas/rendererStylesheet.js +2 -2
  56. package/dist/inspector/canvas/resizeHandleScale.js +1 -1
  57. package/dist/inspector/canvas/sessionStore.d.ts +11 -26
  58. package/dist/inspector/canvas/sessionStore.js +43 -183
  59. package/dist/inspector/canvas/staleChangeDetector.js +9 -3
  60. package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
  61. package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
  62. package/dist/inspector/canvas/viewStore.d.ts +38 -0
  63. package/dist/inspector/canvas/viewStore.js +104 -0
  64. package/dist/inspector/canvas/wheelPan.d.ts +8 -0
  65. package/dist/inspector/canvas/wheelPan.js +8 -0
  66. package/dist/inspector/changes/changesLog.d.ts +6 -5
  67. package/dist/inspector/changes/changesLog.js +40 -32
  68. package/dist/inspector/changes/draftChanges.d.ts +56 -0
  69. package/dist/inspector/changes/draftChanges.js +300 -0
  70. package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
  71. package/dist/inspector/changes/previewDiagnostics.js +11 -25
  72. package/dist/inspector/clientManifest.d.ts +1 -1
  73. package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
  74. package/dist/inspector/comments/CommentsOverlay.js +250 -0
  75. package/dist/inspector/comments/element.d.ts +6 -0
  76. package/dist/inspector/comments/element.js +47 -0
  77. package/dist/inspector/comments/store.d.ts +29 -0
  78. package/dist/inspector/comments/store.js +121 -0
  79. package/dist/inspector/comments/verification.d.ts +5 -0
  80. package/dist/inspector/comments/verification.js +52 -0
  81. package/dist/inspector/drafts/model.d.ts +35 -0
  82. package/dist/inspector/drafts/model.js +45 -0
  83. package/dist/inspector/drafts/store.d.ts +28 -0
  84. package/dist/inspector/drafts/store.js +231 -0
  85. package/dist/inspector/index.js +20 -27
  86. package/dist/inspector/inline-text/inlineTextEditor.js +7 -0
  87. package/dist/inspector/inspection/useBrowserCssInspection.js +15 -1
  88. package/dist/inspector/iterations/capture.d.ts +2 -0
  89. package/dist/inspector/iterations/capture.js +173 -0
  90. package/dist/inspector/iterations/client.d.ts +7 -0
  91. package/dist/inspector/iterations/client.js +52 -0
  92. package/dist/inspector/iterations/coordinator.d.ts +15 -0
  93. package/dist/inspector/iterations/coordinator.js +130 -0
  94. package/dist/inspector/iterations/prompt.d.ts +3 -0
  95. package/dist/inspector/iterations/prompt.js +26 -0
  96. package/dist/inspector/overlay/DomNavigator.js +21 -15
  97. package/dist/inspector/overlay/interactionStyles.js +3 -1
  98. package/dist/inspector/overlay/spacingGestures.d.ts +13 -1
  99. package/dist/inspector/overlay/spacingGestures.js +82 -5
  100. package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
  101. package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
  102. package/dist/inspector/projection/renderedInstance.d.ts +2 -1
  103. package/dist/inspector/projection/renderedInstance.js +9 -5
  104. package/dist/inspector/projection/structuralProjection.d.ts +2 -1
  105. package/dist/inspector/projection/structuralProjection.js +19 -15
  106. package/dist/inspector/projection/textProjection.d.ts +2 -1
  107. package/dist/inspector/projection/textProjection.js +8 -4
  108. package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
  109. package/dist/inspector/prompt/clipboardHandoff.d.ts +10 -18
  110. package/dist/inspector/prompt/clipboardHandoff.js +92 -143
  111. package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
  112. package/dist/inspector/prompt/generatePrompt.js +17 -16
  113. package/dist/inspector/runtime/runtimeConfig.d.ts +9 -0
  114. package/dist/inspector/runtime/runtimeConfig.js +24 -0
  115. package/dist/inspector/settings/SettingsDialog.js +21 -8
  116. package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
  117. package/dist/inspector/shell/AgentStatusButton.js +36 -0
  118. package/dist/inspector/shell/AppShell.js +4 -2
  119. package/dist/inspector/shell/ChangesLog.js +46 -26
  120. package/dist/inspector/shell/CopyPromptButton.js +138 -89
  121. package/dist/inspector/shell/EmptyState.js +9 -8
  122. package/dist/inspector/shell/InspectorShell.js +13 -21
  123. package/dist/inspector/shell/PeekOriginalButton.js +24 -23
  124. package/dist/inspector/shell/originalPreview.js +5 -5
  125. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  126. package/dist/inspector/shell/shortcuts.js +11 -0
  127. package/dist/inspector/sketch/SketchEditor.js +14 -14
  128. package/dist/inspector/sketch/SketchEntryButton.js +1 -1
  129. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
  130. package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
  131. package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
  132. package/dist/inspector/sketch/SketchOverlay.js +7 -7
  133. package/dist/inspector/sketch/SketchWorkspace.js +18 -12
  134. package/dist/inspector/sketch/interaction.d.ts +1 -0
  135. package/dist/inspector/sketch/interaction.js +8 -4
  136. package/dist/inspector/sketch/model.d.ts +5 -0
  137. package/dist/inspector/sketch/model.js +6 -2
  138. package/dist/inspector/sketch/raster.d.ts +1 -2
  139. package/dist/inspector/sketch/raster.js +68 -4
  140. package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
  141. package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
  142. package/dist/inspector/styleEditors/BorderEditor.js +49 -78
  143. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
  144. package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
  145. package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
  146. package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
  147. package/dist/inspector/styleEditors/ColorPicker.js +10 -4
  148. package/dist/inspector/styleEditors/GapField.js +3 -5
  149. package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
  150. package/dist/inspector/styleEditors/GridChildSection.js +18 -9
  151. package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
  152. package/dist/inspector/styleEditors/GridPicker.js +8 -2
  153. package/dist/inspector/styleEditors/GridSection.js +1 -1
  154. package/dist/inspector/styleEditors/GridValueField.js +19 -14
  155. package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
  156. package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
  157. package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
  158. package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
  159. package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
  160. package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
  161. package/dist/inspector/styleEditors/LayoutSection.js +32 -11
  162. package/dist/inspector/styleEditors/SpacingBox.js +7 -5
  163. package/dist/inspector/styleEditors/Typography.js +11 -3
  164. package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
  165. package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
  166. package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
  167. package/dist/inspector/styleEditors/gradientValue.js +114 -0
  168. package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
  169. package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
  170. package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
  171. package/dist/inspector/styleEditors/layoutValue.js +0 -42
  172. package/dist/inspector/styleEditors/nudgeValue.d.ts +2 -2
  173. package/dist/inspector/styleEditors/nudgeValue.js +36 -8
  174. package/dist/inspector/tokens/TokenField.d.ts +7 -0
  175. package/dist/inspector/tokens/TokenField.js +29 -21
  176. package/dist/inspector/tokens/TokenLabel.js +5 -14
  177. package/dist/inspector/tokens/editActions.d.ts +1 -1
  178. package/dist/inspector/tokens/editActions.js +1 -1
  179. package/dist/inspector/ui/AtRuleContext.js +4 -6
  180. package/dist/inspector/ui/Button.d.ts +6 -2
  181. package/dist/inspector/ui/Button.js +4 -3
  182. package/dist/inspector/ui/ControlSurface.d.ts +2 -2
  183. package/dist/inspector/ui/ControlSurface.js +4 -3
  184. package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
  185. package/dist/inspector/ui/InlineStyleWarning.js +13 -0
  186. package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
  187. package/dist/inspector/ui/InspectorPopover.js +4 -5
  188. package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
  189. package/dist/inspector/ui/PopoverListbox.js +8 -7
  190. package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
  191. package/dist/inspector/ui/SegmentedControl.js +2 -2
  192. package/dist/inspector/ui/Select.d.ts +6 -1
  193. package/dist/inspector/ui/Select.js +7 -11
  194. package/dist/inspector/ui/SideValuesField.js +1 -1
  195. package/dist/inspector/ui/Tooltip.d.ts +26 -0
  196. package/dist/inspector/ui/Tooltip.js +32 -0
  197. package/dist/inspector/ui/classNames.d.ts +2 -0
  198. package/dist/inspector/ui/classNames.js +4 -0
  199. package/dist/inspector/ui/customCursors.d.ts +23 -0
  200. package/dist/inspector/ui/customCursors.js +122 -0
  201. package/dist/inspector/ui/dragCursor.d.ts +0 -2
  202. package/dist/inspector/ui/dragCursor.js +2 -10
  203. package/dist/inspector/ui/portalContainer.d.ts +2 -0
  204. package/dist/inspector/ui/portalContainer.js +6 -0
  205. package/dist/inspector/ui/styles.js +29 -21
  206. package/dist/inspector/workspace/commands.d.ts +9 -0
  207. package/dist/inspector/workspace/commands.js +82 -0
  208. package/dist/inspector/workspace/controller.d.ts +4 -0
  209. package/dist/inspector/workspace/controller.js +66 -0
  210. package/dist/inspector/workspace/handoff.d.ts +42 -0
  211. package/dist/inspector/workspace/handoff.js +115 -0
  212. package/dist/inspector/workspace/timeline.d.ts +30 -0
  213. package/dist/inspector/workspace/timeline.js +108 -0
  214. package/dist/internal/agent-protocol.d.ts +19 -2
  215. package/dist/internal/agent-protocol.js +13 -0
  216. package/dist/nudge-ui.mjs +558 -311
  217. package/dist/project/artifacts.d.ts +13 -0
  218. package/dist/project/artifacts.js +253 -0
  219. package/dist/transport/artifacts.d.ts +8 -0
  220. package/dist/transport/artifacts.js +9 -0
  221. package/dist/transport/editor.js +3 -1
  222. package/dist/transport/routes.d.ts +2 -0
  223. package/dist/transport/routes.js +2 -0
  224. package/package.json +6 -2
  225. package/dist/inspector/changes/workspaceChanges.d.ts +0 -47
  226. package/dist/inspector/changes/workspaceChanges.js +0 -265
@@ -1,11 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { AgentActivityOverlay } from "./AgentActivityOverlay.js";
3
+ import { contentDocumentUrl, contentNavigationUrl, contentSourceUrl, iterationId } from "./frameContent.js";
4
+ import { refreshHtmlIteration } from "../iterations/coordinator.js";
2
5
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
6
  import { CANVAS_RENDERER_ATTR } from "./roleDetection.js";
4
- import { removeCanvasCard, updateCardTitle, updateCardUrl, resizeCard, setCardPosition, selectCard, getSelectedCardId, useSelectedCardId, useFocusedCardId, useBoardCamera } from "./canvasStore.js";
5
- import { IconRefresh, IconArrowsDiagonal, IconCornerLeftDown } from "@tabler/icons-react";
7
+ import { useCanvasCards, getCanvasCardLabel, updateCardUrl, resizeCard, setCardPosition, selectCard, getSelectedCardId, useSelectedCardId, useFocusedCardId, } from "./canvasStore.js";
8
+ import { useBoardCamera } from "./viewStore.js";
9
+ import { IconArrowsDiagonal, IconCopy, IconCornerLeftDown, IconBoltFilled, IconArtboard, IconTrash } from "@tabler/icons-react";
6
10
  import { PROTOCOL_VERSION, } from "./frameProtocol.js";
7
11
  import { registerCardFrame, registerCardFrameSource, unregisterCardFrame, sendProjectionToCard, invalidateCanvasPreviewDocument, PROJECT_ID, WORKSPACE_ID } from "./projection.js";
8
12
  import { Button } from "../ui/Button.js";
13
+ import { IconButton } from "../ui/IconButton.js";
14
+ import { Tooltip } from "../ui/Tooltip.js";
9
15
  import { setSelectedElement } from "../selection/selectionStore.js";
10
16
  import { clearCanvasStructuralProjectionReports } from "../projection/structuralProjection.js";
11
17
  import { clearCanvasRenderedInstanceProjectionReports } from "../projection/renderedInstance.js";
@@ -15,12 +21,16 @@ import { getCanvasResizeHandleScale } from "./resizeHandleScale.js";
15
21
  import { disposeBrowserCssInspection } from "../inspection/browserCssInspectionRegistry.js";
16
22
  import { releaseDocumentProjection } from "../projection/structuralProjection.js";
17
23
  import { startClipboardHandoffController } from "../prompt/clipboardHandoff.js";
18
- import { disposeInlineTextEdit } from "../inline-text/inlineTextEditor.js";
24
+ import { disposeInlineTextEdit, isInlineTextEditingActive } from "../inline-text/inlineTextEditor.js";
19
25
  import { configureNudgeUiRuntime, getNudgeUiRuntimeConfig } from "../runtime/runtimeConfig.js";
26
+ import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
20
27
  import { reconcileRuntimeWithDocumentStylesheets } from "../runtime/documentStylesheetOrder.js";
21
28
  import { subscribeCanvasRendererMessages } from "./rendererMessageRouter.js";
22
29
  import { SketchFrameOverlay } from "../sketch/SketchFrameOverlay.js";
23
30
  import { getCanvasResizeEdges, resizeCanvasRect, } from "./canvasResize.js";
31
+ import { getDraftContentsForCard } from "../drafts/store.js";
32
+ import { removeFrame } from "../workspace/commands.js";
33
+ import { artifactDocumentUrl } from "../iterations/client.js";
24
34
  const RESIZE_HANDLES = [
25
35
  { direction: "top-left", cursor: "nwse-resize", transformOrigin: "left top", label: "Resize card from the top-left corner" },
26
36
  { direction: "top", cursor: "ns-resize", transformOrigin: "center top", label: "Resize card from the top edge" },
@@ -38,15 +48,23 @@ function resizeHandleTransform(direction, scale) {
38
48
  return `scale(${scale}, 1)`;
39
49
  return `scale(${scale})`;
40
50
  }
41
- export function CanvasCard({ card, presentation = "canvas", presentationCard = true, onShowFocus, documentOwner }) {
51
+ export function CanvasCard({ card, presentation = "canvas", presentationCard = true, linkedToActive = false, onShowFocus, onDuplicate, onCreateIteration, documentOwner }) {
42
52
  const iframeRef = useRef(null);
43
- const initialUrlRef = useRef(card.navigationUrl ?? card.url);
44
- const navigationUrlRef = useRef(card.navigationUrl);
53
+ const initialUrlRef = useRef(contentNavigationUrl(card.content) ?? contentSourceUrl(card.content));
54
+ const navigationUrlRef = useRef(contentNavigationUrl(card.content));
45
55
  const documentSessionRef = useRef(null);
46
56
  const frameRuntimeRef = useRef(null);
47
57
  const [loadState, setLoadState] = useState("loading");
48
58
  const [errorMessage, setErrorMessage] = useState(null);
59
+ const [creatingIteration, setCreatingIteration] = useState(false);
60
+ const [iterationError, setIterationError] = useState(null);
49
61
  const camera = useBoardCamera();
62
+ const runtimeConfig = useNudgeUiRuntimeConfig();
63
+ const canvasCards = useCanvasCards();
64
+ const canRemove = canvasCards.length > 1;
65
+ const frameLabel = runtimeConfig.demo === true && runtimeConfig.demoCardLabels?.length && card.title
66
+ ? card.title
67
+ : getCanvasCardLabel(card, canvasCards);
50
68
  const selectedCardId = useSelectedCardId();
51
69
  const focusedCardId = useFocusedCardId();
52
70
  const isSelected = selectedCardId === card.id;
@@ -94,22 +112,59 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
94
112
  disposeDocumentSession();
95
113
  unregisterCardFrame(card.id);
96
114
  };
97
- }, [bindDocumentSession, card.id, disposeDocumentSession]);
115
+ }, [bindDocumentSession, card.id, iterationId(card.content), disposeDocumentSession]);
116
+ useEffect(() => {
117
+ if (card.content.kind !== "iteration" || loadState !== "ready")
118
+ return;
119
+ const artifactId = card.content.artifactId;
120
+ let active = true;
121
+ let checking = false;
122
+ let reloadPending = false;
123
+ const timer = window.setInterval(() => {
124
+ const workspace = getDraftContentsForCard(card.id);
125
+ // Pending inspector edits still project over preview.html. Do not replace
126
+ // that base until they have been saved and handed off.
127
+ if (checking || isInlineTextEditingActive() || workspace?.changes.length || workspace?.structuralChanges.length)
128
+ return;
129
+ checking = true;
130
+ const frame = iframeRef.current;
131
+ if (!frame) {
132
+ checking = false;
133
+ return;
134
+ }
135
+ void refreshHtmlIteration(card.id, artifactId, frame).then((changed) => {
136
+ reloadPending ||= changed;
137
+ const latest = getDraftContentsForCard(card.id);
138
+ if (active && reloadPending && !isInlineTextEditingActive() && !latest?.changes.length && !latest?.structuralChanges.length) {
139
+ reloadPending = false;
140
+ loadFrame(`${artifactDocumentUrl(artifactId)}?revision=${Date.now()}`);
141
+ }
142
+ }).catch(() => undefined).finally(() => { checking = false; });
143
+ }, 1500);
144
+ return () => { active = false; window.clearInterval(timer); };
145
+ }, [iterationId(card.content), card.id, loadFrame, loadState]);
98
146
  function handleReload() {
99
147
  loadFrame();
100
148
  }
101
149
  function handleRemove() {
102
150
  if (getSelectedCardId() === card.id)
103
151
  setSelectedElement(null);
104
- removeCanvasCard(card.id);
152
+ removeFrame(card.id);
105
153
  }
106
- function updateFrameTitle(title) {
107
- if (!title)
108
- return;
109
- const runtime = getNudgeUiRuntimeConfig();
110
- if (runtime.demo === true && (runtime.demoCardLabels?.length ?? 0) > 0)
154
+ async function handleIteration(position) {
155
+ if (creatingIteration || !onCreateIteration || loadState !== "ready")
111
156
  return;
112
- updateCardTitle(card.id, title);
157
+ setCreatingIteration(true);
158
+ setIterationError(null);
159
+ try {
160
+ return await onCreateIteration(card.id, position);
161
+ }
162
+ catch (error) {
163
+ setIterationError(error instanceof Error ? error.message : "The iteration could not be created.");
164
+ }
165
+ finally {
166
+ setCreatingIteration(false);
167
+ }
113
168
  }
114
169
  /** The controller half of the handshake: announce workspace identity. */
115
170
  function sendParentReady() {
@@ -158,8 +213,7 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
158
213
  adoptFrameRuntime(data.runtime);
159
214
  setLoadState("ready");
160
215
  setErrorMessage(null);
161
- updateFrameTitle(data.title);
162
- if (data.url)
216
+ if (data.url && !iterationId(card.content))
163
217
  updateCardUrl(card.id, data.url);
164
218
  if (iframeRef.current) {
165
219
  registerCardFrame(card.id, iframeRef.current);
@@ -172,8 +226,7 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
172
226
  return;
173
227
  }
174
228
  if (data.type === "frame-metadata") {
175
- updateFrameTitle(data.title);
176
- if (data.url)
229
+ if (data.url && !iterationId(card.content))
177
230
  updateCardUrl(card.id, data.url);
178
231
  return;
179
232
  }
@@ -182,13 +235,13 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
182
235
  setErrorMessage(data.message || "Frame failed to load");
183
236
  }
184
237
  });
185
- }, [card.id]);
238
+ }, [card.id, iterationId(card.content)]);
186
239
  useLayoutEffect(() => {
187
- if (!card.navigationUrl || navigationUrlRef.current === card.navigationUrl)
240
+ if (!contentNavigationUrl(card.content) || navigationUrlRef.current === contentNavigationUrl(card.content))
188
241
  return;
189
- navigationUrlRef.current = card.navigationUrl;
190
- loadFrame(card.navigationUrl);
191
- }, [card.navigationUrl, loadFrame]);
242
+ navigationUrlRef.current = contentNavigationUrl(card.content);
243
+ loadFrame(contentNavigationUrl(card.content));
244
+ }, [contentNavigationUrl(card.content), loadFrame]);
192
245
  useEffect(() => {
193
246
  if (loadState !== "loading")
194
247
  return;
@@ -220,12 +273,13 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
220
273
  }
221
274
  iframe.addEventListener("load", onLoad);
222
275
  return () => iframe.removeEventListener("load", onLoad);
223
- }, [bindDocumentSession, card.id, disposeDocumentSession]);
276
+ }, [bindDocumentSession, card.id, iterationId(card.content), disposeDocumentSession]);
224
277
  const [isDragging, setIsDragging] = useState(false);
225
278
  const dragRef = useRef({ startX: 0, startY: 0, cardX: 0, cardY: 0 });
279
+ const dragCleanupRef = useRef(null);
226
280
  const resizeCleanupRef = useRef(null);
227
281
  useEffect(() => {
228
- return () => resizeCleanupRef.current?.();
282
+ return () => { resizeCleanupRef.current?.(); dragCleanupRef.current?.(); };
229
283
  }, []);
230
284
  const handleToolbarPointerDown = useCallback((e) => {
231
285
  // SAFETY: pointer events on the card target are HTMLElements in this DOM context.
@@ -237,6 +291,12 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
237
291
  selectCard(card.id);
238
292
  e.stopPropagation();
239
293
  e.preventDefault();
294
+ const iterationDrag = e.altKey && !!onCreateIteration && loadState === "ready" && !creatingIteration;
295
+ let moved = false;
296
+ let iterationId;
297
+ let creationStarted = false;
298
+ let cancelled = false;
299
+ let position = { x: card.x, y: card.y };
240
300
  dragRef.current = {
241
301
  startX: e.clientX,
242
302
  startY: e.clientY,
@@ -249,16 +309,56 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
249
309
  function onMove(ev) {
250
310
  const dx = (ev.clientX - dragRef.current.startX) / camera.zoom;
251
311
  const dy = (ev.clientY - dragRef.current.startY) / camera.zoom;
252
- setCardPosition(card.id, dragRef.current.cardX + dx, dragRef.current.cardY + dy);
312
+ moved ||= Math.hypot(ev.clientX - dragRef.current.startX, ev.clientY - dragRef.current.startY) >= 6;
313
+ position = { x: dragRef.current.cardX + dx, y: dragRef.current.cardY + dy };
314
+ if (iterationDrag) {
315
+ if (iterationId)
316
+ setCardPosition(iterationId, position.x, position.y);
317
+ else if (moved && !creationStarted) {
318
+ creationStarted = true;
319
+ void handleIteration(position).then((id) => {
320
+ if (!id)
321
+ return;
322
+ iterationId = id;
323
+ if (cancelled) {
324
+ removeFrame(id, { undoable: false });
325
+ selectCard(card.id);
326
+ }
327
+ else
328
+ setCardPosition(id, position.x, position.y);
329
+ });
330
+ }
331
+ }
332
+ else
333
+ setCardPosition(card.id, position.x, position.y);
253
334
  }
254
- function onUp() {
255
- setIsDragging(false);
335
+ function cleanup() {
256
336
  window.removeEventListener("pointermove", onMove);
257
337
  window.removeEventListener("pointerup", onUp);
338
+ window.removeEventListener("pointercancel", onCancel);
339
+ dragCleanupRef.current = null;
258
340
  }
341
+ function onCancel() {
342
+ cleanup();
343
+ setIsDragging(false);
344
+ cancelled = true;
345
+ if (iterationId) {
346
+ removeFrame(iterationId, { undoable: false });
347
+ selectCard(card.id);
348
+ }
349
+ }
350
+ function onUp() {
351
+ cleanup();
352
+ setIsDragging(false);
353
+ if (iterationId)
354
+ setCardPosition(iterationId, position.x, position.y);
355
+ }
356
+ dragCleanupRef.current?.();
357
+ dragCleanupRef.current = onCancel;
259
358
  window.addEventListener("pointermove", onMove);
260
359
  window.addEventListener("pointerup", onUp);
261
- }, [card.id, card.x, card.y, camera.zoom]);
360
+ window.addEventListener("pointercancel", onCancel);
361
+ }, [card.id, card.x, card.y, camera.zoom, onCreateIteration, loadState, creatingIteration]);
262
362
  const handleCardPointerDown = useCallback(() => {
263
363
  if (getSelectedCardId() !== card.id)
264
364
  setSelectedElement(null);
@@ -341,7 +441,8 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
341
441
  const next = resizeCanvasRect(card, direction, delta);
342
442
  resizeCard(card.id, next.width, next.height, { x: next.x, y: next.y });
343
443
  }
344
- return (_jsxs("div", { className: `canvas-card${isDragging ? " is-dragging" : ""}${isSelected ? " is-selected" : ""}${isFocused ? " is-focused" : ""}${isFocusPresentationCard ? " is-focus-presentation" : ""}${presentationCard ? " is-presentation-card" : ""}`, "data-card-id": card.id, style: {
444
+ return (_jsxs("div", { className: `canvas-card${linkedToActive ? " is-linked" : ""}${isDragging ? " is-dragging" : ""}${isSelected ? " is-selected" : ""}${isFocused ? " is-focused" : ""}${isFocusPresentationCard ? " is-focus-presentation" : ""}${presentationCard ? " is-presentation-card" : ""}`, "data-card-id": card.id, tabIndex: presentation === "canvas" ? 0 : undefined, role: "group", "aria-label": `Frame: ${frameLabel}`, style: {
445
+ "--canvas-toolbar-scale": toolbarScale,
345
446
  position: "absolute",
346
447
  visibility: presentation === "focus" && !presentationCard ? "hidden" : undefined,
347
448
  pointerEvents: presentation === "focus" && !presentationCard ? "none" : undefined,
@@ -349,13 +450,7 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
349
450
  top: isFocusPresentationCard ? 0 : card.y,
350
451
  width: isFocusPresentationCard ? "100%" : card.width,
351
452
  height: isFocusPresentationCard ? "100%" : card.height,
352
- }, onPointerDown: presentation === "canvas" ? handleCardPointerDown : undefined, children: [presentation === "canvas" ? _jsx("div", { className: "canvas-card__toolbar", onPointerDown: handleToolbarPointerDown, children: _jsxs("div", { className: "canvas-card__drag-surface", "data-test": `canvas-card-drag-${card.id}`, children: [onShowFocus ? (_jsxs(Button, { title: "Focus", variant: "primary", size: "default", "data-test": `canvas-card-focus-${card.id}`, style: {
353
- transform: `scale(${toolbarScale})`,
354
- transformOrigin: "left bottom",
355
- }, onClick: () => onShowFocus(card.id), children: [_jsx(IconCornerLeftDown, { size: "var(--icon-size-small)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), "Focus"] })) : null, _jsx("span", { className: "canvas-card__dimensions", "data-test": `canvas-card-dimensions-${card.id}`, style: {
356
- transform: `translateX(-50%) scale(${toolbarScale})`,
357
- transformOrigin: "center bottom",
358
- }, children: card.title || `${Math.round(card.width)} × ${Math.round(card.height)} px` })] }) }) : null, _jsxs("div", { className: "canvas-card__frame", children: [loadState === "loading" ? (_jsx("div", { className: "canvas-card__loading", "data-test": `canvas-card-loading-${card.id}`, children: "Loading preview..." })) : loadState === "error" ? (_jsxs("div", { className: "canvas-card__error", "data-test": `canvas-card-error-${card.id}`, children: [_jsx("p", { children: errorMessage || "Frame could not be loaded" }), _jsxs("div", { className: "canvas-card__error-actions", children: [_jsx(Button, { size: "compact", variant: "secondary", onClick: handleReload, children: "Retry" }), _jsx(Button, { size: "compact", variant: "danger", onClick: handleRemove, children: "Remove" })] })] })) : null, _jsx("iframe", { ref: iframeRef, className: "canvas-card__iframe", src: initialUrlRef.current, title: card.title || card.url, [CANVAS_RENDERER_ATTR]: "", sandbox: "allow-scripts allow-same-origin allow-forms allow-popups", "data-test": `canvas-card-iframe-${card.id}` }), _jsx(SketchFrameOverlay, { iframe: iframeRef.current, cardUrl: card.url, ready: loadState === "ready" })] }), presentation === "canvas" ? RESIZE_HANDLES.map((handle) => (_jsx("div", { className: `canvas-card__resize-handle canvas-card__resize-handle--${handle.direction}`, "data-test": `canvas-card-resize-${card.id}${handle.direction === "bottom-right" ? "" : `-${handle.direction}`}`, "data-resize-direction": handle.direction, style: {
453
+ }, onPointerDown: presentation === "canvas" ? handleCardPointerDown : undefined, children: [presentation === "canvas" ? _jsx(_Fragment, { children: _jsxs("div", { className: "canvas-card__header", style: { bottom: "100%", minHeight: `calc(var(--control-height-large) * ${toolbarScale})` }, children: [_jsxs("div", { className: "canvas-card__drag-surface", style: { minHeight: `calc(var(--control-height-large) * ${toolbarScale})` }, "data-test": `canvas-card-drag-${card.id}`, onPointerDown: handleToolbarPointerDown, children: [_jsx("div", { className: "canvas-card__identity", style: { zoom: toolbarScale }, children: _jsxs("span", { className: "canvas-card__dimensions", "data-test": `canvas-card-dimensions-${card.id}`, children: [!iterationId(card.content) ? (_jsx(Tooltip, { content: "Live view", children: _jsx("span", { className: "canvas-card__live-badge", "aria-label": "Live view", "data-test": `canvas-card-live-${card.id}`, children: _jsx(IconBoltFilled, { size: 12, "aria-hidden": "true" }) }) })) : (_jsx(Tooltip, { content: "HTML only", children: _jsx("span", { className: "canvas-card__iteration-badge", "aria-label": "HTML only", "data-test": `canvas-card-iteration-badge-${card.id}`, children: _jsx(IconArtboard, { size: 14, "aria-hidden": "true" }) }) })), _jsx("span", { className: "canvas-card__title", children: frameLabel })] }) }), iterationError ? _jsx("span", { className: "canvas-card__iteration-error", role: "status", children: iterationError }) : null] }), _jsx("div", { className: "canvas-card__floating-toolbar", "data-busy": creatingIteration ? "true" : undefined, onPointerDown: (event) => event.stopPropagation(), children: _jsxs("div", { className: "canvas-toolbar canvas-card__actions", style: { zoom: toolbarScale }, role: "toolbar", "aria-label": "Frame actions", "aria-busy": creatingIteration, "data-test": `canvas-card-actions-${card.id}`, children: [onShowFocus ? (_jsx(Tooltip, { content: "Open this frame in focus view", children: _jsxs(Button, { variant: "quiet", size: "default", "data-test": `canvas-card-focus-${card.id}`, onClick: () => onShowFocus(card.id), children: [_jsx(IconCornerLeftDown, { size: "var(--icon-size-small)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), "Focus"] }) })) : null, onDuplicate && !iterationId(card.content) ? (_jsx(Tooltip, { content: "Create a linked copy of this frame", children: _jsxs(Button, { variant: "quiet", size: "default", "data-test": `canvas-card-duplicate-${card.id}`, onClick: () => onDuplicate(card.id), children: [_jsx(IconCopy, { size: "var(--icon-size-small)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), "Duplicate"] }) })) : null, onCreateIteration ? (_jsx(Tooltip, { content: "Unlinked HTML version", stableTrigger: true, children: _jsxs(Button, { variant: "quiet", size: "default", "data-test": `canvas-card-create-iteration-${card.id}`, disabled: creatingIteration || loadState !== "ready", onClick: () => void handleIteration(), children: [_jsx(IconArtboard, { size: "var(--icon-size-small)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), creatingIteration ? "Creating…" : "Iteration"] }) })) : null, _jsx("div", { className: "canvas-toolbar__divider", "aria-hidden": "true" }), _jsx(Tooltip, { content: canRemove ? "Delete frame" : "Keep at least one frame on the canvas", children: _jsx(IconButton, { variant: "quiet", size: "default", label: "Delete frame", "data-test": `canvas-card-delete-${card.id}`, disabled: !canRemove, onClick: handleRemove, children: _jsx(IconTrash, { size: "var(--icon-size-small)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }) }) })] }) })] }) }) : null, _jsxs("div", { className: "canvas-card__frame", children: [loadState === "loading" ? (_jsx("div", { className: "canvas-card__loading", "data-test": `canvas-card-loading-${card.id}`, children: "Loading preview..." })) : loadState === "error" ? (_jsxs("div", { className: "canvas-card__error", "data-test": `canvas-card-error-${card.id}`, children: [_jsx("p", { children: errorMessage || "Frame could not be loaded" }), _jsxs("div", { className: "canvas-card__error-actions", children: [_jsx(Button, { size: "compact", variant: "secondary", onClick: handleReload, children: "Retry" }), _jsx(Button, { size: "compact", variant: "danger", onClick: handleRemove, children: "Remove" })] })] })) : null, _jsx("iframe", { ref: iframeRef, className: "canvas-card__iframe", src: card.content.kind === "iteration" ? contentDocumentUrl(card.content) : initialUrlRef.current, title: frameLabel, [CANVAS_RENDERER_ATTR]: "", sandbox: "allow-scripts allow-same-origin allow-forms allow-popups", "data-test": `canvas-card-iframe-${card.id}` }, iterationId(card.content) ?? "live"), _jsx(AgentActivityOverlay, { cardId: card.id, content: card.content, iframe: loadState === "ready" ? iframeRef.current : null }), _jsx(SketchFrameOverlay, { cardId: card.id, iframe: iframeRef.current, cardUrl: contentSourceUrl(card.content), artifactId: iterationId(card.content), ready: loadState === "ready" })] }), presentation === "canvas" ? RESIZE_HANDLES.map((handle) => (_jsx("div", { className: `canvas-card__resize-handle canvas-card__resize-handle--${handle.direction}`, "data-test": `canvas-card-resize-${card.id}${handle.direction === "bottom-right" ? "" : `-${handle.direction}`}`, "data-resize-direction": handle.direction, style: {
359
454
  // Keep the edge's long axis aligned with the zoomed card. Only
360
455
  // the thickness needs inverse scaling to remain screen-sized.
361
456
  transform: resizeHandleTransform(handle.direction, resizeHandleScale),
@@ -1,18 +1,21 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
3
3
  import { PROTOCOL_VERSION, isElementClickMessage, isInlineTextIntentMessage, } from "./frameProtocol.js";
4
- import { useBoardCamera, useCanvasCards, useCanvasPresentation, useCanvasPresentationTransitioning } from "./canvasStore.js";
4
+ import { useCanvasCards } from "./canvasStore.js";
5
+ import { useBoardCamera, useCanvasPresentation, useCanvasPresentationTransitioning, useCanvasLayoutTransitioning, getCanvasLayoutTransitioning, useTemporaryAppInteraction, } from "./viewStore.js";
5
6
  import { handleElementClick } from "./rendererSelectionProxy.js";
6
7
  import { getSelectedElements, useHierarchy, useSelectedElement, useSelectedElements } from "../selection/selectionStore.js";
7
8
  import { readBorderWidths, toRect, } from "../overlay/overlayGeometry.js";
8
- const overlayStyles = ".canvas-element-overlay {\n position: fixed;\n pointer-events: none;\n z-index: 1;\n background: transparent;\n outline: 1px solid var(--accent);\n outline-offset: -1px;\n}\n\n.canvas-spacing-guide {\n position: fixed;\n z-index: 1;\n border-radius: 999px;\n background: var(--accent);\n outline: none;\n box-shadow: none;\n pointer-events: none;\n}\n\n.canvas-spacing-fill {\n position: fixed;\n z-index: 1;\n pointer-events: none;\n background: rgb(59 130 246 / 14%);\n}\n\n.canvas-spacing-fill[data-kind=\"gap\"] {\n background: rgb(236 72 153 / 14%);\n}\n\n.canvas-spacing-guide[data-kind=\"gap\"] {\n background: #ec4899;\n}\n\n.canvas-selected-outline {\n position: fixed;\n pointer-events: none;\n z-index: 1;\n outline: 1px solid var(--accent);\n outline-offset: -1px;\n}\n\n.canvas-dom-drop-line {\n z-index: 2147483647;\n border-radius: 999px;\n background: #3b82f6;\n box-shadow: 0 0 0 1px rgb(255 255 255 / 95%), 0 1px 5px rgb(37 99 235 / 72%);\n}\n\n.canvas-dom-drop-line::before,\n.canvas-dom-drop-line::after {\n position: absolute;\n top: 50%;\n width: 8px;\n height: 8px;\n border: 2px solid #ffffff;\n border-radius: 50%;\n background: #2563eb;\n content: \"\";\n transform: translateY(-50%);\n}\n\n.canvas-dom-drop-line::before { left: -3px; }\n.canvas-dom-drop-line::after { right: -3px; }\n\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::before,\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::after {\n left: 50%;\n right: auto;\n transform: translateX(-50%);\n}\n\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::before { top: -3px; }\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::after { top: auto; bottom: -3px; }\n\n.canvas-dom-drop-target {\n z-index: 2147483646;\n box-sizing: border-box;\n border: 1px solid rgb(59 130 246 / 72%);\n border-radius: 4px;\n background: rgb(59 130 246 / 5%);\n}\n";
9
+ const overlayStyles = ".canvas-element-overlay {\n position: fixed;\n pointer-events: none;\n z-index: 1;\n background: transparent;\n outline: 1px solid var(--accent);\n outline-offset: -1px;\n}\n\n.canvas-spacing-guide {\n position: fixed;\n z-index: 1;\n border-radius: 999px;\n background: var(--accent);\n outline: none;\n box-shadow: none;\n pointer-events: none;\n}\n\n.canvas-spacing-fill {\n position: fixed;\n z-index: 1;\n pointer-events: none;\n background: rgb(59 130 246 / 14%);\n}\n\n.canvas-spacing-fill[data-kind=\"gap\"] {\n background: rgb(236 72 153 / 14%);\n}\n\n.canvas-spacing-guide[data-kind=\"gap\"] {\n background: #ec4899;\n}\n\n.canvas-spacing-value-badge {\n position: fixed;\n z-index: 2;\n transform: translate(-50%, calc(-100% - var(--space-8)));\n padding: var(--space-4) var(--space-8);\n border-radius: var(--radius-8);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n color: var(--text-primary);\n font-family: var(--font-family);\n font-size: var(--font-size-base);\n font-weight: 400;\n line-height: 1.4;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n pointer-events: none;\n}\n\n.canvas-selected-outline {\n position: fixed;\n pointer-events: none;\n z-index: 1;\n outline: 1px solid var(--accent);\n outline-offset: -1px;\n}\n\n.canvas-dom-drop-line {\n z-index: 2147483647;\n border-radius: 999px;\n background: #3b82f6;\n box-shadow: 0 0 0 1px rgb(255 255 255 / 95%), 0 1px 5px rgb(37 99 235 / 72%);\n}\n\n.canvas-dom-drop-line::before,\n.canvas-dom-drop-line::after {\n position: absolute;\n top: 50%;\n width: 8px;\n height: 8px;\n border: 2px solid #ffffff;\n border-radius: 50%;\n background: #2563eb;\n content: \"\";\n transform: translateY(-50%);\n}\n\n.canvas-dom-drop-line::before { left: -3px; }\n.canvas-dom-drop-line::after { right: -3px; }\n\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::before,\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::after {\n left: 50%;\n right: auto;\n transform: translateX(-50%);\n}\n\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::before { top: -3px; }\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::after { top: auto; bottom: -3px; }\n\n.canvas-dom-drop-target {\n z-index: 2147483646;\n box-sizing: border-box;\n border: 1px solid rgb(59 130 246 / 72%);\n border-radius: 4px;\n background: rgb(59 130 246 / 5%);\n}\n";
9
10
  import { deleteElement, getDropLocationAtPoint, moveElement, nudgeElement } from "../overlay/structuralGestures.js";
10
- import { redo, undo } from "../changes/changesLog.js";
11
+ import { changeKey } from "../changes/model.js";
12
+ import { getChanges, redo, subscribeChanges, undo } from "../changes/changesLog.js";
11
13
  import { resolveSelectionFromElement } from "../selection/resolveSelection.js";
12
14
  import { setSelectedElement } from "../selection/selectionStore.js";
13
15
  import { clearDropGuide, showDropGuide, useDropGuide } from "../overlay/dropGuide.js";
14
16
  import { DropGuideOverlay } from "../overlay/DropGuideOverlay.js";
15
17
  import { getMeasurementGeometry } from "../overlay/measurementGeometry.js";
18
+ import { getElementComputedStyle } from "../runtime/domRealm.js";
16
19
  import { MeasurementGuideOverlay } from "../overlay/MeasurementGuideOverlay.js";
17
20
  import { projectMeasurementSegments } from "./measurementProjection.js";
18
21
  import { getRegisteredFrames, PROJECT_ID, WORKSPACE_ID } from "./projection.js";
@@ -23,7 +26,7 @@ import { setInspectorOpen, toggleInspector } from "../shell/openStore.js";
23
26
  import { EMPTY_TEXT_PROJECTION_ATTR } from "../projection/textProjection.js";
24
27
  import { subscribeCanvasRendererMessages } from "./rendererMessageRouter.js";
25
28
  import { setStyles } from "../tokens/editActions.js";
26
- import { getSpacingAffordanceAtPoint, getSpacingAffordanceForDescriptor, SPACING_GUIDE_HANDLE_LENGTH, spacingValueCss, spacingValueForDrag, } from "../overlay/spacingGestures.js";
29
+ import { getSpacingAffordanceAtPoint, getSpacingAffordanceForDescriptor, getSpacingHoverAtPoint, SPACING_GUIDE_HANDLE_LENGTH, spacingValueCss, spacingValueForDrag, } from "../overlay/spacingGestures.js";
27
30
  import { beginLayoutPreview, endLayoutPreview } from "../styleEditors/layoutPreviewState.js";
28
31
  import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
29
32
  import { DomNavigator } from "../overlay/DomNavigator.js";
@@ -36,6 +39,20 @@ function projectRect(iframe, rect, zoom) {
36
39
  height: rect.height * zoom,
37
40
  };
38
41
  }
42
+ function projectPoint(iframe, x, y, zoom) {
43
+ const frameRect = iframe.getBoundingClientRect();
44
+ return {
45
+ x: frameRect.left + x * zoom,
46
+ y: frameRect.top + y * zoom,
47
+ };
48
+ }
49
+ function spacingBadgeForDrag(drag, point) {
50
+ return {
51
+ iframe: drag.iframe,
52
+ point: { x: point.x, y: point.y },
53
+ value: drag.currentValue,
54
+ };
55
+ }
39
56
  function overlayStyle(rect) {
40
57
  return {
41
58
  position: "fixed",
@@ -121,19 +138,95 @@ function restoreSpacingPreview(drag) {
121
138
  }
122
139
  endLayoutPreview(drag.element, drag.affordance.property);
123
140
  }
141
+ /**
142
+ * Commits a managed rule for the drag's final value. Returns the created change
143
+ * records (empty for a no-op drag). The preview inline is intentionally left
144
+ * applied at the committed value once a record exists; the caller must register
145
+ * a PendingSpacingCommit and release it after the renderer acknowledges the
146
+ * projected sheet. Restoring the authored declaration first would flash the old
147
+ * value for the postMessage round trip the sheet takes.
148
+ */
124
149
  function commitSpacingDrag(drag) {
125
- // Restore the authored declaration before committing the managed rule. The
126
- // captured start value is the baseline because the pointer remains hovered
127
- // and CSS inspection can still report the transient preview value.
128
- restoreSpacingPreview(drag);
129
- if (drag.currentValue === drag.startValue)
130
- return;
131
- setStyles(drag.element, [{
150
+ if (drag.currentValue === drag.startValue) {
151
+ restoreSpacingPreview(drag);
152
+ return [];
153
+ }
154
+ return setStyles(drag.element, [{
132
155
  property: drag.affordance.property,
133
156
  value: spacingValueCss(drag.currentValue),
134
157
  oldRawValue: spacingValueCss(drag.startValue),
135
158
  }]);
136
159
  }
160
+ /** Removes a deferred preview inline as soon as the projected sheet alone
161
+ * reproduces the committed value; keeps it otherwise and retries on the next
162
+ * projection acknowledgement from that frame. */
163
+ function tryReleasePendingSpacingCommit(commit) {
164
+ const { element, property, committedValue } = commit;
165
+ if (!element.isConnected)
166
+ return true;
167
+ element.style.removeProperty(property);
168
+ const computed = getElementComputedStyle(element).getPropertyValue(property).trim();
169
+ if (computed === committedValue) {
170
+ endLayoutPreview(element, property);
171
+ return true;
172
+ }
173
+ element.style.setProperty(property, committedValue);
174
+ return false;
175
+ }
176
+ function releasePendingSpacingCommits(pending, iframe) {
177
+ if (pending.length === 0)
178
+ return;
179
+ let write = 0;
180
+ for (const commit of pending) {
181
+ if (commit.iframe !== iframe || !tryReleasePendingSpacingCommit(commit)) {
182
+ pending[write] = commit;
183
+ write += 1;
184
+ }
185
+ }
186
+ pending.length = write;
187
+ }
188
+ /** Restores any held preview inline whose committed rule was undone, reverted,
189
+ * or cleared before its sheet landed. Runs synchronously with the workspace
190
+ * change, so the inline never outlives the intent behind it. */
191
+ function reconcilePendingSpacingCommits(pending) {
192
+ if (pending.length === 0)
193
+ return;
194
+ const survivingKeys = new Set(getChanges().map(changeKey));
195
+ let write = 0;
196
+ for (const commit of pending) {
197
+ if (commit.keys.some((key) => survivingKeys.has(key))) {
198
+ pending[write] = commit;
199
+ write += 1;
200
+ continue;
201
+ }
202
+ const { element, property, originalInlineValue, originalInlinePriority } = commit;
203
+ if (originalInlineValue) {
204
+ element.style.setProperty(property, originalInlineValue, originalInlinePriority);
205
+ }
206
+ else {
207
+ element.style.removeProperty(property);
208
+ }
209
+ endLayoutPreview(element, property);
210
+ }
211
+ pending.length = write;
212
+ }
213
+ /** Registers a held preview inline, folding an older unapplied commit for the
214
+ * same element and property into the newest one so the held value and the
215
+ * authored baseline both stay coherent across rapid successive drags. */
216
+ function pushPendingSpacingCommit(pending, next) {
217
+ const existing = pending.findIndex((commit) => commit.iframe === next.iframe
218
+ && commit.element === next.element
219
+ && commit.property === next.property);
220
+ if (existing === -1) {
221
+ pending.push(next);
222
+ return;
223
+ }
224
+ pending[existing] = {
225
+ ...next,
226
+ originalInlineValue: pending[existing].originalInlineValue,
227
+ originalInlinePriority: pending[existing].originalInlinePriority,
228
+ };
229
+ }
137
230
  function sendMeasureModifier(iframe, cardId, altKey) {
138
231
  iframe.contentWindow?.postMessage({
139
232
  type: "measure-modifier",
@@ -156,6 +249,14 @@ export function CanvasElementOverlay() {
156
249
  const camera = useBoardCamera();
157
250
  const presentation = useCanvasPresentation();
158
251
  const presentationTransitioning = useCanvasPresentationTransitioning();
252
+ const layoutTransitioning = useCanvasLayoutTransitioning();
253
+ const temporaryAppInteraction = useTemporaryAppInteraction();
254
+ useEffect(() => {
255
+ if (!layoutTransitioning)
256
+ return;
257
+ setHover(null);
258
+ setMeasureState(null);
259
+ }, [layoutTransitioning]);
159
260
  const projectionZoom = presentation === "focus" ? 1 : camera.zoom;
160
261
  const cards = useCanvasCards(); // Projected geometry must follow card drag and resize updates.
161
262
  const cardGeometryKey = cards.map((card) => `${card.id}:${card.x}:${card.y}:${card.width}:${card.height}`).join("|");
@@ -163,6 +264,8 @@ export function CanvasElementOverlay() {
163
264
  const previousProjectionGeometryKey = useRef(projectionGeometryKey);
164
265
  const dragRef = useRef(null);
165
266
  const spacingDragRef = useRef(null);
267
+ const pendingSpacingCommitsRef = useRef([]);
268
+ const [spacingDragBadge, setSpacingDragBadge] = useState(null);
166
269
  const measureAltKeyRef = useRef(false);
167
270
  const dropGuide = useDropGuide("canvas");
168
271
  const projectedDropGuide = projectGuideToCanvas(dropGuide, projectionZoom);
@@ -245,6 +348,14 @@ export function CanvasElementOverlay() {
245
348
  const elementId = el.getAttribute(RENDERER_ELEMENT_ID_ATTR);
246
349
  return elementId ? { elementId } : null;
247
350
  }, [selected?.domElement]);
351
+ // A held spacing preview inline must not outlive the committed rule behind
352
+ // it: undo/redo/revert/clear remove the record synchronously, so restore the
353
+ // authored declaration before the renderer's next replace-styles lands.
354
+ useEffect(() => {
355
+ return subscribeChanges(() => {
356
+ reconcilePendingSpacingCommits(pendingSpacingCommitsRef.current);
357
+ });
358
+ }, []);
248
359
  useEffect(() => {
249
360
  const refreshSpacingPreviewOverlay = (drag) => {
250
361
  const descriptor = {
@@ -281,6 +392,12 @@ export function CanvasElementOverlay() {
281
392
  }
282
393
  if (data.type === "renderer-hello")
283
394
  return;
395
+ if (data.type === "projection-applied") {
396
+ releasePendingSpacingCommits(pendingSpacingCommitsRef.current, sourceIframe);
397
+ return;
398
+ }
399
+ if (getCanvasLayoutTransitioning())
400
+ return;
284
401
  if (data.type === "element-hover") {
285
402
  const msg = data;
286
403
  if (!msg.cid)
@@ -289,7 +406,7 @@ export function CanvasElementOverlay() {
289
406
  setHover((current) => current?.cardId === sourceCardId ? null : current);
290
407
  return;
291
408
  }
292
- const spacingDescriptor = msg.spacing ?? null;
409
+ const spacingDescriptor = msg.hoverSpacing ?? msg.spacing ?? null;
293
410
  const hoverPoint = msg.point ?? null;
294
411
  const hoverElement = spacingDescriptor
295
412
  ? findFrameElement(sourceIframe, msg.elementId, msg.cid, msg.src)
@@ -297,7 +414,7 @@ export function CanvasElementOverlay() {
297
414
  const hoverAffordance = spacingDescriptor && hoverElement
298
415
  ? getSpacingAffordanceForDescriptor(hoverElement, spacingDescriptor)
299
416
  : spacingDescriptor && hoverPoint && sourceIframe.contentDocument
300
- ? getSpacingAffordanceAtPoint(sourceIframe.contentDocument, hoverPoint.x, hoverPoint.y)
417
+ ? getSpacingHoverAtPoint(sourceIframe.contentDocument, hoverPoint.x, hoverPoint.y)
301
418
  : null;
302
419
  const matchingAffordance = spacingDescriptorMatches(hoverAffordance, spacingDescriptor)
303
420
  ? hoverAffordance
@@ -354,6 +471,7 @@ export function CanvasElementOverlay() {
354
471
  }
355
472
  else if (data.type === "element-deselect") {
356
473
  clearSpacingDrag(spacingDragRef);
474
+ setSpacingDragBadge(null);
357
475
  dragRef.current = null;
358
476
  clearDropGuide("canvas");
359
477
  setSelectedElement(null);
@@ -391,6 +509,7 @@ export function CanvasElementOverlay() {
391
509
  if (selectedElement)
392
510
  setSelectedElement(selectedElement);
393
511
  applySpacingPreview(spacingDragRef.current, msg.point, msg.shiftKey === true);
512
+ setSpacingDragBadge(spacingBadgeForDrag(spacingDragRef.current, msg.point));
394
513
  refreshSpacingPreviewOverlay(spacingDragRef.current);
395
514
  return;
396
515
  }
@@ -404,6 +523,7 @@ export function CanvasElementOverlay() {
404
523
  const spacing = spacingDragRef.current;
405
524
  if (spacing && spacing.iframe === sourceIframe) {
406
525
  applySpacingPreview(spacing, data.point, data.shiftKey === true);
526
+ setSpacingDragBadge(spacingBadgeForDrag(spacing, data.point));
407
527
  refreshSpacingPreviewOverlay(spacing);
408
528
  return;
409
529
  }
@@ -414,13 +534,25 @@ export function CanvasElementOverlay() {
414
534
  const spacing = spacingDragRef.current;
415
535
  if (spacing && spacing.iframe === sourceIframe) {
416
536
  spacingDragRef.current = null;
537
+ setSpacingDragBadge(null);
417
538
  if (msg.cancelled) {
418
539
  restoreSpacingPreview(spacing);
419
540
  refreshSpacingPreviewOverlay(spacing);
420
541
  }
421
542
  else {
422
543
  applySpacingPreview(spacing, msg.point, msg.shiftKey === true);
423
- commitSpacingDrag(spacing);
544
+ const records = commitSpacingDrag(spacing);
545
+ if (records.length > 0) {
546
+ pushPendingSpacingCommit(pendingSpacingCommitsRef.current, {
547
+ iframe: sourceIframe,
548
+ element: spacing.element,
549
+ property: spacing.affordance.property,
550
+ committedValue: spacingValueCss(spacing.currentValue),
551
+ keys: records.map((record) => changeKey(record)),
552
+ originalInlineValue: spacing.originalInlineValue,
553
+ originalInlinePriority: spacing.originalInlinePriority,
554
+ });
555
+ }
424
556
  refreshSpacingPreviewOverlay(spacing);
425
557
  }
426
558
  clearDropGuide("canvas");
@@ -495,6 +627,7 @@ export function CanvasElementOverlay() {
495
627
  unsubscribe();
496
628
  clearSpacingDrag(spacingDragRef);
497
629
  dragRef.current = null;
630
+ pendingSpacingCommitsRef.current = [];
498
631
  clearDropGuide("canvas");
499
632
  };
500
633
  }, []);
@@ -505,6 +638,9 @@ export function CanvasElementOverlay() {
505
638
  const projectedHoverRect = hover ? projectRect(hover.iframe, hover.rect, projectionZoom) : null;
506
639
  const projectedSpacingGuides = hover?.spacingGuides.map((guide) => projectRect(hover.iframe, guide, projectionZoom)) ?? [];
507
640
  const projectedSpacingAreas = hover?.spacingAreas.map((area) => projectRect(hover.iframe, area, projectionZoom)) ?? [];
641
+ const projectedSpacingBadge = spacingDragBadge
642
+ ? projectPoint(spacingDragBadge.iframe, spacingDragBadge.point.x, spacingDragBadge.point.y, projectionZoom)
643
+ : null;
508
644
  const hoverSpacing = hover?.spacing ?? null;
509
645
  const measureStateForSelectedFrame = measureState?.iframe === selectedFrame ? measureState : null;
510
646
  const hoverInSelectedFrame = hover?.iframe === selectedFrame ? hover : null;
@@ -529,9 +665,9 @@ export function CanvasElementOverlay() {
529
665
  hoveredBorders: hoverInSelectedFrame.borders,
530
666
  }).segments, projectionZoom)
531
667
  : [];
532
- if (presentationTransitioning)
668
+ if (presentationTransitioning || layoutTransitioning || temporaryAppInteraction)
533
669
  return _jsx("style", { "data-test": "canvas-element-overlay-styles", children: overlayStyles });
534
- return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "canvas-element-overlay-styles", children: overlayStyles }), showGuideOverlay && selectedRect && selectedFrameRect ? (_jsx(MeasurementGuideOverlay, { testId: "canvas-measurement-overlay", selectedRect: selectedRect, guideViewport: selectedFrameRect, segments: measurementSegments })) : null, projectedHoverRect ? (_jsxs(_Fragment, { children: [hoverSpacing ? projectedSpacingAreas.map((area, index) => (_jsx("div", { className: "canvas-spacing-fill", "data-test": "canvas-spacing-fill", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-area-index": index, style: overlayStyle(area), "aria-hidden": "true" }, `${hoverSpacing.property}-area-${index}`))) : null, showHoverOutline ? (_jsx("div", { className: "canvas-element-overlay", "data-test": "canvas-hover-outline", style: overlayStyle(projectedHoverRect), "aria-hidden": "true" })) : null, hoverSpacing ? projectedSpacingGuides.map((guide, index) => (_jsx("div", { className: "canvas-spacing-guide", "data-test": "canvas-spacing-guide", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-guide-index": index, style: spacingGuideStyle(guide), "aria-hidden": "true" }, `${hoverSpacing.property}-${index}`))) : null] })) : null, projectedSelectedGeometry.map(({ element, rect }, index) => (_jsx("div", { className: "canvas-selected-outline", "data-test": "canvas-selected-outline", "data-selected-cid": element.cid, "data-selected-index": index, "data-selected-src": element.src, style: overlayStyle(rect), "aria-hidden": "true" }, `${element.cid}-${index}`))), domNavigationEnabled && !inlineTextSession && selectedElements.length === 1 && selected && selectedRect && selectedFrame instanceof HTMLIFrameElement ? (_jsx(DomNavigator, { selected: selected, hierarchy: hierarchy, anchor: selectedRect, project: (element) => element.ownerDocument === selected.domElement.ownerDocument
670
+ return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "canvas-element-overlay-styles", children: overlayStyles }), showGuideOverlay && selectedRect && selectedFrameRect ? (_jsx(MeasurementGuideOverlay, { testId: "canvas-measurement-overlay", selectedRect: selectedRect, guideViewport: selectedFrameRect, segments: measurementSegments })) : null, projectedHoverRect ? (_jsxs(_Fragment, { children: [hoverSpacing ? projectedSpacingAreas.map((area, index) => (_jsx("div", { className: "canvas-spacing-fill", "data-test": "canvas-spacing-fill", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-area-index": index, style: overlayStyle(area), "aria-hidden": "true" }, `${hoverSpacing.property}-area-${index}`))) : null, showHoverOutline ? (_jsx("div", { className: "canvas-element-overlay", "data-test": "canvas-hover-outline", style: overlayStyle(projectedHoverRect), "aria-hidden": "true" })) : null, hoverSpacing ? projectedSpacingGuides.map((guide, index) => (_jsx("div", { className: "canvas-spacing-guide", "data-test": "canvas-spacing-guide", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-guide-index": index, style: spacingGuideStyle(guide), "aria-hidden": "true" }, `${hoverSpacing.property}-${index}`))) : null] })) : null, projectedSelectedGeometry.map(({ element, rect }, index) => (_jsx("div", { className: "canvas-selected-outline", "data-test": "canvas-selected-outline", "data-selected-cid": element.cid, "data-selected-index": index, "data-selected-src": element.src, style: overlayStyle(rect), "aria-hidden": "true" }, `${element.cid}-${index}`))), projectedSpacingBadge && spacingDragBadge ? (_jsx("div", { className: "canvas-spacing-value-badge", "data-test": "canvas-spacing-value-badge", style: { left: projectedSpacingBadge.x, top: projectedSpacingBadge.y }, "aria-hidden": "true", children: spacingValueCss(spacingDragBadge.value) })) : null, domNavigationEnabled && !inlineTextSession && selectedElements.length === 1 && selected && selectedRect && selectedFrame instanceof HTMLIFrameElement ? (_jsx(DomNavigator, { selected: selected, hierarchy: hierarchy, anchor: selectedRect, project: (element) => element.ownerDocument === selected.domElement.ownerDocument
535
671
  ? projectRect(selectedFrame, toRect(element.getBoundingClientRect()), projectionZoom)
536
672
  : null })) : null, _jsx(DropGuideOverlay, { guide: projectedDropGuide, lineClassName: "canvas-dom-drop-line", lineTestId: "canvas-dom-drop-line", targetClassName: "canvas-dom-drop-target", targetTestId: "canvas-dom-drop-target" })] }));
537
673
  }
@@ -0,0 +1,10 @@
1
+ import type { ReactElement, SVGProps } from "react";
2
+ type CanvasToolIconProps = Pick<SVGProps<SVGSVGElement>, "aria-hidden" | "className" | "style"> & {
3
+ readonly size?: string | number;
4
+ };
5
+ export declare function DesignPointerFilledIcon(props: CanvasToolIconProps): ReactElement;
6
+ export declare function SketchThickIcon(props: CanvasToolIconProps): ReactElement;
7
+ export declare function PanHandFilledIcon(props: CanvasToolIconProps): ReactElement;
8
+ export declare function SelectPlayOutlineIcon(props: CanvasToolIconProps): ReactElement;
9
+ export declare function SelectPlayFilledIcon(props: CanvasToolIconProps): ReactElement;
10
+ export {};