nudge-ui 0.2.0 → 0.2.2

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 (231) hide show
  1. package/bin/nudge-ui.mjs +3 -1
  2. package/dist/cli/agentCommand.d.ts +8 -0
  3. package/dist/cli/agentCommand.js +59 -0
  4. package/dist/client.mjs +878 -11
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -1
  6. package/dist/hosts/astro/clientTransport.js +63 -11
  7. package/dist/hosts/astro/integration.d.ts +2 -2
  8. package/dist/hosts/astro/integration.js +4 -3
  9. package/dist/hosts/environment.d.ts +15 -0
  10. package/dist/hosts/environment.js +16 -0
  11. package/dist/hosts/next/sidecar.d.ts +4 -0
  12. package/dist/hosts/next/sidecar.js +56 -7
  13. package/dist/hosts/next/wrapper.d.ts +2 -0
  14. package/dist/hosts/next/wrapper.js +32 -3
  15. package/dist/hosts/projectBridge.d.ts +21 -0
  16. package/dist/hosts/projectBridge.js +98 -0
  17. package/dist/hosts/static/cli.d.ts +1 -1
  18. package/dist/hosts/static/cli.js +6 -1
  19. package/dist/hosts/static/manifest.d.ts +1 -1
  20. package/dist/hosts/static/manifest.js +3 -1
  21. package/dist/hosts/static/server.js +45 -3
  22. package/dist/hosts/vite/vite.d.ts +6 -1
  23. package/dist/hosts/vite/vite.js +82 -15
  24. package/dist/inspector/agent/McpConnectionDialog.d.ts +5 -4
  25. package/dist/inspector/agent/McpConnectionDialog.js +20 -37
  26. package/dist/inspector/agent/client.d.ts +16 -3
  27. package/dist/inspector/agent/client.js +114 -11
  28. package/dist/inspector/agent/connectionStatus.d.ts +3 -3
  29. package/dist/inspector/agent/connectionStatus.js +11 -4
  30. package/dist/inspector/agent/httpTransport.d.ts +8 -0
  31. package/dist/inspector/agent/httpTransport.js +78 -18
  32. package/dist/inspector/agent/protocol.d.ts +11 -3
  33. package/dist/inspector/agent/protocol.js +1 -1
  34. package/dist/inspector/agent/verification.d.ts +1 -1
  35. package/dist/inspector/agent/verification.js +69 -12
  36. package/dist/inspector/canvas/CanvasCard.d.ts +5 -3
  37. package/dist/inspector/canvas/CanvasCard.js +181 -91
  38. package/dist/inspector/canvas/CanvasElementOverlay.js +288 -52
  39. package/dist/inspector/canvas/CanvasToolbar.d.ts +12 -0
  40. package/dist/inspector/canvas/CanvasToolbar.js +24 -0
  41. package/dist/inspector/canvas/CanvasWorkspace.d.ts +4 -1
  42. package/dist/inspector/canvas/CanvasWorkspace.js +410 -72
  43. package/dist/inspector/canvas/activeCanvasDocument.d.ts +5 -0
  44. package/dist/inspector/canvas/activeCanvasDocument.js +23 -0
  45. package/dist/inspector/canvas/canvasResize.d.ts +30 -0
  46. package/dist/inspector/canvas/canvasResize.js +52 -0
  47. package/dist/inspector/canvas/canvasStore.d.ts +30 -6
  48. package/dist/inspector/canvas/canvasStore.js +128 -61
  49. package/dist/inspector/canvas/frameProtocol.d.ts +82 -5
  50. package/dist/inspector/canvas/frameProtocol.js +63 -2
  51. package/dist/inspector/canvas/keyboardShortcuts.d.ts +4 -0
  52. package/dist/inspector/canvas/keyboardShortcuts.js +16 -0
  53. package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
  54. package/dist/inspector/canvas/linkEligibility.js +0 -29
  55. package/dist/inspector/canvas/projection.d.ts +7 -0
  56. package/dist/inspector/canvas/projection.js +118 -25
  57. package/dist/inspector/canvas/rendererBootstrap.js +42 -29
  58. package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
  59. package/dist/inspector/canvas/rendererElementSelector.js +385 -58
  60. package/dist/inspector/canvas/rendererMessageRouter.d.ts +11 -0
  61. package/dist/inspector/canvas/rendererMessageRouter.js +43 -0
  62. package/dist/inspector/canvas/rendererSelectionProxy.js +6 -2
  63. package/dist/inspector/canvas/sessionStore.d.ts +1 -0
  64. package/dist/inspector/canvas/sessionStore.js +12 -4
  65. package/dist/inspector/canvas/staleChangeDetector.js +16 -14
  66. package/dist/inspector/changes/changesLog.js +31 -1
  67. package/dist/inspector/changes/managedStyleProjection.d.ts +1 -1
  68. package/dist/inspector/changes/managedStyleProjection.js +3 -3
  69. package/dist/inspector/changes/structuralTypes.d.ts +4 -0
  70. package/dist/inspector/changes/workspaceChanges.d.ts +4 -0
  71. package/dist/inspector/changes/workspaceChanges.js +29 -3
  72. package/dist/inspector/clientManifest.d.ts +7 -0
  73. package/dist/inspector/clientManifest.js +12 -0
  74. package/dist/inspector/componentSemantics/adapterRegistry.js +4 -3
  75. package/dist/inspector/componentSemantics/runtimeBridge.d.ts +2 -2
  76. package/dist/inspector/componentSemantics/runtimeBridge.js +16 -3
  77. package/dist/inspector/index.d.ts +2 -0
  78. package/dist/inspector/index.js +80 -32
  79. package/dist/inspector/inline-text/inlineTextDiagnostics.d.ts +10 -0
  80. package/dist/inspector/inline-text/inlineTextDiagnostics.js +65 -0
  81. package/dist/inspector/inline-text/inlineTextEditor.d.ts +3 -5
  82. package/dist/inspector/inline-text/inlineTextEditor.js +74 -206
  83. package/dist/inspector/inline-text/inlineTextFeedback.d.ts +5 -0
  84. package/dist/inspector/inline-text/inlineTextFeedback.js +27 -0
  85. package/dist/inspector/inline-text/inlineTextLifecycle.js +2 -0
  86. package/dist/inspector/inline-text/textEditChange.d.ts +14 -0
  87. package/dist/inspector/inline-text/textEditChange.js +165 -0
  88. package/dist/inspector/inspection/selectionProperty.d.ts +1 -0
  89. package/dist/inspector/inspection/selectionProperty.js +1 -0
  90. package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
  91. package/dist/inspector/overlay/DomNavigator.js +40 -0
  92. package/dist/inspector/overlay/InspectorOverlay.js +6 -2
  93. package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
  94. package/dist/inspector/overlay/clickPolicy.js +34 -3
  95. package/dist/inspector/overlay/elementSelector.d.ts +1 -1
  96. package/dist/inspector/overlay/elementSelector.js +18 -7
  97. package/dist/inspector/overlay/selectedGeometry.js +21 -2
  98. package/dist/inspector/overlay/spacingGestures.d.ts +45 -0
  99. package/dist/inspector/overlay/spacingGestures.js +351 -0
  100. package/dist/inspector/overlay/structuralGestures.d.ts +2 -2
  101. package/dist/inspector/overlay/structuralGestures.js +12 -10
  102. package/dist/inspector/projection/managedStylesheet.js +4 -1
  103. package/dist/inspector/projection/renderedInstance.d.ts +2 -0
  104. package/dist/inspector/projection/renderedInstance.js +5 -1
  105. package/dist/inspector/projection/structuralProjection.d.ts +2 -0
  106. package/dist/inspector/projection/structuralProjection.js +85 -1
  107. package/dist/inspector/projection/structuralProjectionBoundary.js +17 -1
  108. package/dist/inspector/projection/textProjection.d.ts +2 -0
  109. package/dist/inspector/projection/textProjection.js +10 -4
  110. package/dist/inspector/prompt/clipboardHandoff.d.ts +1 -1
  111. package/dist/inspector/prompt/clipboardHandoff.js +3 -3
  112. package/dist/inspector/prompt/generatePrompt.d.ts +2 -1
  113. package/dist/inspector/prompt/generatePrompt.js +19 -2
  114. package/dist/inspector/runtime/editorShell.d.ts +2 -0
  115. package/dist/inspector/runtime/editorShell.js +4 -0
  116. package/dist/inspector/runtime/runtimeConfig.d.ts +5 -1
  117. package/dist/inspector/runtime/runtimeConfig.js +4 -0
  118. package/dist/inspector/selection/hierarchy.d.ts +6 -0
  119. package/dist/inspector/selection/hierarchy.js +8 -0
  120. package/dist/inspector/selection/selectionStore.js +2 -0
  121. package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
  122. package/dist/inspector/settings/SettingsDialog.js +12 -2
  123. package/dist/inspector/shell/AppShell.js +5 -1
  124. package/dist/inspector/shell/ChangesLog.js +26 -22
  125. package/dist/inspector/shell/CopyPromptButton.js +102 -19
  126. package/dist/inspector/shell/EmptyState.js +1 -1
  127. package/dist/inspector/shell/InspectorShell.d.ts +0 -1
  128. package/dist/inspector/shell/InspectorShell.js +38 -42
  129. package/dist/inspector/shell/PeekOriginalButton.d.ts +7 -0
  130. package/dist/inspector/shell/PeekOriginalButton.js +57 -0
  131. package/dist/inspector/shell/PeekOriginalIcon.d.ts +7 -0
  132. package/dist/inspector/shell/PeekOriginalIcon.js +9 -0
  133. package/dist/inspector/shell/originalPreview.d.ts +9 -0
  134. package/dist/inspector/shell/originalPreview.js +61 -0
  135. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  136. package/dist/inspector/shell/shortcuts.js +15 -0
  137. package/dist/inspector/sketch/SketchChanges.d.ts +2 -0
  138. package/dist/inspector/sketch/SketchChanges.js +72 -0
  139. package/dist/inspector/sketch/SketchEditor.d.ts +21 -0
  140. package/dist/inspector/sketch/SketchEditor.js +402 -0
  141. package/dist/inspector/sketch/SketchEntryButton.d.ts +4 -0
  142. package/dist/inspector/sketch/SketchEntryButton.js +11 -0
  143. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +11 -0
  144. package/dist/inspector/sketch/SketchFrameOverlay.js +119 -0
  145. package/dist/inspector/sketch/SketchLayersPanel.d.ts +2 -0
  146. package/dist/inspector/sketch/SketchLayersPanel.js +63 -0
  147. package/dist/inspector/sketch/SketchOverlay.d.ts +19 -0
  148. package/dist/inspector/sketch/SketchOverlay.js +420 -0
  149. package/dist/inspector/sketch/SketchPromptPanel.d.ts +15 -0
  150. package/dist/inspector/sketch/SketchPromptPanel.js +18 -0
  151. package/dist/inspector/sketch/SketchWorkspace.d.ts +13 -0
  152. package/dist/inspector/sketch/SketchWorkspace.js +152 -0
  153. package/dist/inspector/sketch/capture.d.ts +55 -0
  154. package/dist/inspector/sketch/capture.js +350 -0
  155. package/dist/inspector/sketch/freehand.d.ts +13 -0
  156. package/dist/inspector/sketch/freehand.js +64 -0
  157. package/dist/inspector/sketch/geometry.d.ts +17 -0
  158. package/dist/inspector/sketch/geometry.js +98 -0
  159. package/dist/inspector/sketch/handoff.d.ts +21 -0
  160. package/dist/inspector/sketch/handoff.js +72 -0
  161. package/dist/inspector/sketch/interaction.d.ts +21 -0
  162. package/dist/inspector/sketch/interaction.js +98 -0
  163. package/dist/inspector/sketch/model.d.ts +104 -0
  164. package/dist/inspector/sketch/model.js +282 -0
  165. package/dist/inspector/sketch/persistence.d.ts +21 -0
  166. package/dist/inspector/sketch/persistence.js +205 -0
  167. package/dist/inspector/sketch/raster.d.ts +22 -0
  168. package/dist/inspector/sketch/raster.js +203 -0
  169. package/dist/inspector/sketch/sketchNote.d.ts +10 -0
  170. package/dist/inspector/sketch/sketchNote.js +30 -0
  171. package/dist/inspector/sketch/store.d.ts +47 -0
  172. package/dist/inspector/sketch/store.js +434 -0
  173. package/dist/inspector/styleEditors/BorderEditor.js +15 -19
  174. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -6
  175. package/dist/inspector/styleEditors/BoxShadowEditor.js +12 -1
  176. package/dist/inspector/styleEditors/ColorPicker.js +11 -36
  177. package/dist/inspector/styleEditors/GapField.d.ts +3 -2
  178. package/dist/inspector/styleEditors/GapField.js +2 -2
  179. package/dist/inspector/styleEditors/GridSection.js +1 -1
  180. package/dist/inspector/styleEditors/LayoutSection.js +11 -8
  181. package/dist/inspector/styleEditors/SpacingBox.js +31 -33
  182. package/dist/inspector/styleEditors/layoutPreviewState.d.ts +5 -0
  183. package/dist/inspector/styleEditors/layoutPreviewState.js +19 -0
  184. package/dist/inspector/styleEditors/layoutValue.js +3 -0
  185. package/dist/inspector/styleEditors/nudgeValue.d.ts +11 -0
  186. package/dist/inspector/styleEditors/nudgeValue.js +93 -5
  187. package/dist/inspector/styleEditors/stylePresence.d.ts +7 -0
  188. package/dist/inspector/styleEditors/stylePresence.js +22 -0
  189. package/dist/inspector/styleEditors/textRelevance.d.ts +3 -0
  190. package/dist/inspector/styleEditors/textRelevance.js +35 -0
  191. package/dist/inspector/styleEditors/useFieldVisibility.d.ts +6 -0
  192. package/dist/inspector/styleEditors/useFieldVisibility.js +26 -0
  193. package/dist/inspector/tokens/TokenField.js +130 -7
  194. package/dist/inspector/tokens/TokensPanel.js +2 -1
  195. package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
  196. package/dist/inspector/tokens/dragPointerLock.js +70 -0
  197. package/dist/inspector/tokens/editActions.d.ts +2 -0
  198. package/dist/inspector/tokens/editActions.js +2 -2
  199. package/dist/inspector/tokens/resolution/cssomCollector.d.ts +1 -1
  200. package/dist/inspector/tokens/resolution/cssomCollector.js +9 -5
  201. package/dist/inspector/tokens/resolution/ruleMatching.d.ts +40 -0
  202. package/dist/inspector/tokens/resolution/ruleMatching.js +423 -0
  203. package/dist/inspector/tokens/resolution/selectorSemantics.d.ts +19 -3
  204. package/dist/inspector/tokens/resolution/selectorSemantics.js +378 -4
  205. package/dist/inspector/tokens/resolution.d.ts +3 -26
  206. package/dist/inspector/tokens/resolution.js +44 -735
  207. package/dist/inspector/tokens/tokenAvailability.d.ts +18 -0
  208. package/dist/inspector/tokens/tokenAvailability.js +204 -0
  209. package/dist/inspector/ui/Button.d.ts +1 -1
  210. package/dist/inspector/ui/Disclosure.d.ts +22 -0
  211. package/dist/inspector/ui/Disclosure.js +13 -0
  212. package/dist/inspector/ui/SideValuesField.js +2 -2
  213. package/dist/inspector/ui/StatusCallout.d.ts +4 -1
  214. package/dist/inspector/ui/StatusCallout.js +2 -2
  215. package/dist/inspector/ui/dragCursor.d.ts +8 -0
  216. package/dist/inspector/ui/dragCursor.js +21 -0
  217. package/dist/inspector/ui/styles.js +21 -17
  218. package/dist/internal/agent-protocol.d.ts +65 -1
  219. package/dist/internal/agent-protocol.js +183 -3
  220. package/dist/nudge-ui.mjs +276 -58
  221. package/dist/transport/editor.d.ts +47 -0
  222. package/dist/transport/editor.js +163 -0
  223. package/dist/transport/index.d.ts +1 -0
  224. package/dist/transport/index.js +1 -0
  225. package/dist/transport/routes.d.ts +10 -0
  226. package/dist/transport/routes.js +10 -0
  227. package/package.json +3 -1
  228. package/dist/inspector/shell/DomNavigation.d.ts +0 -8
  229. package/dist/inspector/shell/DomNavigation.js +0 -30
  230. package/dist/inspector/shell/panelLayout.d.ts +0 -2
  231. package/dist/inspector/shell/panelLayout.js +0 -29
