nudge-ui 0.2.2 → 0.2.4

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 (244) hide show
  1. package/README.md +164 -46
  2. package/dist/client.mjs +260 -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 +15 -7
  26. package/dist/inspector/agent/verification.js +113 -41
  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 +135 -38
  31. package/dist/inspector/canvas/CanvasElementOverlay.js +166 -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 +270 -122
  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/frameScrollbarStyles.d.ts +3 -0
  48. package/dist/inspector/canvas/frameScrollbarStyles.js +21 -0
  49. package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -1
  50. package/dist/inspector/canvas/keyboardShortcuts.js +4 -4
  51. package/dist/inspector/canvas/layout.d.ts +52 -0
  52. package/dist/inspector/canvas/layout.js +106 -0
  53. package/dist/inspector/canvas/projection.d.ts +15 -5
  54. package/dist/inspector/canvas/projection.js +69 -13
  55. package/dist/inspector/canvas/rendererBootstrap.js +32 -4
  56. package/dist/inspector/canvas/rendererElementSelector.js +47 -6
  57. package/dist/inspector/canvas/rendererStylesheet.js +2 -2
  58. package/dist/inspector/canvas/resizeHandleScale.js +1 -1
  59. package/dist/inspector/canvas/sessionStore.d.ts +11 -26
  60. package/dist/inspector/canvas/sessionStore.js +43 -183
  61. package/dist/inspector/canvas/staleChangeDetector.js +9 -3
  62. package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
  63. package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
  64. package/dist/inspector/canvas/viewStore.d.ts +38 -0
  65. package/dist/inspector/canvas/viewStore.js +104 -0
  66. package/dist/inspector/canvas/wheelPan.d.ts +8 -0
  67. package/dist/inspector/canvas/wheelPan.js +8 -0
  68. package/dist/inspector/changes/changesLog.d.ts +9 -6
  69. package/dist/inspector/changes/changesLog.js +48 -32
  70. package/dist/inspector/changes/codecs.d.ts +1 -0
  71. package/dist/inspector/changes/codecs.js +10 -1
  72. package/dist/inspector/changes/draftChanges.d.ts +58 -0
  73. package/dist/inspector/changes/draftChanges.js +304 -0
  74. package/dist/inspector/changes/editModel.d.ts +10 -0
  75. package/dist/inspector/changes/editModel.js +9 -2
  76. package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
  77. package/dist/inspector/changes/previewDiagnostics.js +11 -25
  78. package/dist/inspector/changes/types.d.ts +7 -0
  79. package/dist/inspector/clientManifest.d.ts +1 -1
  80. package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
  81. package/dist/inspector/comments/CommentsOverlay.js +250 -0
  82. package/dist/inspector/comments/element.d.ts +6 -0
  83. package/dist/inspector/comments/element.js +54 -0
  84. package/dist/inspector/comments/store.d.ts +29 -0
  85. package/dist/inspector/comments/store.js +121 -0
  86. package/dist/inspector/comments/verification.d.ts +5 -0
  87. package/dist/inspector/comments/verification.js +52 -0
  88. package/dist/inspector/componentSemantics/adapterRegistry.d.ts +6 -1
  89. package/dist/inspector/componentSemantics/adapterRegistry.js +23 -1
  90. package/dist/inspector/componentSemantics/reactRuntime.d.ts +2 -1
  91. package/dist/inspector/componentSemantics/reactRuntime.js +38 -0
  92. package/dist/inspector/componentSemantics/runtimeBridge.d.ts +4 -2
  93. package/dist/inspector/componentSemantics/runtimeBridge.js +17 -0
  94. package/dist/inspector/componentSemantics/types.d.ts +11 -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 +20 -27
  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 +174 -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.js +21 -15
  111. package/dist/inspector/overlay/interactionStyles.js +3 -1
  112. package/dist/inspector/overlay/spacingGestures.d.ts +13 -1
  113. package/dist/inspector/overlay/spacingGestures.js +82 -5
  114. package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
  115. package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
  116. package/dist/inspector/projection/renderedInstance.d.ts +17 -3
  117. package/dist/inspector/projection/renderedInstance.js +62 -11
  118. package/dist/inspector/projection/structuralProjection.d.ts +2 -1
  119. package/dist/inspector/projection/structuralProjection.js +21 -17
  120. package/dist/inspector/projection/textProjection.d.ts +2 -1
  121. package/dist/inspector/projection/textProjection.js +64 -15
  122. package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
  123. package/dist/inspector/prompt/clipboardHandoff.d.ts +15 -18
  124. package/dist/inspector/prompt/clipboardHandoff.js +97 -143
  125. package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
  126. package/dist/inspector/prompt/generatePrompt.js +22 -17
  127. package/dist/inspector/runtime/runtimeConfig.d.ts +9 -0
  128. package/dist/inspector/runtime/runtimeConfig.js +24 -0
  129. package/dist/inspector/selection/sourceSite.d.ts +12 -0
  130. package/dist/inspector/selection/sourceSite.js +28 -0
  131. package/dist/inspector/settings/SettingsDialog.js +21 -8
  132. package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
  133. package/dist/inspector/shell/AgentStatusButton.js +36 -0
  134. package/dist/inspector/shell/AppShell.js +4 -2
  135. package/dist/inspector/shell/ChangesLog.js +46 -26
  136. package/dist/inspector/shell/CopyPromptButton.js +184 -93
  137. package/dist/inspector/shell/EmptyState.js +9 -8
  138. package/dist/inspector/shell/InspectorShell.js +13 -21
  139. package/dist/inspector/shell/PeekOriginalButton.js +24 -23
  140. package/dist/inspector/shell/originalPreview.js +5 -5
  141. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  142. package/dist/inspector/shell/shortcuts.js +11 -0
  143. package/dist/inspector/sketch/SketchEditor.js +14 -14
  144. package/dist/inspector/sketch/SketchEntryButton.js +1 -1
  145. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
  146. package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
  147. package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
  148. package/dist/inspector/sketch/SketchOverlay.js +7 -7
  149. package/dist/inspector/sketch/SketchWorkspace.js +18 -12
  150. package/dist/inspector/sketch/interaction.d.ts +1 -0
  151. package/dist/inspector/sketch/interaction.js +8 -4
  152. package/dist/inspector/sketch/model.d.ts +5 -0
  153. package/dist/inspector/sketch/model.js +6 -2
  154. package/dist/inspector/sketch/raster.d.ts +1 -2
  155. package/dist/inspector/sketch/raster.js +68 -4
  156. package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
  157. package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
  158. package/dist/inspector/styleEditors/BorderEditor.js +49 -78
  159. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
  160. package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
  161. package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
  162. package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
  163. package/dist/inspector/styleEditors/ColorPicker.js +10 -4
  164. package/dist/inspector/styleEditors/GapField.js +3 -5
  165. package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
  166. package/dist/inspector/styleEditors/GridChildSection.js +18 -9
  167. package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
  168. package/dist/inspector/styleEditors/GridPicker.js +8 -2
  169. package/dist/inspector/styleEditors/GridSection.js +1 -1
  170. package/dist/inspector/styleEditors/GridValueField.js +19 -14
  171. package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
  172. package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
  173. package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
  174. package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
  175. package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
  176. package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
  177. package/dist/inspector/styleEditors/LayoutSection.js +32 -11
  178. package/dist/inspector/styleEditors/SpacingBox.js +7 -5
  179. package/dist/inspector/styleEditors/Typography.js +11 -3
  180. package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
  181. package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
  182. package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
  183. package/dist/inspector/styleEditors/gradientValue.js +114 -0
  184. package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
  185. package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
  186. package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
  187. package/dist/inspector/styleEditors/layoutValue.js +0 -42
  188. package/dist/inspector/styleEditors/nudgeValue.d.ts +2 -2
  189. package/dist/inspector/styleEditors/nudgeValue.js +36 -8
  190. package/dist/inspector/tokens/TokenField.d.ts +7 -0
  191. package/dist/inspector/tokens/TokenField.js +35 -25
  192. package/dist/inspector/tokens/TokenLabel.js +5 -14
  193. package/dist/inspector/tokens/editActions.d.ts +1 -1
  194. package/dist/inspector/tokens/editActions.js +7 -1
  195. package/dist/inspector/ui/AtRuleContext.js +4 -6
  196. package/dist/inspector/ui/Button.d.ts +6 -2
  197. package/dist/inspector/ui/Button.js +4 -3
  198. package/dist/inspector/ui/ControlSurface.d.ts +2 -2
  199. package/dist/inspector/ui/ControlSurface.js +4 -3
  200. package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
  201. package/dist/inspector/ui/InlineStyleWarning.js +13 -0
  202. package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
  203. package/dist/inspector/ui/InspectorPopover.js +4 -5
  204. package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
  205. package/dist/inspector/ui/PopoverListbox.js +8 -7
  206. package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
  207. package/dist/inspector/ui/SegmentedControl.js +2 -2
  208. package/dist/inspector/ui/Select.d.ts +6 -1
  209. package/dist/inspector/ui/Select.js +7 -11
  210. package/dist/inspector/ui/SideValuesField.js +1 -1
  211. package/dist/inspector/ui/Tooltip.d.ts +26 -0
  212. package/dist/inspector/ui/Tooltip.js +32 -0
  213. package/dist/inspector/ui/classNames.d.ts +2 -0
  214. package/dist/inspector/ui/classNames.js +4 -0
  215. package/dist/inspector/ui/customCursors.d.ts +23 -0
  216. package/dist/inspector/ui/customCursors.js +122 -0
  217. package/dist/inspector/ui/dragCursor.d.ts +0 -2
  218. package/dist/inspector/ui/dragCursor.js +2 -10
  219. package/dist/inspector/ui/portalContainer.d.ts +2 -0
  220. package/dist/inspector/ui/portalContainer.js +6 -0
  221. package/dist/inspector/ui/styles.js +29 -21
  222. package/dist/inspector/workspace/commands.d.ts +9 -0
  223. package/dist/inspector/workspace/commands.js +82 -0
  224. package/dist/inspector/workspace/controller.d.ts +4 -0
  225. package/dist/inspector/workspace/controller.js +66 -0
  226. package/dist/inspector/workspace/handoff.d.ts +42 -0
  227. package/dist/inspector/workspace/handoff.js +115 -0
  228. package/dist/inspector/workspace/sentItems.d.ts +19 -0
  229. package/dist/inspector/workspace/sentItems.js +51 -0
  230. package/dist/inspector/workspace/timeline.d.ts +30 -0
  231. package/dist/inspector/workspace/timeline.js +108 -0
  232. package/dist/internal/agent-protocol.d.ts +19 -2
  233. package/dist/internal/agent-protocol.js +13 -0
  234. package/dist/nudge-ui.mjs +558 -311
  235. package/dist/project/artifacts.d.ts +13 -0
  236. package/dist/project/artifacts.js +253 -0
  237. package/dist/transport/artifacts.d.ts +8 -0
  238. package/dist/transport/artifacts.js +9 -0
  239. package/dist/transport/editor.js +3 -1
  240. package/dist/transport/routes.d.ts +2 -0
  241. package/dist/transport/routes.js +2 -0
  242. package/package.json +7 -3
  243. package/dist/inspector/changes/workspaceChanges.d.ts +0 -47
  244. package/dist/inspector/changes/workspaceChanges.js +0 -265