@@ -1,30 +1,66 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- import { useCanvasCards, exitCanvas, exitCanvasToCard, addCanvasCard, removeCanvasCard, findCardByNormalizedUrl, focusCard, getSelectedCardId, setBoardCamera, getBoardCamera, fitAllCards, hasFitAllRan, useBoardCamera, } from "./canvasStore.js";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ 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";
4
5
  import { CanvasCard } from "./CanvasCard.js";
6
+ import { Button } from "../ui/Button.js";
5
7
  import { useCanvasMode } from "./canvasStore.js";
6
8
  import { subscribeChanges } from "../changes/changesLog.js";
9
+ import { subscribeOriginalPreview, isOriginalPreviewActive } from "../shell/originalPreview.js";
7
10
  import { recordCanvasStructuralProjectionReports } from "../projection/structuralProjection.js";
8
11
  import { recordCanvasRenderedInstanceProjectionReports } from "../projection/renderedInstance.js";
9
12
  import { recordCanvasTextProjectionReports } from "../projection/textProjection.js";
10
- import { findCanvasFrameBySource, getRegisteredFrames, PROJECT_ID, projectToAllReadyCards, recordCanvasProjectionApplied, WORKSPACE_ID, } from "./projection.js";
13
+ import { getChangesList, isPreviewableChange } from "../changes/changesLog.js";
14
+ import { changeKey } from "../changes/model.js";
15
+ import { getWorkspaceChanges } from "../changes/workspaceChanges.js";
16
+ import { beginPreviewAttempt, publishPreviewDiagnostic } from "../changes/previewDiagnostics.js";
17
+ import { verifyManagedStyleProjection } from "../changes/managedStyleProjection.js";
18
+ import { getCanvasPreviewDocument, getRegisteredFrames, isCanvasCanonicalProjectionRevisionCurrent, PROJECT_ID, projectToAllReadyCards, recordCanvasProjectionApplied, WORKSPACE_ID, } from "./projection.js";
11
19
  import { normalizeUrl } from "./normalizeUrl.js";