@@ -1,10 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { getCanonicalFrameProjection } from "./projection.js";
3
+ import { getDraftForCard } from "../drafts/store.js";
4
+ import { iterationId, contentSourceUrl, getLinkedFrameIds } from "./frameContent.js";
2
5
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
6
  import { IconCornerLeftUp } from "@tabler/icons-react";
4
- import { useCanvasCards, activateIframeWorkspace, removeCanvasCard, getSelectedCardId, getFocusedCardId, getCanvasCards, setBoardCamera, getBoardCamera, fitAllCards, setCanvasPresentation, focusCard, hasFitAllRan, useBoardCamera, useCanvasPresentation, useCanvasPresentationTransitioning, useSelectedCardId, useFocusedCardId, resizeCard, setCardPosition, updateCardTitle, addCanvasCard, findCardByNormalizedUrl, selectCard, CARD_GAP, updateCardUrl, } from "./canvasStore.js";
7
+ import { useCanvasCards, activateIframeWorkspace, getSelectedCardId, getFocusedCardId, getCanvasCards, batchCanvasChanges, fitAllCards, focusCanvasCards, focusCard, useSelectedCardId, useFocusedCardId, resizeCard, setCardPosition, updateCardTitle, addCanvasCard, selectCard, deselectCard, CARD_GAP, updateCardUrl, } from "./canvasStore.js";
8
+ import { setBoardCamera, getBoardCamera, setCanvasPresentation, hasFitAllRan, useBoardCamera, useCanvasPresentation, useCanvasPresentationTransitioning, useCanvasLayoutTransitioning, useTemporaryAppInteraction, setTemporaryAppInteraction, } from "./viewStore.js";
5
9
  import { CanvasCard } from "./CanvasCard.js";
10
+ import { useCanvasLayoutAnimation } from "./useCanvasLayoutAnimation.js";
6
11
  import { Button } from "../ui/Button.js";
7
- import { useCanvasMode } from "./canvasStore.js";
12
+ import { useCanvasMode } from "./viewStore.js";
8
13
  import { subscribeChanges } from "../changes/changesLog.js";
9
14
  import { subscribeOriginalPreview, isOriginalPreviewActive } from "../shell/originalPreview.js";
10
15
  import { recordCanvasStructuralProjectionReports } from "../projection/structuralProjection.js";
@@ -12,31 +17,37 @@ import { recordCanvasRenderedInstanceProjectionReports } from "../projection/ren
12
17
  import { recordCanvasTextProjectionReports } from "../projection/textProjection.js";
13
18
  import { getChangesList, isPreviewableChange } from "../changes/changesLog.js";
14
19
  import { changeKey } from "../changes/model.js";
15
- import { getWorkspaceChanges } from "../changes/workspaceChanges.js";
20
+ import { getActiveDraftChanges } from "../changes/draftChanges.js";
16
21
  import { beginPreviewAttempt, publishPreviewDiagnostic } from "../changes/previewDiagnostics.js";
17
22
  import { verifyManagedStyleProjection } from "../changes/managedStyleProjection.js";
18
23
  import { getCanvasPreviewDocument, getRegisteredFrames, isCanvasCanonicalProjectionRevisionCurrent, PROJECT_ID, projectToAllReadyCards, recordCanvasProjectionApplied, WORKSPACE_ID, } from "./projection.js";
19
- import { normalizeUrl } from "./normalizeUrl.js";
24
+ import { normalizeUrl, normalizedUrlKey } from "./normalizeUrl.js";
20
25
  import { createNudgeUiEditorUrl } from "../../transport/editor.js";
21
26
  import { disposeInlineTextEdit } from "../inline-text/inlineTextEditor.js";
22
27
  import { PROTOCOL_VERSION, isProjectionAppliedMessage, isKeyboardShortcutMessage, isRenderedInstanceProjectionReportMessage, isTextProjectionReportMessage, isStructuralProjectionReportMessage, } from "./frameProtocol.js";
28
+ import { wheelPanDelta } from "./wheelPan.js";
23
29
  import { iframePointToClientPoint, zoomCameraAtPointer } from "./canvasGestures.js";
24
- const canvasWorkspaceStyles = ".canvas-workspace {\n position: fixed;\n inset: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface-panel);\n pointer-events: auto;\n}\n\n.canvas-workspace__board {\n flex: 1;\n overflow: hidden;\n position: relative;\n background: var(--surface-panel);\n}\n\n.canvas-workspace__focus-action {\n position: absolute;\n top: max(12px, env(safe-area-inset-top));\n left: max(12px, env(safe-area-inset-left));\n z-index: calc(var(--z-index) - 1);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--space-4);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n pointer-events: auto;\n}\n\n.canvas-workspace__board.is-grabbable {\n cursor: grab;\n}\n\n.canvas-workspace__board.is-grabbing {\n cursor: grabbing;\n}\n\n/* Canvas gestures take precedence over every card interaction while Space is\n held. This also closes the short postMessage gap before an iframe receives\n its mirrored modifier state. */\n.canvas-workspace__board.is-grabbable .canvas-card,\n.canvas-workspace__board.is-grabbing .canvas-card {\n pointer-events: none;\n}\n\n.canvas-workspace__board-content {\n position: absolute;\n width: 100%;\n height: 100%;\n /*\n * Leave `will-change` unset so the browser can re-rasterize this subtree at\n * the current camera scale. A permanent `will-change: transform` promotes\n * it to a fixed bitmap, which makes iframe content look soft when zoomed.\n */\n}\n\n.canvas-workspace__board.is-grabbing .canvas-workspace__board-content {\n /* Panning benefits from a cached composited layer; zoom restores the crisp\n * default path as soon as the gesture ends. */\n will-change: transform;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-workspace__board-content {\n transition: transform 200ms ease;\n}\n\n.canvas-workspace[data-presentation=\"focus\"] .canvas-workspace__board-content {\n inset: 0;\n transform: none !important;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-card.is-presentation-card {\n transition: left 200ms ease, top 200ms ease;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-card:not(.is-presentation-card) {\n visibility: hidden !important;\n pointer-events: none !important;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .canvas-workspace.is-presentation-transitioning .canvas-workspace__board-content,\n .canvas-workspace.is-presentation-transitioning .canvas-card.is-presentation-card {\n transition: none;\n }\n}\n\n.canvas-workspace__target-error {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n color: #6b7280;\n font: 500 14px/1.4 system-ui, sans-serif;\n}\n";
25
- const canvasCardStyles = ".canvas-card {\n position: relative;\n}\n\n/* Extend the card's hover region to the toolbar above the iframe. */\n.canvas-card::before {\n content: \"\";\n position: absolute;\n right: 0;\n bottom: 100%;\n left: 0;\n height: calc(var(--control-height-large) + var(--space-8));\n pointer-events: auto;\n}\n\n.canvas-card__frame {\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--surface-panel);\n border-radius: 0;\n overflow: hidden;\n}\n\n.canvas-card.is-selected .canvas-card__frame {\n outline: 2px solid var(--accent);\n outline-offset: -2px;\n}\n\n.canvas-card.is-focus-presentation .canvas-card__frame {\n outline: none;\n}\n\n.canvas-card.is-focused .canvas-card__frame {\n animation: canvas-card-focus 900ms ease-out;\n}\n\n@keyframes canvas-card-focus {\n 0%, 35% { outline: 3px solid var(--accent); outline-offset: -3px; }\n 100% { outline: 0 solid transparent; outline-offset: -2px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .canvas-card.is-focused .canvas-card__frame {\n animation: none;\n }\n}\n\n.canvas-card__toolbar {\n display: flex;\n align-items: center;\n position: absolute;\n left: 0;\n right: 0;\n bottom: calc(100% + var(--space-8));\n justify-content: flex-start;\n z-index: 1;\n cursor: grab;\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transition: opacity 120ms ease;\n}\n\n.canvas-card:hover .canvas-card__toolbar,\n.canvas-card:focus-within .canvas-card__toolbar,\n.canvas-card.is-selected .canvas-card__toolbar,\n.canvas-card.is-focused .canvas-card__toolbar {\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n}\n\n.canvas-card.is-dragging .canvas-card__toolbar {\n cursor: grabbing;\n}\n\n.canvas-card__drag-surface {\n position: relative;\n display: flex;\n align-items: center;\n gap: var(--space-8);\n flex: 1 1 auto;\n min-width: 0;\n min-height: var(--control-height-large);\n padding: 0;\n cursor: grab;\n user-select: none;\n touch-action: none;\n}\n\n.canvas-card.is-dragging .canvas-card__drag-surface {\n cursor: grabbing;\n}\n\n.canvas-card__dimensions {\n position: absolute;\n left: 50%;\n bottom: 0;\n color: var(--text-secondary);\n font-size: var(--font-size-base);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n}\n\n.canvas-card__iframe {\n width: 100%;\n height: 100%;\n border: none;\n display: block;\n}\n\n.canvas-card__sketch-overlay {\n position: absolute;\n inset: 0;\n z-index: 2;\n display: block;\n pointer-events: none;\n}\n\n.canvas-card__loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: var(--text-muted, #777777);\n font-size: 12px;\n font-weight: 500;\n}\n\n.canvas-card__error {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n height: 100%;\n padding: 20px;\n text-align: center;\n}\n\n.canvas-card__error p {\n margin: 0;\n color: var(--text-muted, #777777);\n font-size: 12px;\n font-weight: 500;\n}\n\n.canvas-card__error-actions {\n display: flex;\n gap: 8px;\n}\n\n.canvas-card__resize-handle {\n position: absolute;\n z-index: 3;\n display: flex;\n align-items: flex-end;\n justify-content: flex-end;\n padding: 4px;\n color: var(--text-muted, #777777);\n opacity: 0;\n transition: opacity 150ms ease;\n touch-action: none;\n user-select: none;\n}\n\n.canvas-card__resize-handle--top,\n.canvas-card__resize-handle--bottom {\n right: 16px;\n left: 16px;\n height: 16px;\n cursor: ns-resize;\n}\n\n.canvas-card__resize-handle--top {\n top: 0;\n}\n\n.canvas-card__resize-handle--bottom {\n bottom: 0;\n}\n\n.canvas-card__resize-handle--left,\n.canvas-card__resize-handle--right {\n top: 16px;\n bottom: 16px;\n width: 16px;\n cursor: ew-resize;\n}\n\n.canvas-card__resize-handle--left {\n left: 0;\n}\n\n.canvas-card__resize-handle--right {\n right: 0;\n}\n\n.canvas-card__resize-handle--top-left,\n.canvas-card__resize-handle--top-right,\n.canvas-card__resize-handle--bottom-right,\n.canvas-card__resize-handle--bottom-left {\n z-index: 4;\n width: 24px;\n height: 24px;\n cursor: nwse-resize;\n}\n\n.canvas-card__resize-handle--top-left {\n top: 0;\n left: 0;\n}\n\n.canvas-card__resize-handle--top-right {\n top: 0;\n right: 0;\n cursor: nesw-resize;\n}\n\n.canvas-card__resize-handle--bottom-right {\n right: 0;\n bottom: 0;\n}\n\n.canvas-card__resize-handle--bottom-left {\n bottom: 0;\n left: 0;\n cursor: nesw-resize;\n}\n\n.canvas-card:hover .canvas-card__resize-handle,\n.canvas-card:focus-within .canvas-card__resize-handle {\n opacity: 1;\n}\n\n.canvas-card__resize-handle:hover {\n color: var(--accent);\n}\n\n.canvas-card__resize-handle:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n opacity: 1;\n}\n";
26
- const foundationStyles = ":host {\n all: initial;\n --surface-panel: #F5F5F4;\n --surface-control: var(--surface-well);\n --surface-control-hover: var(--surface-well-2x);\n --surface-raised: #FAFAFA;\n --surface-raised-2x: #fff ;\n --surface-raised-hover: #F5F5F4;\n --surface-well: hsla(0, 0%, 0%, 0.05);\n --surface-well-2x: hsla(0, 0%, 0%, 0.10);\n\n --border-subtle: hsla(0, 0%, 0%, 0.1);\n --border-medium: hsla(0, 0%, 0%, 0.2);\n --border-strong: #d8d8d8;\n\n --text-primary: #242424;\n --text-secondary: #6f6f6f;\n --text-muted: #777777;\n --text-muted-subtle: #a1a1a1;\n\n --accent: #0096ff;\n --accent-surface: rgba(0, 176, 255, 0.15);\n --warning: #d97706;\n --warning-text: #9a5b00;\n --warning-surface: #fff6e5;\n --danger: #df4b4b;\n --danger-text: #bd3636;\n --danger-surface: rgba(223, 75, 75, 0.2);\n\n --space-0: 0;\n --space-4: 4px;\n --space-8: 8px;\n --space-12: 12px;\n --space-16: 16px;\n --space-20: 20px;\n --space-24: 24px;\n --space-32: 32px;\n --space-40: 40px;\n --space-48: 48px;\n\n /* Radius */\n --radius-0: 0;\n --radius-4: 4px;\n --radius-8: 8px;\n --radius-12: 12px;\n --radius-16: 16px;\n --radius-20: 20px;\n --radius-24: 24px;\n --radius-pill: 999px;\n\n --radius-control: var(--radius-8);\n --shadow-outline: 0 0 0 1px rgba(0, 0, 0, 0.05);\n --shadow-popup: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 16px rgba(0, 0, 0, 0.10);\n --shadow-control: inset 0 -2px 0 rgba(0, 0, 0, 0.03), 0 0 0 0.5px rgba(0, 0, 0, 0.25);\n --z-index: 2147483647;\n\n --font-family: \"Inter Variable\", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n --font-size-base: 12px;\n --font-size-small: 10px;\n --font-weight-base: 400;\n --font-weight-label: 600;\n\n /* Control semantic sizes */\n --control-height-small: 24px;\n --control-height-large: 32px;\n --control-height-xlarge: 40px;\n --label-column: 104px;\n\n --icon-size-small: 16px;\n --icon-size-large: 20px;\n --icon-stroke-width: 1.5px;\n\n position: fixed;\n inset: 0;\n z-index: var(--z-index);\n pointer-events: none;\n font-family: var(--font-family);\n background: transparent;\n box-sizing: border-box;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nbutton,\ninput,\nselect {\n font-family: inherit;\n}\n";
30
+ const canvasWorkspaceStyles = ".canvas-workspace {\n position: fixed;\n inset: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface-panel);\n pointer-events: auto;\n}\n\n.canvas-workspace__board {\n flex: 1;\n overflow: hidden;\n position: relative;\n background: var(--surface-panel);\n}\n\n.canvas-workspace__focus-action {\n position: absolute;\n top: max(12px, env(safe-area-inset-top));\n left: max(12px, env(safe-area-inset-left));\n z-index: calc(var(--z-index) - 1);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--space-4);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n pointer-events: auto;\n}\n\n.canvas-workspace__board.is-grabbable {\n cursor: grab;\n}\n\n.canvas-workspace__board.is-grabbing {\n cursor: grabbing;\n}\n\n/* Canvas gestures take precedence over every card interaction while Space is\n held. This also closes the short postMessage gap before an iframe receives\n its mirrored modifier state. */\n.canvas-workspace__board.is-grabbable .canvas-card,\n.canvas-workspace__board.is-grabbing .canvas-card {\n pointer-events: none;\n}\n\n.canvas-workspace__board-content {\n position: absolute;\n width: 100%;\n height: 100%;\n /*\n * Leave `will-change` unset so the browser can re-rasterize this subtree at\n * the current camera scale. A permanent `will-change: transform` promotes\n * it to a fixed bitmap, which makes iframe content look soft when zoomed.\n */\n}\n\n.canvas-workspace__board.is-grabbing .canvas-workspace__board-content {\n /* Panning benefits from a cached composited layer; zoom restores the crisp\n * default path as soon as the gesture ends. */\n will-change: transform;\n}\n\n.canvas-workspace[data-presentation=\"focus\"] .canvas-workspace__board-content {\n inset: 0;\n transform: none !important;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-card:not(.is-presentation-card) {\n visibility: hidden !important;\n pointer-events: none !important;\n}\n\n.canvas-workspace__target-error {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n color: #6b7280;\n font: 500 14px/1.4 system-ui, sans-serif;\n}\n\n/* Moving iframes must not acquire hover state beneath a stationary pointer. */\n.canvas-workspace.is-presentation-transitioning .canvas-card__iframe,\n.canvas-workspace__board.is-layout-transitioning .canvas-card__iframe {\n pointer-events: none;\n}\n";
31
+ const canvasCardStyles = ".canvas-card {\n position: relative;\n}\n\n/* Extend the card's hover region to the toolbar above the iframe. */\n.canvas-card::before {\n content: \"\";\n position: absolute;\n right: 0;\n bottom: 100%;\n left: 0;\n height: calc((var(--control-height-large) + var(--space-16)) * var(--canvas-toolbar-scale, 1));\n pointer-events: auto;\n}\n\n.canvas-card__frame {\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--surface-panel);\n border-radius: 0;\n overflow: hidden;\n}\n\n.canvas-card.is-selected .canvas-card__frame {\n outline: 2px solid var(--frame-accent, var(--accent));\n outline-offset: -2px;\n}\n\n.canvas-card.is-focus-presentation .canvas-card__frame {\n outline: none;\n}\n\n.canvas-card.is-focused.is-selected:not(.is-linked) .canvas-card__frame {\n animation: canvas-card-focus 900ms ease-out;\n}\n\n@keyframes canvas-card-focus {\n 0%, 35% { outline: 3px solid var(--frame-accent, var(--accent)); outline-offset: -3px; }\n 100% { outline: 0 solid transparent; outline-offset: -2px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .canvas-card.is-focused .canvas-card__frame {\n animation: none;\n }\n}\n\n.canvas-card__header {\n display: flex;\n align-items: center;\n position: absolute;\n left: 0;\n right: 0;\n bottom: calc(100% + var(--space-8));\n justify-content: flex-end;\n z-index: 3;\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n}\n\n.canvas-card__drag-surface {\n position: relative;\n display: flex;\n align-items: center;\n gap: var(--space-8);\n flex: 0 1 auto;\n margin-right: auto;\n min-width: 0;\n min-height: var(--control-height-large);\n padding: 0;\n cursor: grab;\n user-select: none;\n touch-action: none;\n}\n\n.canvas-card.is-dragging .canvas-card__drag-surface {\n cursor: grabbing;\n}\n\n.canvas-card__identity {\n display: flex;\n align-items: center;\n gap: var(--space-8);\n min-width: 0;\n}\n\n.canvas-card__dimensions {\n display: flex;\n align-items: center;\n gap: var(--space-8);\n position: relative;\n flex: 0 1 auto;\n min-width: 0;\n max-width: 260px;\n color: var(--text-secondary);\n font-size: var(--font-size-base);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n pointer-events: none;\n}\n\n.canvas-card__title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.canvas-card__live-badge,\n.canvas-card__iteration-badge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 20px;\n height: 20px;\n background: var(--accent-surface);\n color: var(--accent);\n border-radius: var(--radius-4);\n pointer-events: auto;\n}\n\n.canvas-card__floating-toolbar {\n position: absolute;\n left: 50%;\n flex-shrink: 0;\n width: max-content;\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transform: translate(-50%, calc(-24px + var(--space-4)));\n transition: opacity 140ms ease-out, transform 140ms ease-out, visibility 140ms;\n}\n\n.canvas-card:hover .canvas-card__floating-toolbar,\n.canvas-card:focus-within .canvas-card__floating-toolbar,\n.canvas-card__floating-toolbar[data-busy=\"true\"] {\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n transform: translate(-50%, -24px);\n}\n\n.canvas-card__actions.canvas-toolbar {\n position: static;\n width: max-content;\n transform: none;\n white-space: nowrap;\n}\n\n.canvas-card__actions .canvas-toolbar__divider {\n margin-inline: 0;\n}\n\n@media (hover: none) {\n .canvas-card.is-selected .canvas-card__floating-toolbar {\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n transform: translate(-50%, -24px);\n }\n}\n\n.canvas-card.is-dragging .canvas-card__floating-toolbar,\n.canvas-workspace__board.is-grabbable .canvas-card__floating-toolbar,\n.canvas-workspace__board.is-grabbing .canvas-card__floating-toolbar,\n.canvas-workspace.is-presentation-transitioning .canvas-card__floating-toolbar,\n.canvas-workspace__board.is-layout-transitioning .canvas-card__floating-toolbar {\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transition: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .canvas-card__floating-toolbar { transition: none; }\n}\n\n.canvas-card__iteration-error {\n position: absolute;\n right: 0;\n bottom: calc(100% + var(--space-8));\n color: var(--text-danger, #b42318);\n font-size: 12px;\n white-space: nowrap;\n}\n\n.canvas-card__iframe {\n width: 100%;\n height: 100%;\n border: none;\n display: block;\n}\n\n.canvas-card__sketch-overlay {\n position: absolute;\n inset: 0;\n z-index: 2;\n display: block;\n pointer-events: none;\n}\n\n.canvas-card__loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: var(--text-muted, #777777);\n font-size: 12px;\n font-weight: 500;\n}\n\n.canvas-card__error {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n height: 100%;\n padding: 20px;\n text-align: center;\n}\n\n.canvas-card__error p {\n margin: 0;\n color: var(--text-muted, #777777);\n font-size: 12px;\n font-weight: 500;\n}\n\n.canvas-card__error-actions {\n display: flex;\n gap: 8px;\n}\n\n.canvas-card__resize-handle {\n position: absolute;\n z-index: 3;\n display: flex;\n align-items: flex-end;\n justify-content: flex-end;\n padding: 4px;\n color: var(--text-muted, #777777);\n opacity: 0;\n transition: opacity 150ms ease;\n touch-action: none;\n user-select: none;\n}\n\n.canvas-card__resize-handle--top,\n.canvas-card__resize-handle--bottom {\n right: 16px;\n left: 16px;\n height: 16px;\n cursor: ns-resize;\n}\n\n.canvas-card__resize-handle--top {\n top: 0;\n}\n\n.canvas-card__resize-handle--bottom {\n bottom: 0;\n}\n\n.canvas-card__resize-handle--left,\n.canvas-card__resize-handle--right {\n top: 16px;\n bottom: 16px;\n width: 16px;\n cursor: ew-resize;\n}\n\n.canvas-card__resize-handle--left {\n left: 0;\n}\n\n.canvas-card__resize-handle--right {\n right: 0;\n}\n\n.canvas-card__resize-handle--top-left,\n.canvas-card__resize-handle--top-right,\n.canvas-card__resize-handle--bottom-right,\n.canvas-card__resize-handle--bottom-left {\n z-index: 4;\n width: 24px;\n height: 24px;\n cursor: nwse-resize;\n}\n\n.canvas-card__resize-handle--top-left {\n top: 0;\n left: 0;\n}\n\n.canvas-card__resize-handle--top-right {\n top: 0;\n right: 0;\n cursor: nesw-resize;\n}\n\n.canvas-card__resize-handle--bottom-right {\n right: 0;\n bottom: 0;\n}\n\n.canvas-card__resize-handle--bottom-left {\n bottom: 0;\n left: 0;\n cursor: nesw-resize;\n}\n\n.canvas-card:hover .canvas-card__resize-handle,\n.canvas-card:focus-within .canvas-card__resize-handle {\n opacity: 1;\n}\n\n.canvas-card__resize-handle:hover {\n color: var(--accent);\n}\n\n.canvas-card__resize-handle:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n opacity: 1;\n}\n\n.canvas-card.is-linked {\n --frame-accent: #a855f7;\n}\n\n.canvas-card.is-linked:not(.is-focus-presentation) .canvas-card__frame {\n outline: 2px solid var(--frame-accent);\n outline-offset: -2px;\n}\n\n.canvas-card__iteration-badge {\n color: var(--text-secondary);\n background: var(--surface-raised-2x);\n}\n";
32
+ const foundationStyles = ":host {\n all: initial;\n --surface-panel: #F5F5F4;\n --surface-control: var(--surface-well);\n --surface-control-hover: var(--surface-well-2x);\n --surface-raised: #FAFAFA;\n --surface-raised-2x: #fff ;\n --surface-raised-hover: #F5F5F4;\n --surface-well: hsla(0, 0%, 0%, 0.05);\n --surface-well-2x: hsla(0, 0%, 0%, 0.10);\n\n --border-subtle: hsla(0, 0%, 0%, 0.1);\n --border-medium: hsla(0, 0%, 0%, 0.2);\n --border-strong: #d8d8d8;\n\n --text-primary: #242424;\n --text-secondary: #6f6f6f;\n --text-muted: #777777;\n --text-muted-subtle: #a1a1a1;\n\n --accent: #0096ff;\n --accent-surface: rgba(0, 176, 255, 0.15);\n --warning: #d97706;\n --warning-text: #9a5b00;\n --warning-surface: #fff6e5;\n --danger: #df4b4b;\n --danger-text: #bd3636;\n --danger-surface: rgba(223, 75, 75, 0.2);\n --success: #2e7d52;\n\n --space-0: 0;\n --space-4: 4px;\n --space-8: 8px;\n --space-12: 12px;\n --space-16: 16px;\n --space-20: 20px;\n --space-24: 24px;\n --space-32: 32px;\n --space-40: 40px;\n --space-48: 48px;\n\n /* Radius */\n --radius-0: 0;\n --radius-4: 4px;\n --radius-8: 8px;\n --radius-12: 12px;\n --radius-16: 16px;\n --radius-20: 20px;\n --radius-24: 24px;\n --radius-pill: 999px;\n\n --radius-control: var(--radius-8);\n --shadow-outline: 0 0 0 1px rgba(0, 0, 0, 0.05);\n --shadow-popup: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 16px rgba(0, 0, 0, 0.10);\n --shadow-control: inset 0 -2px 0 rgba(0, 0, 0, 0.03), 0 0 0 0.5px rgba(0, 0, 0, 0.25);\n --z-index: 2147483647;\n\n --font-family: \"Inter Variable\", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n --font-size-base: 12px;\n --font-size-small: 10px;\n --font-weight-base: 400;\n --font-weight-label: 600;\n\n /* Control semantic sizes */\n --control-height-small: 24px;\n --control-height-large: 32px;\n --control-height-xlarge: 40px;\n --label-column: 104px;\n\n --icon-size-small: 16px;\n --icon-size-large: 20px;\n --icon-stroke-width: 1.5px;\n\n position: fixed;\n inset: 0;\n z-index: var(--z-index);\n pointer-events: none;\n font-family: var(--font-family);\n background: transparent;\n box-sizing: border-box;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nbutton,\ninput,\nselect {\n font-family: inherit;\n}\n";
27
33
  import { useInspectorOpen } from "../shell/openStore.js";