12
- import { PROTOCOL_VERSION, isRendererMessageFor, isProjectionAppliedMessage, isRenderedInstanceProjectionReportMessage, isTextProjectionReportMessage, isStructuralProjectionReportMessage, } from "./frameProtocol.js";
20
+ import { createNudgeUiEditorUrl } from "../../transport/editor.js";
21
+ import { disposeInlineTextEdit } from "../inline-text/inlineTextEditor.js";
22
+ import { PROTOCOL_VERSION, isProjectionAppliedMessage, isKeyboardShortcutMessage, isRenderedInstanceProjectionReportMessage, isTextProjectionReportMessage, isStructuralProjectionReportMessage, } from "./frameProtocol.js";
13
23
  import { iframePointToClientPoint, zoomCameraAtPointer } from "./canvasGestures.js";
14
- const canvasWorkspaceStyles = ".canvas-workspace {\n position: fixed;\n inset: 0;\n display: flex;\n flex-direction: column;\n background: #ffffff;\n pointer-events: auto;\n}\n\n.canvas-workspace__board {\n flex: 1;\n overflow: hidden;\n position: relative;\n background: #ffffff;\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 will-change: transform;\n}\n";
15
- const canvasCardStyles = ".canvas-card {\n position: relative;\n}\n\n.canvas-card__frame {\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--surface-panel);\n border-radius: var(--radius-control);\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-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.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: space-between;\n z-index: 1;\n cursor: grab;\n}\n\n.canvas-card.is-dragging .canvas-card__toolbar {\n cursor: grabbing;\n}\n\n.canvas-card__drag-surface {\n display: flex;\n align-items: center;\n flex: 1 1 auto;\n min-width: 0;\n min-height: var(--control-height-large);\n padding: 0 var(--space-12);\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 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__actions {\n display: flex;\n align-items: center;\n gap: var(--space-4);\n flex-shrink: 0;\n}\n\n.canvas-card__iframe {\n width: 100%;\n height: 100%;\n border: none;\n display: block;\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 bottom: 0;\n right: 0;\n width: 24px;\n height: 24px;\n display: flex;\n align-items: flex-end;\n justify-content: flex-end;\n padding: 4px;\n cursor: nwse-resize;\n color: var(--text-muted, #777777);\n opacity: 0;\n transition: opacity 150ms ease;\n touch-action: none;\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";
16
- 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: #fff0f0;\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 --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";
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";
17
27
  import { useInspectorOpen } from "../shell/openStore.js";