28
34
  import { isEditableEvent } from "../shell/shortcuts.js";
29
35
  import { acknowledgeAgentRendererReady } from "./agentPresentation.js";
30
36
  import { useInspectorSession } from "../session/sessionContext.js";
37
+ import { isNudgeUiFeatureEnabled } from "../runtime/runtimeConfig.js";
31
38
  import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
32
39
  import { subscribeCanvasRendererMessages } from "./rendererMessageRouter.js";
33
40
  import { adjacentCanvasZoomLevel, CanvasToolbar, } from "./CanvasToolbar.js";
34
- import { canvasToolForEvent, canvasToolForCode } from "./keyboardShortcuts.js";
35
- const canvasToolbarStyles = ".canvas-toolbar {\n position: absolute;\n left: 50%;\n bottom: var(--space-20);\n /* Keep the persistent controls above the live sketch surface. The sketch\n toolbar gets the next layer so both toolbars remain available together. */\n z-index: calc(var(--z-index) - 2);\n display: flex;\n align-items: center;\n gap: var(--space-4);\n padding: var(--space-4);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n transform: translateX(-50%);\n pointer-events: auto;\n}\n\n/* The live sketch surface is a separate stacking context. Keep a copy of the\n toolbar at the root layer while sketching so pointer events reach the\n controls instead of the full-screen SVG drawing surface. */\n.canvas-toolbar__portal {\n position: fixed;\n inset: 0;\n z-index: var(--z-index);\n pointer-events: none;\n}\n\n.canvas-toolbar__portal > .canvas-toolbar {\n pointer-events: auto;\n}\n\n.canvas-toolbar__tools {\n display: flex;\n align-items: center;\n gap: var(--space-4);\n}\n\n.canvas-toolbar__divider {\n flex-shrink: 0;\n width: 1px;\n height: var(--control-height-small);\n margin-inline: var(--space-4);\n background: var(--border-subtle);\n}\n\n.canvas-toolbar__tool[data-active=\"true\"] {\n background: var(--accent-surface);\n color: var(--accent);\n}\n";
41
+ import { APP_INTERACTION_KEY, canvasToolForEvent, canvasToolForCode } from "./keyboardShortcuts.js";
42
+ const canvasToolbarStyles = ".canvas-toolbar {\n position: absolute;\n left: 50%;\n bottom: var(--space-20);\n /* Keep the persistent controls above the live sketch surface. The sketch\n toolbar gets the next layer so both toolbars remain available together. */\n z-index: calc(var(--z-index) - 2);\n display: flex;\n align-items: center;\n gap: var(--space-4);\n padding: var(--space-4);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n transform: translateX(-50%);\n pointer-events: auto;\n}\n\n/* The live sketch surface is a separate stacking context. Keep a copy of the\n toolbar at the root layer while sketching so pointer events reach the\n controls instead of the full-screen SVG drawing surface. */\n.canvas-toolbar__portal {\n position: fixed;\n inset: 0;\n z-index: var(--z-index);\n pointer-events: none;\n}\n\n.canvas-toolbar__portal > .canvas-toolbar {\n pointer-events: auto;\n}\n\n.canvas-toolbar__tools {\n display: flex;\n align-items: center;\n gap: var(--space-4);\n}\n\n.canvas-toolbar__divider {\n flex-shrink: 0;\n width: 1px;\n height: var(--control-height-small);\n margin-inline: 0;\n background: var(--border-subtle);\n}\n\n.canvas-toolbar__tool[data-active=\"true\"] {\n background: var(--accent-surface);\n color: var(--accent);\n}\n";
36
43
  import { getActiveCanvasFrame } from "./activeCanvasDocument.js";