18
28
  import { isEditableEvent } from "../shell/shortcuts.js";
19
29
  import { acknowledgeAgentRendererReady } from "./agentPresentation.js";
20
30
  import { useInspectorSession } from "../session/sessionContext.js";
21
- const WORKSPACE_STYLES = [foundationStyles, canvasWorkspaceStyles, canvasCardStyles].join("\n");
22
- export function CanvasWorkspace() {
31
+ import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
32
+ import { subscribeCanvasRendererMessages } from "./rendererMessageRouter.js";
33
+ 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";
36
+ import { getActiveCanvasFrame } from "./activeCanvasDocument.js";
37
+ import { startSketchCapture } from "../sketch/SketchWorkspace.js";
38
+ import { cancelSketchInteraction, useSketchInteractionActive } from "../sketch/interaction.js";
39
+ import { isNudgeUiDev } from "../runtime/devFlag.js";
40
+ const WORKSPACE_STYLES = [foundationStyles, canvasWorkspaceStyles, canvasCardStyles, canvasToolbarStyles].join("\n");
41
+ const PRESENTATION_TRANSITION_MS = 200;
42
+ function snapshotRect(rect) {
43
+ return {
44
+ left: rect.left,
45
+ top: rect.top,
46
+ width: rect.width,
47
+ height: rect.height,
48
+ };
49
+ }
50
+ export function CanvasWorkspace({ primaryUrl }) {
23
51
  const mode = useCanvasMode();
52
+ const runtimeConfig = useNudgeUiRuntimeConfig();
24
53
  const inspectorSession = useInspectorSession();
25
54
  const inspectorOpen = useInspectorOpen();
26
55
  const cards = useCanvasCards();
27
56
  const camera = useBoardCamera();
57
+ const presentation = useCanvasPresentation();
58
+ const presentationTransitioning = useCanvasPresentationTransitioning();
59
+ const selectedCardId = useSelectedCardId();
60
+ const focusedCardId = useFocusedCardId();
61
+ const sketchActive = useSketchInteractionActive();
62
+ const sketchEnabled = isNudgeUiDev() && runtimeConfig.demo !== true;
63
+ const sketchAvailable = sketchEnabled && getActiveCanvasFrame() !== null;
28
64
  const boardRef = useRef(null);
29
65
  const panningRef = useRef(false);
30
66
  const panOriginRef = useRef({ x: 0, y: 0 });
@@ -32,7 +68,152 @@ export function CanvasWorkspace() {
32
68
  const framePanContextRef = useRef(null);
33
69
  const spaceHeldRef = useRef(false);
34
70
  const fitAllScheduledRef = useRef(false);
71
+ const activatedTargetRef = useRef(null);
72
+ const demoSeededRef = useRef(false);
73
+ const previousPresentationRef = useRef(presentation);
74
+ const focusCardRectRef = useRef(null);
75
+ const canvasEntryAnimationRef = useRef(null);
35
76
  const [boardCursorClass, setBoardCursorClass] = useState("");
77
+ const [interactionTool, setInteractionTool] = useState("design");
78
+ const rendererInteractionsEnabled = interactionTool !== "select";
79
+ const linksOpenInCards = interactionTool === "select" && presentation === "canvas";
80
+ const presentationCardId = selectedCardId ?? focusedCardId ?? cards[0]?.id ?? null;
81
+ useLayoutEffect(() => {
82
+ const board = boardRef.current;
83
+ const cardElement = presentationCardId
84
+ ? board?.querySelector(`[data-card-id="${presentationCardId}"]`)
85
+ : 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]);
149
+ useLayoutEffect(() => {
150
+ const board = boardRef.current;
151
+ if (!primaryUrl || !board)
152
+ return;
153
+ if (activatedTargetRef.current === primaryUrl && cards.length > 0)
154
+ return;
155
+ activatedTargetRef.current = primaryUrl;
156
+ const viewport = {
157
+ width: board.clientWidth,
158
+ height: board.clientHeight,
159
+ };
160
+ const primaryCard = activateIframeWorkspace(primaryUrl, viewport);
161
+ if (runtimeConfig.demo === true && primaryCard && !demoSeededRef.current) {
162
+ demoSeededRef.current = true;
163
+ const demoPages = runtimeConfig.demoPages ?? [];
164
+ const demoCardLabels = runtimeConfig.demoCardLabels ?? [];
165
+ const orderedDemoCards = demoCardLabels.length > 0;
166
+ const seededCards = [];
167
+ const demoCardWidth = primaryCard.width;
168
+ let nextX = 0;
169
+ for (const [index, route] of demoPages.entries()) {
170
+ const url = new URL(route, primaryUrl);
171
+ if (url.origin !== window.location.origin)
172
+ continue;
173
+ const card = activateIframeWorkspace(url.href, viewport);
174
+ if (!card)
175
+ continue;
176
+ resizeCard(card.id, demoCardWidth, 900);
177
+ seededCards.push(card);
178
+ if (!orderedDemoCards) {
179
+ setCardPosition(card.id, nextX, 0);
180
+ nextX += demoCardWidth + CARD_GAP;
181
+ }
182
+ if (orderedDemoCards && demoCardLabels[index]) {
183
+ updateCardTitle(card.id, demoCardLabels[index]);
184
+ }
185
+ }
186
+ if (orderedDemoCards) {
187
+ resizeCard(primaryCard.id, demoCardWidth, primaryCard.height);
188
+ let layoutX = 0;
189
+ // Read each card's final width so restored or resized cards cannot overlap.
190
+ const orderedCardIds = new Set([...seededCards.map((card) => card.id), primaryCard.id]);
191
+ for (const cardId of orderedCardIds) {
192
+ const card = getCanvasCards().find((candidate) => candidate.id === cardId);
193
+ if (!card)
194
+ continue;
195
+ setCardPosition(card.id, layoutX, 0);
196
+ layoutX += card.width + CARD_GAP;
197
+ }
198
+ const primaryLabel = demoCardLabels[demoPages.length];
199
+ if (primaryLabel)
200
+ updateCardTitle(primaryCard.id, primaryLabel);
201
+ }
202
+ focusCard(primaryCard.id);
203
+ }
204
+ }, [primaryUrl, cards.length, runtimeConfig.demo, runtimeConfig.demoPages, runtimeConfig.demoCardLabels]);
205
+ useEffect(() => {
206
+ if (primaryUrl && activatedTargetRef.current !== primaryUrl)
207
+ return;
208
+ const activeCardId = selectedCardId ?? focusedCardId ?? cards[0]?.id;
209
+ const activeCard = cards.find((card) => card.id === activeCardId);
210
+ if (!activeCard)
211
+ return;
212
+ const editorUrl = createNudgeUiEditorUrl(activeCard.url);
213
+ if (window.location.href !== editorUrl) {
214
+ window.history.replaceState(window.history.state, "", editorUrl);
215
+ }
216
+ }, [cards, focusedCardId, primaryUrl, selectedCardId]);
36
217
  const sendPanModifier = useCallback((iframe, cardId, spaceHeld) => {
37
218
  iframe.contentWindow?.postMessage({
38
219
  type: "pan-modifier",
@@ -43,6 +224,44 @@ export function CanvasWorkspace() {
43
224
  spaceHeld,
44
225
  }, window.location.origin);
45
226
  }, []);
227
+ const sendInspectorInteractionState = useCallback((iframe, cardId, open, interactionsEnabled) => {
228
+ iframe.contentWindow?.postMessage({
229
+ type: "inspector-interaction-state",
230
+ protocolVersion: PROTOCOL_VERSION,
231
+ projectId: PROJECT_ID,
232
+ workspaceId: WORKSPACE_ID,
233
+ cardId,
234
+ open,
235
+ interactionsEnabled,
236
+ }, window.location.origin);
237
+ }, []);
238
+ const sendLinkTargetState = useCallback((iframe, cardId, openInCard) => {
239
+ iframe.contentWindow?.postMessage({
240
+ type: "link-target-state",
241
+ protocolVersion: PROTOCOL_VERSION,
242
+ projectId: PROJECT_ID,
243
+ workspaceId: WORKSPACE_ID,
244
+ cardId,
245
+ openInCard,
246
+ }, window.location.origin);
247
+ }, []);
248
+ const sendBoardGestureState = useCallback((iframe, cardId, enabled) => {
249
+ iframe.contentWindow?.postMessage({
250
+ type: "board-gesture-state",
251
+ protocolVersion: PROTOCOL_VERSION,
252
+ projectId: PROJECT_ID,
253
+ workspaceId: WORKSPACE_ID,
254
+ cardId,
255
+ enabled,
256
+ }, window.location.origin);
257
+ }, []);
258
+ useEffect(() => {
259
+ for (const [cardId, iframe] of getRegisteredFrames()) {
260
+ sendInspectorInteractionState(iframe, cardId, inspectorOpen, rendererInteractionsEnabled);
261
+ sendBoardGestureState(iframe, cardId, presentation === "canvas");
262
+ sendLinkTargetState(iframe, cardId, linksOpenInCards);
263
+ }
264
+ }, [inspectorOpen, linksOpenInCards, presentation, rendererInteractionsEnabled, sendBoardGestureState, sendInspectorInteractionState, sendLinkTargetState]);
46
265
  const broadcastPanModifier = useCallback((spaceHeld) => {
47
266
  for (const [cardId, iframe] of getRegisteredFrames()) {
48
267
  sendPanModifier(iframe, cardId, spaceHeld);
@@ -84,56 +303,177 @@ export function CanvasWorkspace() {
84
303
  panningRef.current = false;
85
304
  setBoardCursorClass(spaceHeldRef.current ? "is-grabbable" : "");
86
305
  }, []);
306
+ const stopPanMode = useCallback(() => {
307
+ spaceHeldRef.current = false;
308
+ broadcastPanModifier(false);
309
+ endPanning();
310
+ setBoardCursorClass("");
311
+ }, [broadcastPanModifier, endPanning]);
312
+ const handleToolChange = useCallback((nextTool) => {
313
+ if (nextTool === "sketch") {
314
+ if (!sketchEnabled)
315
+ return;
316
+ const hostElement = getActiveCanvasFrame();
317
+ if (!hostElement)
318
+ return;
319
+ stopPanMode();
320
+ setInteractionTool(nextTool);
321
+ startSketchCapture(hostElement);
322
+ return;
323
+ }
324
+ if (sketchActive)
325
+ 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
+ }
338
+ setInteractionTool(nextTool);
339
+ stopPanMode();
340
+ }, [broadcastPanModifier, presentation, sketchActive, sketchEnabled, stopPanMode]);
341
+ useEffect(() => {
342
+ if (mode !== "canvas")
343
+ return;
344
+ function onKeyDown(event) {
345
+ if (isEditableEvent(event))
346
+ return;
347
+ const nextTool = canvasToolForEvent(event);
348
+ if (!nextTool || (nextTool === "sketch" && !sketchAvailable))
349
+ return;
350
+ event.preventDefault();
351
+ handleToolChange(nextTool);
352
+ }
353
+ window.addEventListener("keydown", onKeyDown, true);
354
+ return () => window.removeEventListener("keydown", onKeyDown, true);
355
+ }, [handleToolChange, mode, sketchAvailable]);
356
+ const handleZoomStep = useCallback((direction) => {
357
+ if (presentation === "focus") {
358
+ if (direction < 0) {
359
+ const board = boardRef.current;
360
+ setCanvasPresentation("canvas", board ? { width: board.clientWidth, height: board.clientHeight } : undefined);
361
+ }
362
+ return;
363
+ }
364
+ const nextZoom = adjacentCanvasZoomLevel(getBoardCamera().zoom, direction);
365
+ if (nextZoom === null)
366
+ return;
367
+ setBoardCamera({ ...getBoardCamera(), zoom: nextZoom });
368
+ }, [presentation]);
369
+ const showCanvas = useCallback(() => {
370
+ handleZoomStep(-1);
371
+ }, [handleZoomStep]);
372
+ const showFocus = useCallback((cardId) => {
373
+ focusCard(cardId);
374
+ setCanvasPresentation("focus");
375
+ }, []);
376
+ useEffect(() => {
377
+ if (!sketchActive && interactionTool === "sketch") {
378
+ setInteractionTool("design");
379
+ }
380
+ }, [interactionTool, sketchActive]);
381
+ useLayoutEffect(() => {
382
+ if (presentation !== "focus")
383
+ return;
384
+ endPanning();
385
+ spaceHeldRef.current = false;
386
+ setBoardCursorClass("");
387
+ broadcastPanModifier(false);
388
+ }, [broadcastPanModifier, endPanning, presentation]);
87
389
  useEffect(() => {
88
- return subscribeChanges(() => {
390
+ const stopChanges = subscribeChanges(() => {
391
+ projectToAllReadyCards();
392
+ });
393
+ const stopPreview = subscribeOriginalPreview(() => {
89
394
  projectToAllReadyCards();
90
395
  });
396
+ return () => {
397
+ stopChanges();
398
+ stopPreview();
399
+ };
91
400
  }, []);
92
401
  useEffect(() => {
93
- function onMessage(event) {
94
- if (event.origin !== window.location.origin)
95
- return;
96
- if (!event.data || typeof event.data !== "object")
402
+ return subscribeCanvasRendererMessages(({ cardId, iframe, identity, message: msg }) => {
403
+ if (msg.type === "renderer-hello")
97
404
  return;
98
- const frame = findCanvasFrameBySource(event.source);
99
- if (!frame)
100
- return;
101
- if (!isRendererMessageFor(event.data, {
102
- projectId: PROJECT_ID,
103
- workspaceId: WORKSPACE_ID,
104
- cardId: frame.cardId,
105
- }))
405
+ if (isKeyboardShortcutMessage(msg, identity)) {
406
+ if (msg.phase === "keydown") {
407
+ const nextTool = canvasToolForCode(msg.code);
408
+ if (nextTool && (nextTool !== "sketch" || sketchAvailable))
409
+ handleToolChange(nextTool);
410
+ }
106
411
  return;
107
- const identity = {
108
- projectId: PROJECT_ID,
109
- workspaceId: WORKSPACE_ID,
110
- cardId: frame.cardId,
412
+ }
413
+ const frame = { cardId, iframe };
414
+ const syncActiveFrameUrl = (url) => {
415
+ updateCardUrl(frame.cardId, url);
416
+ const activeCardId = getSelectedCardId() ?? getFocusedCardId() ?? getCanvasCards()[0]?.id;
417
+ if (activeCardId !== frame.cardId && getCanvasCards().length !== 1)
418
+ return;
419
+ window.history.replaceState(window.history.state, "", createNudgeUiEditorUrl(url));
111
420
  };
112
- if (isProjectionAppliedMessage(event.data, identity)) {
113
- recordCanvasProjectionApplied(frame.cardId, event.data.revision);
421
+ if (isProjectionAppliedMessage(msg, identity)) {
422
+ recordCanvasProjectionApplied(frame.cardId, msg.revision);
423
+ // While hold-to-view-original is active the frame intentionally shows
424
+ // the page without inspector changes; skip conflict verification.
425
+ if (isOriginalPreviewActive())
426
+ return;
427
+ const frameDocument = frame.iframe.contentDocument;
428
+ if (frameDocument && isCanvasCanonicalProjectionRevisionCurrent(frameDocument, msg.revision)) {
429
+ const attempt = beginPreviewAttempt(getCanvasPreviewDocument(frame.cardId), getWorkspaceChanges().revision);
430
+ if (attempt) {
431
+ for (const change of getChangesList()) {
432
+ if (!isPreviewableChange(change))
433
+ continue;
434
+ const result = verifyManagedStyleProjection(change, null, frameDocument);
435
+ if (result) {
436
+ publishPreviewDiagnostic(attempt, changeKey(change), result);
437
+ if (result.status === "conflict" && result.reason === "animation") {
438
+ window.setTimeout(() => {
439
+ if (!isCanvasCanonicalProjectionRevisionCurrent(frameDocument, msg.revision))
440
+ return;
441
+ const settled = verifyManagedStyleProjection(change, null, frameDocument);
442
+ if (settled)
443
+ publishPreviewDiagnostic(attempt, changeKey(change), settled);
444
+ }, 300);
445
+ }
446
+ }
447
+ }
448
+ }
449
+ }
114
450
  return;
115
451
  }
116
- if (isStructuralProjectionReportMessage(event.data, identity)) {
117
- recordCanvasStructuralProjectionReports(frame.cardId, event.data.revision, event.data.reports);
452
+ if (isStructuralProjectionReportMessage(msg, identity)) {
453
+ recordCanvasStructuralProjectionReports(frame.cardId, msg.revision, msg.reports);
118
454
  return;
119
455
  }
120
- if (isRenderedInstanceProjectionReportMessage(event.data, identity)) {
121
- recordCanvasRenderedInstanceProjectionReports(frame.cardId, event.data.revision, event.data.reports);
456
+ if (isRenderedInstanceProjectionReportMessage(msg, identity)) {
457
+ recordCanvasRenderedInstanceProjectionReports(frame.cardId, msg.revision, msg.reports);
122
458
  return;
123
459
  }
124
- if (isTextProjectionReportMessage(event.data, identity)) {
125
- recordCanvasTextProjectionReports(frame.cardId, event.data.revision, event.data.reports);
460
+ if (isTextProjectionReportMessage(msg, identity)) {
461
+ recordCanvasTextProjectionReports(frame.cardId, msg.revision, msg.reports);
126
462
  return;
127
463
  }
128
- // SAFETY: isRendererMessageFor validated the frame identity and message shape above.
129
- const msg = event.data;
130
464
  if (msg.type === "frame-ready") {
131
465
  acknowledgeAgentRendererReady(frame.cardId);
466
+ syncActiveFrameUrl(msg.url);
132
467
  // A card can finish loading after Space was pressed on the controller.
133
468
  // Seed it with the current modifier state before its first pointer event.
134
469
  sendPanModifier(frame.iframe, frame.cardId, spaceHeldRef.current);
470
+ sendInspectorInteractionState(frame.iframe, frame.cardId, inspectorOpen, rendererInteractionsEnabled);
471
+ sendBoardGestureState(frame.iframe, frame.cardId, presentation === "canvas");
472
+ sendLinkTargetState(frame.iframe, frame.cardId, linksOpenInCards);
135
473
  }
136
474
  if (msg.type === "pan-modifier") {
475
+ if (presentation !== "canvas")
476
+ return;
137
477
  spaceHeldRef.current = msg.spaceHeld;
138
478
  if (!panningRef.current) {
139
479
  setBoardCursorClass(msg.spaceHeld ? "is-grabbable" : "");
@@ -142,6 +482,8 @@ export function CanvasWorkspace() {
142
482
  return;
143
483
  }
144
484
  if (msg.type === "zoom") {
485
+ if (presentation !== "canvas")
486
+ return;
145
487
  const iframeRect = frame.iframe.getBoundingClientRect();
146
488
  const point = iframePointToClientPoint(iframeRect, msg.point, getBoardCamera().zoom);
147
489
  zoomAtPointer(point, msg.deltaY);
@@ -179,12 +521,12 @@ export function CanvasWorkspace() {
179
521
  endPanning();
180
522
  return;
181
523
  }
182
- if (msg.type === "external-navigation") {
183
- const destination = normalizeUrl(msg.url);
184
- if (!destination || destination.origin === window.location.origin)
185
- return;
186
- exitCanvas();
187
- window.location.href = msg.url;
524
+ if (msg.type === "frame-metadata") {
525
+ const frameDocument = frame.iframe.contentDocument;
526
+ if (frameDocument)
527
+ disposeInlineTextEdit("route-disposed", frameDocument);
528
+ if (msg.url)
529
+ syncActiveFrameUrl(msg.url);
188
530
  return;
189
531
  }
190
532
  if (msg.type !== "navigation-intent")
@@ -192,17 +534,16 @@ export function CanvasWorkspace() {
192
534
  const normalized = normalizeUrl(msg.url);
193
535
  if (!normalized || normalized.origin !== window.location.origin)
194
536
  return;
195
- const existing = findCardByNormalizedUrl(normalized);
196
- if (existing) {
197
- focusCard(existing.id);
198
- }
199
- else {
200
- addCanvasCard(msg.url);
537
+ if (msg.openInCard === true) {
538
+ const card = findCardByNormalizedUrl(normalized) ?? addCanvasCard(msg.url);
539
+ selectCard(card.id);
540
+ return;
201
541
  }
202
- }
203
- window.addEventListener("message", onMessage);
204
- return () => window.removeEventListener("message", onMessage);
205
- }, [broadcastPanModifier, endPanning, movePanning, sendPanModifier, startPanning, zoomAtPointer]);
542
+ const frameDocument = frame.iframe.contentDocument;
543
+ if (frameDocument)
544
+ disposeInlineTextEdit("route-disposed", frameDocument);
545
+ });
546
+ }, [broadcastPanModifier, endPanning, handleToolChange, inspectorOpen, linksOpenInCards, movePanning, presentation, rendererInteractionsEnabled, sendBoardGestureState, sendInspectorInteractionState, sendLinkTargetState, sendPanModifier, sketchAvailable, startPanning, zoomAtPointer]);
206
547
  useEffect(() => {
207
548
  if (mode === "canvas" && !hasFitAllRan()) {
208
549
  if (!fitAllScheduledRef.current) {
@@ -216,7 +557,7 @@ export function CanvasWorkspace() {
216
557
  }, [mode, cards.length, inspectorOpen, fitCanvasToBoard]);
217
558
  useEffect(() => {
218
559
  function onKeyDown(e) {
219
- if (mode !== "canvas")
560
+ if (mode !== "canvas" || presentation !== "canvas" || sketchActive)
220
561
  return;
221
562
  if ((e.key === "Delete" || e.key === "Backspace") && !isEditableEvent(e)) {
222
563
  const selectedCardId = getSelectedCardId();
@@ -227,9 +568,7 @@ export function CanvasWorkspace() {
227
568
  return;
228
569
  }
229
570
  if (e.code === "Space" && !e.repeat) {
230
- // SAFETY: keyboard event targets are HTMLElements in the workspace DOM.
231
- const target = e.target;
232
- if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable))
571
+ if (isEditableEvent(e))
233
572
  return;
234
573
  e.preventDefault();
235
574
  spaceHeldRef.current = true;
@@ -241,10 +580,10 @@ export function CanvasWorkspace() {
241
580
  }
242
581
  function onKeyUp(e) {
243
582
  if (e.code === "Space") {
244
- spaceHeldRef.current = false;
245
- broadcastPanModifier(false);
583
+ spaceHeldRef.current = interactionTool === "pan";
584
+ broadcastPanModifier(spaceHeldRef.current);
246
585
  if (!panningRef.current) {
247
- setBoardCursorClass("");
586
+ setBoardCursorClass(spaceHeldRef.current ? "is-grabbable" : "");
248
587
  }
249
588
  }
250
589
  }
@@ -254,8 +593,10 @@ export function CanvasWorkspace() {
254
593
  window.removeEventListener("keydown", onKeyDown, true);
255
594
  window.removeEventListener("keyup", onKeyUp, true);
256
595
  };
257
- }, [broadcastPanModifier, mode]);
596
+ }, [broadcastPanModifier, interactionTool, mode, presentation, sketchActive]);
258
597
  const handleBoardPointerDown = useCallback((e) => {
598
+ if (presentation !== "canvas" || sketchActive)
599
+ return;
259
600
  if (panningRef.current)
260
601
  return;
261
602
  if (!spaceHeldRef.current)
@@ -275,9 +616,9 @@ export function CanvasWorkspace() {
275
616
  }
276
617
  window.addEventListener("pointermove", onMove);
277
618
  window.addEventListener("pointerup", onUp);
278
- }, [endPanning, movePanning, startPanning]);
619
+ }, [endPanning, movePanning, presentation, sketchActive, startPanning]);
279
620
  useEffect(() => {
280
- if (mode !== "canvas")
621
+ if (mode !== "canvas" || presentation !== "canvas" || sketchActive)
281
622
  return;
282
623
  function handleGlobalWheel(e) {
283
624
  if (!e.ctrlKey && !e.metaKey)
@@ -288,14 +629,11 @@ export function CanvasWorkspace() {
288
629
  }
289
630
  window.addEventListener("wheel", handleGlobalWheel, { capture: true, passive: false });
290
631
  return () => window.removeEventListener("wheel", handleGlobalWheel, { capture: true });
291
- }, [mode, zoomAtPointer]);
292
- function handleEdit(card) {
293
- exitCanvasToCard(card);
294
- }
295
- if (mode !== "canvas" || cards.length === 0)
632
+ }, [mode, presentation, sketchActive, zoomAtPointer]);
633
+ if (mode !== "canvas")
296
634
  return null;
297
- return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "canvas-styles", children: WORKSPACE_STYLES }), _jsx("div", { className: "canvas-workspace", "data-test": "canvas-workspace", style: { right: inspectorOpen ? "min(320px, 100vw)" : 0 }, children: _jsx("div", { className: `canvas-workspace__board ${boardCursorClass}`.trim(), "data-test": "canvas-board", ref: boardRef, onPointerDown: handleBoardPointerDown, children: _jsx("div", { className: "canvas-workspace__board-content", style: {
298
- transform: `translate(${camera.x}px, ${camera.y}px) scale(${camera.zoom})`,
299
- transformOrigin: "0 0",
300
- }, "data-test": "canvas-board-content", children: cards.map((card) => (_jsx(CanvasCard, { card: card, onEdit: handleEdit, documentOwner: inspectorSession }, card.id))) }) }) })] }));
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: {
636
+ transform: `translate(${camera.x}px, ${camera.y}px) scale(${camera.zoom})`,
637
+ 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] }));
301
639
  }