37
44
  import { startSketchCapture } from "../sketch/SketchWorkspace.js";
38
45
  import { cancelSketchInteraction, useSketchInteractionActive } from "../sketch/interaction.js";
39
46
  import { isNudgeUiDev } from "../runtime/devFlag.js";
47
+ import { createCommentCursor, DESIGN_SELECT_CURSOR, PAN_CURSOR } from "../ui/customCursors.js";
48
+ import { addLinkedFrame, createIteration as createWorkspaceIteration, removeFrame } from "../workspace/commands.js";
49
+ import { getSelectedElements, setSelectedElement } from "../selection/selectionStore.js";
50
+ import { setCommentToolActive } from "../comments/store.js";
40
51
  const WORKSPACE_STYLES = [foundationStyles, canvasWorkspaceStyles, canvasCardStyles, canvasToolbarStyles].join("\n");
41
52
  const PRESENTATION_TRANSITION_MS = 200;
42
53
  function snapshotRect(rect) {
@@ -60,8 +71,10 @@ export function CanvasWorkspace({ primaryUrl }) {
60
71
  const focusedCardId = useFocusedCardId();
61
72
  const sketchActive = useSketchInteractionActive();
62
73
  const sketchEnabled = isNudgeUiDev() && runtimeConfig.demo !== true;
74
+ const canvasIterationsEnabled = isNudgeUiFeatureEnabled(runtimeConfig.features, "canvasIterations");
63
75
  const sketchAvailable = sketchEnabled && getActiveCanvasFrame() !== null;
64
76
  const boardRef = useRef(null);
77
+ useCanvasLayoutAnimation(boardRef, cards, camera, presentation);
65
78
  const panningRef = useRef(false);
66
79
  const panOriginRef = useRef({ x: 0, y: 0 });
67
80
  const cameraAtPanStartRef = useRef({ x: 0, y: 0 });
@@ -71,81 +84,75 @@ export function CanvasWorkspace({ primaryUrl }) {
71
84
  const activatedTargetRef = useRef(null);
72
85
  const demoSeededRef = useRef(false);
73
86
  const previousPresentationRef = useRef(presentation);
74
- const focusCardRectRef = useRef(null);
75
- const canvasEntryAnimationRef = useRef(null);
87
+ const presentationAnimationRef = useRef(null);
76
88
  const [boardCursorClass, setBoardCursorClass] = useState("");
77
- const [interactionTool, setInteractionTool] = useState("design");
78
- const rendererInteractionsEnabled = interactionTool !== "select";
89
+ const [selectedTool, setInteractionTool] = useState("design");
90
+ const temporaryAppInteraction = useTemporaryAppInteraction();
91
+ const interactionTool = temporaryAppInteraction ? "select" : selectedTool;
92
+ const layoutTransitioning = useCanvasLayoutTransitioning();
93
+ const rendererInteractionsEnabled = interactionTool !== "select" && !layoutTransitioning;
94
+ const rendererCursor = interactionTool === "comment" ? "comment" : "design";
95
+ const boardCursor = boardCursorClass === "is-grabbing"
96
+ ? "grabbing"
97
+ : boardCursorClass !== ""
98
+ ? PAN_CURSOR
99
+ : interactionTool === "comment"
100
+ ? createCommentCursor(boardRef.current)
101
+ : interactionTool === "design"
102
+ ? DESIGN_SELECT_CURSOR
103
+ : undefined;
79
104
  const linksOpenInCards = interactionTool === "select" && presentation === "canvas";
80
105
  const presentationCardId = selectedCardId ?? focusedCardId ?? cards[0]?.id ?? null;
106
+ const linkedFrameIds = getLinkedFrameIds(cards, selectedCardId);
81
107
  useLayoutEffect(() => {
108
+ const previousPresentation = previousPresentationRef.current;
109
+ previousPresentationRef.current = presentation;
110
+ if (previousPresentation === presentation)
111
+ return;
112
+ presentationAnimationRef.current?.cancel();
113
+ presentationAnimationRef.current = null;
114
+ if (!presentationTransitioning)
115
+ return;
82
116
  const board = boardRef.current;
117
+ const card = cards.find((candidate) => candidate.id === presentationCardId);
83
118
  const cardElement = presentationCardId
84
119
  ? board?.querySelector(`[data-card-id="${presentationCardId}"]`)
85
120
  : null;
86
- const boardContent = board?.querySelector('[data-test="canvas-board-content"]');
87
- if (presentation === "focus") {
88
- canvasEntryAnimationRef.current?.cancel();
89
- canvasEntryAnimationRef.current = null;
90
- if (boardContent)
91
- boardContent.style.transition = "";
92
- if (cardElement)
93
- cardElement.style.transition = "";
94
- focusCardRectRef.current = cardElement ? snapshotRect(cardElement.getBoundingClientRect()) : null;
95
- }
96
- else if (presentation === "canvas"
97
- && previousPresentationRef.current === "focus"
98
- && presentationTransitioning
99
- && cardElement
100
- && boardContent
101
- && focusCardRectRef.current
102
- && typeof cardElement.animate === "function") {
103
- // Commit the final layout before measuring it. The old implementation
104
- // transitioned both the camera and the card's position, which made the
105
- // nested scale and translation produce a sideways drift.
106
- boardContent.style.transition = "none";
107
- cardElement.style.transition = "none";
108
- const oldRect = focusCardRectRef.current;
109
- const newRect = snapshotRect(cardElement.getBoundingClientRect());
110
- if (newRect.width > 0 && newRect.height > 0 && camera.zoom > 0) {
111
- const oldCenterX = oldRect.left + oldRect.width / 2;
112
- const oldCenterY = oldRect.top + oldRect.height / 2;
113
- const newCenterX = newRect.left + newRect.width / 2;
114
- const newCenterY = newRect.top + newRect.height / 2;
115
- const localTranslateX = (oldCenterX - newCenterX) / camera.zoom;
116
- const localTranslateY = (oldCenterY - newCenterY) / camera.zoom;
117
- const scaleX = oldRect.width / newRect.width;
118
- const scaleY = oldRect.height / newRect.height;
119
- const animation = cardElement.animate([
120
- {
121
- transformOrigin: "center center",
122
- transform: `translate(${localTranslateX}px, ${localTranslateY}px) scale(${scaleX}, ${scaleY})`,
123
- },
124
- { transformOrigin: "center center", transform: "none" },
125
- ], { duration: PRESENTATION_TRANSITION_MS, easing: "ease", fill: "both" });
126
- canvasEntryAnimationRef.current = animation;
127
- void animation.finished.then(() => {
128
- if (canvasEntryAnimationRef.current !== animation)
129
- return;
130
- canvasEntryAnimationRef.current = null;
131
- animation.cancel();
132
- boardContent.style.transition = "";
133
- cardElement.style.transition = "";
134
- }).catch(() => {
135
- if (canvasEntryAnimationRef.current !== animation)
136
- return;
137
- canvasEntryAnimationRef.current = null;
138
- boardContent.style.transition = "";
139
- cardElement.style.transition = "";
140
- });
141
- }
142
- else {
143
- boardContent.style.transition = "";
144
- cardElement.style.transition = "";
145
- }
146
- }
147
- previousPresentationRef.current = presentation;
148
- }, [camera.zoom, presentation, presentationCardId, presentationTransitioning]);
121
+ if (!board || !card || !cardElement || typeof cardElement.animate !== "function" || camera.zoom <= 0)
122
+ return;
123
+ // The previous layout is already gone, so derive it: focus fills the
124
+ // board, and canvas places the card through the camera.
125
+ const boardRect = snapshotRect(board.getBoundingClientRect());
126
+ const canvasRect = {
127
+ left: boardRect.left + camera.x + card.x * camera.zoom,
128
+ top: boardRect.top + camera.y + card.y * camera.zoom,
129
+ width: card.width * camera.zoom,
130
+ height: card.height * camera.zoom,
131
+ };
132
+ const oldRect = presentation === "focus" ? canvasRect : boardRect;
133
+ const localScale = presentation === "focus" ? 1 : camera.zoom;
134
+ const newRect = snapshotRect(cardElement.getBoundingClientRect());
135
+ if (newRect.width <= 0 || newRect.height <= 0)
136
+ return;
137
+ const localTranslateX = (oldRect.left + oldRect.width / 2 - (newRect.left + newRect.width / 2)) / localScale;
138
+ const localTranslateY = (oldRect.top + oldRect.height / 2 - (newRect.top + newRect.height / 2)) / localScale;
139
+ const scaleX = oldRect.width / newRect.width;
140
+ const scaleY = oldRect.height / newRect.height;
141
+ const animation = cardElement.animate([
142
+ {
143
+ transformOrigin: "center center",
144
+ transform: `translate(${localTranslateX}px, ${localTranslateY}px) scale(${scaleX}, ${scaleY})`,
145
+ },
146
+ { transformOrigin: "center center", transform: "none" },
147
+ ], { duration: PRESENTATION_TRANSITION_MS, easing: "ease", fill: "both" });
148
+ presentationAnimationRef.current = animation;
149
+ void animation.finished.then(() => {
150
+ if (presentationAnimationRef.current !== animation)
151
+ return;
152
+ presentationAnimationRef.current = null;
153
+ animation.cancel();
154
+ }).catch(() => undefined);
155
+ }, [camera, cards, presentation, presentationCardId, presentationTransitioning]);
149
156
  useLayoutEffect(() => {
150
157
  const board = boardRef.current;
151
158
  if (!primaryUrl || !board)
@@ -157,7 +164,16 @@ export function CanvasWorkspace({ primaryUrl }) {
157
164
  width: board.clientWidth,
158
165
  height: board.clientHeight,
159
166
  };
160
- const primaryCard = activateIframeWorkspace(primaryUrl, viewport);
167
+ const primaryCard = activateIframeWorkspace(primaryUrl, viewport, {
168
+ replaceActiveCard: presentation === "focus" && runtimeConfig.demo !== true,
169
+ preserveIterationFocus: true,
170
+ });
171
+ if (primaryCard && cards.length === 0 && runtimeConfig.demo !== true) {
172
+ setBoardCamera({
173
+ x: 40, y: 120,
174
+ zoom: Math.max(0.25, Math.min(0.9, (viewport.width - 80) / primaryCard.width, (viewport.height - 160) / primaryCard.height)),
175
+ });
176
+ }
161
177
  if (runtimeConfig.demo === true && primaryCard && !demoSeededRef.current) {
162
178
  demoSeededRef.current = true;
163
179
  const demoPages = runtimeConfig.demoPages ?? [];
@@ -201,7 +217,7 @@ export function CanvasWorkspace({ primaryUrl }) {
201
217
  }
202
218
  focusCard(primaryCard.id);
203
219
  }
204
- }, [primaryUrl, cards.length, runtimeConfig.demo, runtimeConfig.demoPages, runtimeConfig.demoCardLabels]);
220
+ }, [primaryUrl, cards.length, presentation, runtimeConfig.demo, runtimeConfig.demoPages, runtimeConfig.demoCardLabels]);
205
221
  useEffect(() => {
206
222
  if (primaryUrl && activatedTargetRef.current !== primaryUrl)
207
223
  return;
@@ -209,7 +225,7 @@ export function CanvasWorkspace({ primaryUrl }) {
209
225
  const activeCard = cards.find((card) => card.id === activeCardId);
210
226
  if (!activeCard)
211
227
  return;
212
- const editorUrl = createNudgeUiEditorUrl(activeCard.url);
228
+ const editorUrl = createNudgeUiEditorUrl(contentSourceUrl(activeCard.content));
213
229
  if (window.location.href !== editorUrl) {
214
230
  window.history.replaceState(window.history.state, "", editorUrl);
215
231
  }
@@ -222,9 +238,10 @@ export function CanvasWorkspace({ primaryUrl }) {
222
238
  workspaceId: WORKSPACE_ID,
223
239
  cardId,
224
240
  spaceHeld,
241
+ cursor: spaceHeld ? "drag" : rendererCursor,
225
242
  }, window.location.origin);
226
- }, []);
227
- const sendInspectorInteractionState = useCallback((iframe, cardId, open, interactionsEnabled) => {
243
+ }, [rendererCursor]);
244
+ const sendInspectorInteractionState = useCallback((iframe, cardId, open, interactionsEnabled, cursor) => {
228
245
  iframe.contentWindow?.postMessage({
229
246
  type: "inspector-interaction-state",
230
247
  protocolVersion: PROTOCOL_VERSION,
@@ -233,6 +250,8 @@ export function CanvasWorkspace({ primaryUrl }) {
233
250
  cardId,
234
251
  open,
235
252
  interactionsEnabled,
253
+ cursor,
254
+ commentCursor: cursor === "comment" ? createCommentCursor(boardRef.current) : undefined,
236
255
  }, window.location.origin);
237
256
  }, []);
238
257
  const sendLinkTargetState = useCallback((iframe, cardId, openInCard) => {
@@ -253,15 +272,16 @@ export function CanvasWorkspace({ primaryUrl }) {
253
272
  workspaceId: WORKSPACE_ID,
254
273
  cardId,
255
274
  enabled,
275
+ panScroll: enabled && interactionTool !== "select",
256
276
  }, window.location.origin);
257
- }, []);
277
+ }, [interactionTool]);
258
278
  useEffect(() => {
259
279
  for (const [cardId, iframe] of getRegisteredFrames()) {
260
- sendInspectorInteractionState(iframe, cardId, inspectorOpen, rendererInteractionsEnabled);
280
+ sendInspectorInteractionState(iframe, cardId, inspectorOpen, rendererInteractionsEnabled, rendererCursor);
261
281
  sendBoardGestureState(iframe, cardId, presentation === "canvas");
262
282
  sendLinkTargetState(iframe, cardId, linksOpenInCards);
263
283
  }
264
- }, [inspectorOpen, linksOpenInCards, presentation, rendererInteractionsEnabled, sendBoardGestureState, sendInspectorInteractionState, sendLinkTargetState]);
284
+ }, [inspectorOpen, linksOpenInCards, presentation, rendererCursor, rendererInteractionsEnabled, sendBoardGestureState, sendInspectorInteractionState, sendLinkTargetState]);
265
285
  const broadcastPanModifier = useCallback((spaceHeld) => {
266
286
  for (const [cardId, iframe] of getRegisteredFrames()) {
267
287
  sendPanModifier(iframe, cardId, spaceHeld);
@@ -273,6 +293,12 @@ export function CanvasWorkspace({ primaryUrl }) {
273
293
  return;
274
294
  setBoardCamera(zoomCameraAtPointer(getBoardCamera(), point, board.getBoundingClientRect(), deltaY));
275
295
  }, []);
296
+ const panCanvasByWheel = useCallback((deltaX, deltaY) => {
297
+ if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY))
298
+ return;
299
+ const camera = getBoardCamera();
300
+ setBoardCamera({ ...camera, x: camera.x - deltaX, y: camera.y - deltaY });
301
+ }, []);
276
302
  const fitCanvasToBoard = useCallback(() => {
277
303
  const board = boardRef.current;
278
304
  fitAllCards(board ? { width: board.clientWidth, height: board.clientHeight } : undefined);
@@ -310,6 +336,14 @@ export function CanvasWorkspace({ primaryUrl }) {
310
336
  setBoardCursorClass("");
311
337
  }, [broadcastPanModifier, endPanning]);
312
338
  const handleToolChange = useCallback((nextTool) => {
339
+ if (nextTool === "select" && selectedTool === "select") {
340
+ setInteractionTool("design");
341
+ stopPanMode();
342
+ return;
343
+ }
344
+ if (nextTool === "select") {
345
+ setSelectedElement(null);
346
+ }
313
347
  if (nextTool === "sketch") {
314
348
  if (!sketchEnabled)
315
349
  return;
@@ -317,27 +351,52 @@ export function CanvasWorkspace({ primaryUrl }) {
317
351
  if (!hostElement)
318
352
  return;
319
353
  stopPanMode();
354
+ setCommentToolActive(false);
320
355
  setInteractionTool(nextTool);
321
356
  startSketchCapture(hostElement);
322
357
  return;
323
358
  }
324
359
  if (sketchActive)
325
360
  cancelSketchInteraction();
326
- if (nextTool === "pan") {
327
- if (presentation === "focus") {
328
- const board = boardRef.current;
329
- setCanvasPresentation("canvas", board ? { width: board.clientWidth, height: board.clientHeight } : undefined);
330
- }
331
- setInteractionTool(nextTool);
332
- spaceHeldRef.current = true;
333
- broadcastPanModifier(true);
334
- if (!panningRef.current)
335
- setBoardCursorClass("is-grabbable");
336
- return;
337
- }
361
+ setCommentToolActive(nextTool === "comment");
338
362
  setInteractionTool(nextTool);
339
363
  stopPanMode();
340
- }, [broadcastPanModifier, presentation, sketchActive, sketchEnabled, stopPanMode]);
364
+ }, [selectedTool, sketchActive, sketchEnabled, stopPanMode]);
365
+ useEffect(() => () => setCommentToolActive(false), []);
366
+ useEffect(() => {
367
+ setCommentToolActive(interactionTool === "comment");
368
+ if (temporaryAppInteraction)
369
+ stopPanMode();
370
+ }, [interactionTool, temporaryAppInteraction, stopPanMode]);
371
+ useEffect(() => {
372
+ const onKeyDown = (event) => {
373
+ if (mode === "canvas" && event.code === APP_INTERACTION_KEY && !event.repeat && !isEditableEvent(event))
374
+ setTemporaryAppInteraction(true);
375
+ };
376
+ const onKeyUp = (event) => {
377
+ if (event.code === APP_INTERACTION_KEY)
378
+ setTemporaryAppInteraction(false);
379
+ };
380
+ const onBlur = () => {
381
+ if (!document.hasFocus())
382
+ setTemporaryAppInteraction(false);
383
+ };
384
+ const onVisibility = () => {
385
+ if (document.visibilityState === "hidden")
386
+ setTemporaryAppInteraction(false);
387
+ };
388
+ window.addEventListener("keydown", onKeyDown, true);
389
+ window.addEventListener("keyup", onKeyUp, true);
390
+ window.addEventListener("blur", onBlur);
391
+ document.addEventListener("visibilitychange", onVisibility);
392
+ return () => {
393
+ window.removeEventListener("keydown", onKeyDown, true);
394
+ window.removeEventListener("keyup", onKeyUp, true);
395
+ window.removeEventListener("blur", onBlur);
396
+ document.removeEventListener("visibilitychange", onVisibility);
397
+ setTemporaryAppInteraction(false);
398
+ };
399
+ }, [mode]);
341
400
  useEffect(() => {
342
401
  if (mode !== "canvas")
343
402
  return;
@@ -356,8 +415,21 @@ export function CanvasWorkspace({ primaryUrl }) {
356
415
  const handleZoomStep = useCallback((direction) => {
357
416
  if (presentation === "focus") {
358
417
  if (direction < 0) {
359
- const board = boardRef.current;
360
- setCanvasPresentation("canvas", board ? { width: board.clientWidth, height: board.clientHeight } : undefined);
418
+ batchCanvasChanges(() => {
419
+ const board = boardRef.current;
420
+ const cards = getCanvasCards();
421
+ const activeId = getSelectedCardId() ?? getFocusedCardId();
422
+ const card = cards.find((candidate) => candidate.id === activeId) ?? cards[0];
423
+ if (board && card) {
424
+ const zoom = 0.9;
425
+ setBoardCamera({
426
+ x: board.clientWidth / 2 - (card.x + card.width / 2) * zoom,
427
+ y: board.clientHeight / 2 - (card.y + card.height / 2) * zoom,
428
+ zoom,
429
+ });
430
+ }
431
+ setCanvasPresentation("canvas");
432
+ });
361
433
  }
362
434
  return;
363
435
  }
@@ -373,6 +445,23 @@ export function CanvasWorkspace({ primaryUrl }) {
373
445
  focusCard(cardId);
374
446
  setCanvasPresentation("focus");
375
447
  }, []);
448
+ const duplicateDesign = useCallback((cardId) => {
449
+ const duplicate = addLinkedFrame(cardId);
450
+ if (!duplicate)
451
+ return;
452
+ setSelectedElement(null);
453
+ setCanvasPresentation("canvas");
454
+ const board = boardRef.current;
455
+ focusCanvasCards([duplicate.id], board ? { width: board.clientWidth, height: board.clientHeight } : undefined);
456
+ }, []);
457
+ const createIteration = useCallback(async (cardId, position) => {
458
+ const iteration = await createWorkspaceIteration(cardId, position);
459
+ setSelectedElement(null);
460
+ const board = boardRef.current;
461
+ if (board && !position)
462
+ focusCanvasCards([iteration.id], { width: board.clientWidth, height: board.clientHeight });
463
+ return iteration.id;
464
+ }, []);
376
465
  useEffect(() => {
377
466
  if (!sketchActive && interactionTool === "sketch") {
378
467
  setInteractionTool("design");
@@ -402,6 +491,11 @@ export function CanvasWorkspace({ primaryUrl }) {
402
491
  return subscribeCanvasRendererMessages(({ cardId, iframe, identity, message: msg }) => {
403
492
  if (msg.type === "renderer-hello")
404
493
  return;
494
+ if (msg.type === "app-interaction-modifier") {
495
+ if (mode === "canvas")
496
+ setTemporaryAppInteraction(msg.held === true);
497
+ return;
498
+ }
405
499
  if (isKeyboardShortcutMessage(msg, identity)) {
406
500
  if (msg.phase === "keydown") {
407
501
  const nextTool = canvasToolForCode(msg.code);
@@ -412,11 +506,14 @@ export function CanvasWorkspace({ primaryUrl }) {
412
506
  }
413
507
  const frame = { cardId, iframe };
414
508
  const syncActiveFrameUrl = (url) => {
415
- updateCardUrl(frame.cardId, url);
416
- const activeCardId = getSelectedCardId() ?? getFocusedCardId() ?? getCanvasCards()[0]?.id;
417
- if (activeCardId !== frame.cardId && getCanvasCards().length !== 1)
509
+ if (getCanvasCards().some((card) => card.id === frame.cardId && iterationId(card.content)))
418
510
  return;
419
- window.history.replaceState(window.history.state, "", createNudgeUiEditorUrl(url));
511
+ const activeCardId = getSelectedCardId() ?? getFocusedCardId() ?? getCanvasCards()[0]?.id;
512
+ if (activeCardId === frame.cardId || getCanvasCards().length === 1) {
513
+ activatedTargetRef.current = url;
514
+ window.history.replaceState(window.history.state, "", createNudgeUiEditorUrl(url));
515
+ }
516
+ updateCardUrl(frame.cardId, url);
420
517
  };
421
518
  if (isProjectionAppliedMessage(msg, identity)) {
422
519
  recordCanvasProjectionApplied(frame.cardId, msg.revision);
@@ -425,10 +522,12 @@ export function CanvasWorkspace({ primaryUrl }) {
425
522
  if (isOriginalPreviewActive())
426
523
  return;
427
524
  const frameDocument = frame.iframe.contentDocument;
428
- if (frameDocument && isCanvasCanonicalProjectionRevisionCurrent(frameDocument, msg.revision)) {
429
- const attempt = beginPreviewAttempt(getCanvasPreviewDocument(frame.cardId), getWorkspaceChanges().revision);
525
+ const plan = getCanonicalFrameProjection(frame.cardId, msg.revision);
526
+ const draft = getDraftForCard(frame.cardId);
527
+ if (frameDocument && plan && draft && isCanvasCanonicalProjectionRevisionCurrent(frameDocument, msg.revision)) {
528
+ const attempt = beginPreviewAttempt(getCanvasPreviewDocument(frame.cardId), plan.draftRevision);
430
529
  if (attempt) {
431
- for (const change of getChangesList()) {
530
+ for (const change of draft.contents.changes) {
432
531
  if (!isPreviewableChange(change))
433
532
  continue;
434
533
  const result = verifyManagedStyleProjection(change, null, frameDocument);
@@ -450,15 +549,21 @@ export function CanvasWorkspace({ primaryUrl }) {
450
549
  return;
451
550
  }
452
551
  if (isStructuralProjectionReportMessage(msg, identity)) {
453
- recordCanvasStructuralProjectionReports(frame.cardId, msg.revision, msg.reports);
552
+ const plan = getCanonicalFrameProjection(frame.cardId, msg.revision);
553
+ if (plan)
554
+ recordCanvasStructuralProjectionReports(frame.cardId, msg.revision, msg.reports, plan);
454
555
  return;
455
556
  }
456
557
  if (isRenderedInstanceProjectionReportMessage(msg, identity)) {
457
- recordCanvasRenderedInstanceProjectionReports(frame.cardId, msg.revision, msg.reports);
558
+ const plan = getCanonicalFrameProjection(frame.cardId, msg.revision);
559
+ if (plan)
560
+ recordCanvasRenderedInstanceProjectionReports(frame.cardId, msg.revision, msg.reports, plan);
458
561
  return;
459
562
  }
460
563
  if (isTextProjectionReportMessage(msg, identity)) {
461
- recordCanvasTextProjectionReports(frame.cardId, msg.revision, msg.reports);
564
+ const plan = getCanonicalFrameProjection(frame.cardId, msg.revision);
565
+ if (plan)
566
+ recordCanvasTextProjectionReports(frame.cardId, msg.revision, msg.reports, plan);
462
567
  return;
463
568
  }
464
569
  if (msg.type === "frame-ready") {
@@ -467,7 +572,7 @@ export function CanvasWorkspace({ primaryUrl }) {
467
572
  // A card can finish loading after Space was pressed on the controller.
468
573
  // Seed it with the current modifier state before its first pointer event.
469
574
  sendPanModifier(frame.iframe, frame.cardId, spaceHeldRef.current);
470
- sendInspectorInteractionState(frame.iframe, frame.cardId, inspectorOpen, rendererInteractionsEnabled);
575
+ sendInspectorInteractionState(frame.iframe, frame.cardId, inspectorOpen, rendererInteractionsEnabled, spaceHeldRef.current ? "drag" : rendererCursor);
471
576
  sendBoardGestureState(frame.iframe, frame.cardId, presentation === "canvas");
472
577
  sendLinkTargetState(frame.iframe, frame.cardId, linksOpenInCards);
473
578
  }
@@ -481,6 +586,11 @@ export function CanvasWorkspace({ primaryUrl }) {
481
586
  broadcastPanModifier(msg.spaceHeld);
482
587
  return;
483
588
  }
589
+ if (msg.type === "wheel-pan") {
590
+ if (presentation === "canvas" && interactionTool !== "select")
591
+ panCanvasByWheel(msg.deltaX, msg.deltaY);
592
+ return;
593
+ }
484
594
  if (msg.type === "zoom") {
485
595
  if (presentation !== "canvas")
486
596
  return;
@@ -531,19 +641,33 @@ export function CanvasWorkspace({ primaryUrl }) {
531
641
  }
532
642
  if (msg.type !== "navigation-intent")
533
643
  return;
644
+ if (getCanvasCards().some((card) => card.id === frame.cardId && iterationId(card.content)))
645
+ return;
534
646
  const normalized = normalizeUrl(msg.url);
535
647
  if (!normalized || normalized.origin !== window.location.origin)
536
648
  return;
537
- if (msg.openInCard === true) {
538
- const card = findCardByNormalizedUrl(normalized) ?? addCanvasCard(msg.url);
649
+ if (msg.openInCard === true && presentation === "canvas") {
650
+ const card = getCanvasCards().find((candidate) => {
651
+ const route = !iterationId(candidate.content) && normalizeUrl(contentSourceUrl(candidate.content));
652
+ return route && normalizedUrlKey(route) === normalizedUrlKey(normalized);
653
+ }) ?? addCanvasCard(msg.url);
539
654
  selectCard(card.id);
655
+ const board = boardRef.current;
656
+ if (board)
657
+ focusCanvasCards([card.id], { width: board.clientWidth, height: board.clientHeight });
658
+ return;
659
+ }
660
+ if (msg.openInCard === true) {
661
+ // A delayed renderer message must not create a new frame after entering focus mode.
662
+ selectCard(frame.cardId);
663
+ activateIframeWorkspace(msg.url, { width: boardRef.current?.clientWidth ?? window.innerWidth, height: boardRef.current?.clientHeight ?? window.innerHeight }, { replaceActiveCard: true });
540
664
  return;
541
665
  }
542
666
  const frameDocument = frame.iframe.contentDocument;
543
667
  if (frameDocument)
544
668
  disposeInlineTextEdit("route-disposed", frameDocument);
545
669
  });
546
- }, [broadcastPanModifier, endPanning, handleToolChange, inspectorOpen, linksOpenInCards, movePanning, presentation, rendererInteractionsEnabled, sendBoardGestureState, sendInspectorInteractionState, sendLinkTargetState, sendPanModifier, sketchAvailable, startPanning, zoomAtPointer]);
670
+ }, [broadcastPanModifier, endPanning, handleToolChange, interactionTool, mode, panCanvasByWheel, inspectorOpen, linksOpenInCards, movePanning, presentation, rendererCursor, rendererInteractionsEnabled, sendBoardGestureState, sendInspectorInteractionState, sendLinkTargetState, sendPanModifier, sketchAvailable, startPanning, zoomAtPointer]);
547
671
  useEffect(() => {
548
672
  if (mode === "canvas" && !hasFitAllRan()) {
549
673
  if (!fitAllScheduledRef.current) {
@@ -557,14 +681,16 @@ export function CanvasWorkspace({ primaryUrl }) {
557
681
  }, [mode, cards.length, inspectorOpen, fitCanvasToBoard]);
558
682
  useEffect(() => {
559
683
  function onKeyDown(e) {
560
- if (mode !== "canvas" || presentation !== "canvas" || sketchActive)
684
+ if (mode !== "canvas" || presentation !== "canvas" || sketchActive || interactionTool === "select")
561
685
  return;
562
686
  if ((e.key === "Delete" || e.key === "Backspace") && !isEditableEvent(e)) {
687
+ if (e.defaultPrevented || getSelectedElements().length > 0 || getCanvasCards().length < 2)
688
+ return;
563
689
  const selectedCardId = getSelectedCardId();
564
690
  if (!selectedCardId)
565
691
  return;
566
692
  e.preventDefault();
567
- removeCanvasCard(selectedCardId);
693
+ removeFrame(selectedCardId);
568
694
  return;
569
695
  }
570
696
  if (e.code === "Space" && !e.repeat) {
@@ -580,7 +706,7 @@ export function CanvasWorkspace({ primaryUrl }) {
580
706
  }
581
707
  function onKeyUp(e) {
582
708
  if (e.code === "Space") {
583
- spaceHeldRef.current = interactionTool === "pan";
709
+ spaceHeldRef.current = false;
584
710
  broadcastPanModifier(spaceHeldRef.current);
585
711
  if (!panningRef.current) {
586
712
  setBoardCursorClass(spaceHeldRef.current ? "is-grabbable" : "");
@@ -594,6 +720,15 @@ export function CanvasWorkspace({ primaryUrl }) {
594
720
  window.removeEventListener("keyup", onKeyUp, true);
595
721
  };
596
722
  }, [broadcastPanModifier, interactionTool, mode, presentation, sketchActive]);
723
+ const handleBoardClick = useCallback((event) => {
724
+ if (presentation !== "canvas" || sketchActive || spaceHeldRef.current || panningRef.current)
725
+ return;
726
+ if (event.target === event.currentTarget
727
+ || (event.target instanceof HTMLElement && event.target.dataset.test === "canvas-board-content")) {
728
+ setSelectedElement(null);
729
+ deselectCard();
730
+ }
731
+ }, [presentation, sketchActive]);
597
732
  const handleBoardPointerDown = useCallback((e) => {
598
733
  if (presentation !== "canvas" || sketchActive)
599
734
  return;
@@ -618,22 +753,35 @@ export function CanvasWorkspace({ primaryUrl }) {
618
753
  window.addEventListener("pointerup", onUp);
619
754
  }, [endPanning, movePanning, presentation, sketchActive, startPanning]);
620
755
  useEffect(() => {
621
- if (mode !== "canvas" || presentation !== "canvas" || sketchActive)
756
+ if (mode !== "canvas" || presentation !== "canvas")
622
757
  return;
623
758
  function handleGlobalWheel(e) {
624
- if (!e.ctrlKey && !e.metaKey)
759
+ const zoom = e.ctrlKey || e.metaKey;
760
+ const board = boardRef.current;
761
+ if (zoom && sketchActive)
762
+ return;
763
+ const path = e.composedPath();
764
+ // Comment pins overlay the board without being its DOM descendants.
765
+ const overCommentPin = path.some((node) => node instanceof Element && node.matches(".comment-pin"));
766
+ if (!zoom && (interactionTool === "select" || !board || (!path.includes(board) && !overCommentPin)))
625
767
  return;
626
768
  e.preventDefault();
627
769
  e.stopPropagation();
628
- zoomAtPointer({ x: e.clientX, y: e.clientY }, e.deltaY);
770
+ if (zoom)
771
+ zoomAtPointer({ x: e.clientX, y: e.clientY }, e.deltaY);
772
+ else if (board) {
773
+ const delta = wheelPanDelta(e, { width: board.clientWidth, height: board.clientHeight });
774
+ panCanvasByWheel(delta.x, delta.y);
775
+ }
629
776
  }
630
777
  window.addEventListener("wheel", handleGlobalWheel, { capture: true, passive: false });
631
778
  return () => window.removeEventListener("wheel", handleGlobalWheel, { capture: true });
632
- }, [mode, presentation, sketchActive, zoomAtPointer]);
779
+ }, [mode, presentation, sketchActive, interactionTool, panCanvasByWheel, zoomAtPointer]);
780
+ const renderCard = (card) => (_jsx(CanvasCard, { card: card, presentation: presentation, presentationCard: card.id === presentationCardId, linkedToActive: linkedFrameIds.includes(card.id), onShowFocus: showFocus, onDuplicate: duplicateDesign, onCreateIteration: canvasIterationsEnabled ? createIteration : undefined, documentOwner: inspectorSession }, card.id));
633
781
  if (mode !== "canvas")
634
782
  return null;
635
- return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "canvas-styles", children: WORKSPACE_STYLES }), _jsx("div", { className: `canvas-workspace${presentationTransitioning ? " is-presentation-transitioning" : ""}`, "data-test": "canvas-workspace", "data-presentation": presentation, style: { right: inspectorOpen ? "min(320px, 100vw)" : 0 }, children: _jsxs("div", { className: `canvas-workspace__board ${boardCursorClass}`.trim(), "data-test": "canvas-board", ref: boardRef, onPointerDown: handleBoardPointerDown, children: [!primaryUrl ? (_jsx("div", { className: "canvas-workspace__target-error", "data-test": "canvas-target-error", children: "This editor URL does not identify an application page." })) : null, inspectorOpen && presentation === "focus" ? (_jsx("div", { className: "canvas-workspace__focus-action", "data-test": "canvas-show-canvas", children: _jsxs(Button, { variant: "quiet", size: "default", type: "button", onClick: showCanvas, children: [_jsx(IconCornerLeftUp, { size: "var(--icon-size-small)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), "Canvas"] }) })) : null, _jsx("div", { className: "canvas-workspace__board-content", style: {
783
+ return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "canvas-styles", children: WORKSPACE_STYLES }), _jsx("div", { className: `canvas-workspace${presentationTransitioning ? " is-presentation-transitioning" : ""}`, "data-test": "canvas-workspace", "data-presentation": presentation, style: { right: inspectorOpen ? "min(320px, 100vw)" : 0 }, children: _jsxs("div", { className: `canvas-workspace__board ${boardCursorClass}${layoutTransitioning ? " is-layout-transitioning" : ""}`.trim(), "data-test": "canvas-board", ref: boardRef, style: { cursor: boardCursor }, onPointerDown: handleBoardPointerDown, onClick: handleBoardClick, children: [!primaryUrl ? (_jsx("div", { className: "canvas-workspace__target-error", "data-test": "canvas-target-error", children: "This editor URL does not identify an application page." })) : null, inspectorOpen && presentation === "focus" ? (_jsx("div", { className: "canvas-workspace__focus-action", "data-test": "canvas-show-canvas", children: _jsxs(Button, { variant: "quiet", size: "default", type: "button", onClick: showCanvas, children: [_jsx(IconCornerLeftUp, { size: "var(--icon-size-small)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), "Canvas"] }) })) : null, _jsx("div", { className: "canvas-workspace__board-content", style: {
636
784
  transform: `translate(${camera.x}px, ${camera.y}px) scale(${camera.zoom})`,
637
785
  transformOrigin: "0 0",
638
- }, "data-test": "canvas-board-content", children: cards.map((card) => (_jsx(CanvasCard, { card: card, presentation: presentation, presentationCard: card.id === presentationCardId, onShowFocus: showFocus, documentOwner: inspectorSession }, card.id))) }), !sketchActive ? (_jsx(CanvasToolbar, { tool: interactionTool, sketchEnabled: sketchAvailable, onToolChange: handleToolChange })) : null] }) }), sketchActive ? (_jsx("div", { className: "canvas-toolbar__portal", "data-test": "canvas-toolbar-portal", style: { right: inspectorOpen ? "min(320px, 100vw)" : 0 }, children: _jsx(CanvasToolbar, { tool: interactionTool, sketchEnabled: sketchAvailable, onToolChange: handleToolChange }) })) : null] }));
786
+ }, "data-test": "canvas-board-content", children: cards.map(renderCard) }), inspectorOpen && !sketchActive ? (_jsx(CanvasToolbar, { tool: interactionTool, sketchEnabled: sketchAvailable, onToolChange: handleToolChange })) : null] }) }), inspectorOpen && sketchActive ? (_jsx("div", { className: "canvas-toolbar__portal", "data-test": "canvas-toolbar-portal", style: { right: inspectorOpen ? "min(320px, 100vw)" : 0 }, children: _jsx(CanvasToolbar, { tool: interactionTool, sketchEnabled: sketchAvailable, onToolChange: handleToolChange }) })) : null] }));
639
787
  }