nudge-ui 0.2.0 → 0.2.1

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 (201) 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 +872 -12
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -1
  6. package/dist/hosts/astro/clientTransport.js +60 -11
  7. package/dist/hosts/next/sidecar.d.ts +4 -0
  8. package/dist/hosts/next/sidecar.js +49 -6
  9. package/dist/hosts/next/wrapper.js +30 -2
  10. package/dist/hosts/projectBridge.d.ts +20 -0
  11. package/dist/hosts/projectBridge.js +97 -0
  12. package/dist/hosts/static/cli.d.ts +1 -1
  13. package/dist/hosts/static/cli.js +6 -1
  14. package/dist/hosts/static/manifest.d.ts +1 -1
  15. package/dist/hosts/static/manifest.js +1 -1
  16. package/dist/hosts/static/server.js +44 -3
  17. package/dist/hosts/vite/vite.d.ts +5 -1
  18. package/dist/hosts/vite/vite.js +78 -14
  19. package/dist/inspector/agent/McpConnectionDialog.d.ts +2 -2
  20. package/dist/inspector/agent/McpConnectionDialog.js +11 -35
  21. package/dist/inspector/agent/client.d.ts +10 -3
  22. package/dist/inspector/agent/client.js +80 -9
  23. package/dist/inspector/agent/connectionStatus.js +3 -3
  24. package/dist/inspector/agent/httpTransport.d.ts +2 -0
  25. package/dist/inspector/agent/httpTransport.js +37 -17
  26. package/dist/inspector/agent/protocol.d.ts +8 -3
  27. package/dist/inspector/agent/protocol.js +1 -1
  28. package/dist/inspector/agent/verification.d.ts +1 -1
  29. package/dist/inspector/agent/verification.js +69 -12
  30. package/dist/inspector/canvas/CanvasCard.d.ts +5 -3
  31. package/dist/inspector/canvas/CanvasCard.js +181 -91
  32. package/dist/inspector/canvas/CanvasElementOverlay.js +246 -52
  33. package/dist/inspector/canvas/CanvasToolbar.d.ts +12 -0
  34. package/dist/inspector/canvas/CanvasToolbar.js +24 -0
  35. package/dist/inspector/canvas/CanvasWorkspace.d.ts +4 -1
  36. package/dist/inspector/canvas/CanvasWorkspace.js +392 -73
  37. package/dist/inspector/canvas/activeCanvasDocument.d.ts +5 -0
  38. package/dist/inspector/canvas/activeCanvasDocument.js +23 -0
  39. package/dist/inspector/canvas/canvasResize.d.ts +30 -0
  40. package/dist/inspector/canvas/canvasResize.js +52 -0
  41. package/dist/inspector/canvas/canvasStore.d.ts +30 -6
  42. package/dist/inspector/canvas/canvasStore.js +128 -61
  43. package/dist/inspector/canvas/frameProtocol.d.ts +69 -5
  44. package/dist/inspector/canvas/frameProtocol.js +56 -2
  45. package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -0
  46. package/dist/inspector/canvas/keyboardShortcuts.js +14 -0
  47. package/dist/inspector/canvas/projection.d.ts +7 -0
  48. package/dist/inspector/canvas/projection.js +118 -25
  49. package/dist/inspector/canvas/rendererBootstrap.js +29 -29
  50. package/dist/inspector/canvas/rendererElementSelector.js +341 -30
  51. package/dist/inspector/canvas/rendererMessageRouter.d.ts +11 -0
  52. package/dist/inspector/canvas/rendererMessageRouter.js +43 -0
  53. package/dist/inspector/canvas/rendererSelectionProxy.js +6 -2
  54. package/dist/inspector/canvas/sessionStore.d.ts +1 -0
  55. package/dist/inspector/canvas/sessionStore.js +12 -4
  56. package/dist/inspector/canvas/staleChangeDetector.js +16 -14
  57. package/dist/inspector/changes/changesLog.js +31 -1
  58. package/dist/inspector/changes/managedStyleProjection.d.ts +1 -1
  59. package/dist/inspector/changes/managedStyleProjection.js +3 -3
  60. package/dist/inspector/changes/structuralTypes.d.ts +4 -0
  61. package/dist/inspector/clientManifest.d.ts +5 -0
  62. package/dist/inspector/clientManifest.js +11 -0
  63. package/dist/inspector/componentSemantics/adapterRegistry.js +4 -3
  64. package/dist/inspector/componentSemantics/runtimeBridge.d.ts +2 -2
  65. package/dist/inspector/componentSemantics/runtimeBridge.js +16 -3
  66. package/dist/inspector/index.d.ts +2 -0
  67. package/dist/inspector/index.js +74 -24
  68. package/dist/inspector/inline-text/inlineTextDiagnostics.d.ts +10 -0
  69. package/dist/inspector/inline-text/inlineTextDiagnostics.js +65 -0
  70. package/dist/inspector/inline-text/inlineTextEditor.d.ts +3 -5
  71. package/dist/inspector/inline-text/inlineTextEditor.js +74 -206
  72. package/dist/inspector/inline-text/inlineTextFeedback.d.ts +5 -0
  73. package/dist/inspector/inline-text/inlineTextFeedback.js +27 -0
  74. package/dist/inspector/inline-text/inlineTextLifecycle.js +2 -0
  75. package/dist/inspector/inline-text/textEditChange.d.ts +14 -0
  76. package/dist/inspector/inline-text/textEditChange.js +165 -0
  77. package/dist/inspector/inspection/selectionProperty.d.ts +1 -0
  78. package/dist/inspector/inspection/selectionProperty.js +1 -0
  79. package/dist/inspector/overlay/selectedGeometry.js +21 -2
  80. package/dist/inspector/overlay/spacingGestures.d.ts +45 -0
  81. package/dist/inspector/overlay/spacingGestures.js +353 -0
  82. package/dist/inspector/overlay/structuralGestures.d.ts +2 -2
  83. package/dist/inspector/overlay/structuralGestures.js +12 -10
  84. package/dist/inspector/projection/managedStylesheet.js +4 -1
  85. package/dist/inspector/projection/renderedInstance.d.ts +2 -0
  86. package/dist/inspector/projection/renderedInstance.js +5 -1
  87. package/dist/inspector/projection/structuralProjection.d.ts +2 -0
  88. package/dist/inspector/projection/structuralProjection.js +85 -1
  89. package/dist/inspector/projection/structuralProjectionBoundary.js +17 -1
  90. package/dist/inspector/projection/textProjection.d.ts +2 -0
  91. package/dist/inspector/projection/textProjection.js +10 -4
  92. package/dist/inspector/prompt/clipboardHandoff.d.ts +1 -1
  93. package/dist/inspector/prompt/clipboardHandoff.js +3 -3
  94. package/dist/inspector/prompt/generatePrompt.d.ts +2 -1
  95. package/dist/inspector/prompt/generatePrompt.js +19 -2
  96. package/dist/inspector/runtime/editorShell.d.ts +2 -0
  97. package/dist/inspector/runtime/editorShell.js +4 -0
  98. package/dist/inspector/runtime/runtimeConfig.d.ts +4 -0
  99. package/dist/inspector/runtime/runtimeConfig.js +4 -0
  100. package/dist/inspector/selection/selectionStore.js +2 -0
  101. package/dist/inspector/settings/SettingsDialog.js +10 -0
  102. package/dist/inspector/shell/AppShell.js +5 -1
  103. package/dist/inspector/shell/ChangesLog.js +8 -4
  104. package/dist/inspector/shell/CopyPromptButton.js +95 -19
  105. package/dist/inspector/shell/EmptyState.js +1 -1
  106. package/dist/inspector/shell/InspectorShell.d.ts +0 -1
  107. package/dist/inspector/shell/InspectorShell.js +37 -38
  108. package/dist/inspector/shell/PeekOriginalButton.d.ts +7 -0
  109. package/dist/inspector/shell/PeekOriginalButton.js +57 -0
  110. package/dist/inspector/shell/PeekOriginalIcon.d.ts +7 -0
  111. package/dist/inspector/shell/PeekOriginalIcon.js +9 -0
  112. package/dist/inspector/shell/originalPreview.d.ts +9 -0
  113. package/dist/inspector/shell/originalPreview.js +61 -0
  114. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  115. package/dist/inspector/shell/shortcuts.js +15 -0
  116. package/dist/inspector/sketch/SketchChanges.d.ts +2 -0
  117. package/dist/inspector/sketch/SketchChanges.js +72 -0
  118. package/dist/inspector/sketch/SketchEditor.d.ts +21 -0
  119. package/dist/inspector/sketch/SketchEditor.js +402 -0
  120. package/dist/inspector/sketch/SketchEntryButton.d.ts +4 -0
  121. package/dist/inspector/sketch/SketchEntryButton.js +11 -0
  122. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +11 -0
  123. package/dist/inspector/sketch/SketchFrameOverlay.js +119 -0
  124. package/dist/inspector/sketch/SketchLayersPanel.d.ts +2 -0
  125. package/dist/inspector/sketch/SketchLayersPanel.js +63 -0
  126. package/dist/inspector/sketch/SketchOverlay.d.ts +19 -0
  127. package/dist/inspector/sketch/SketchOverlay.js +420 -0
  128. package/dist/inspector/sketch/SketchPromptPanel.d.ts +15 -0
  129. package/dist/inspector/sketch/SketchPromptPanel.js +18 -0
  130. package/dist/inspector/sketch/SketchWorkspace.d.ts +13 -0
  131. package/dist/inspector/sketch/SketchWorkspace.js +152 -0
  132. package/dist/inspector/sketch/capture.d.ts +55 -0
  133. package/dist/inspector/sketch/capture.js +350 -0
  134. package/dist/inspector/sketch/freehand.d.ts +13 -0
  135. package/dist/inspector/sketch/freehand.js +64 -0
  136. package/dist/inspector/sketch/geometry.d.ts +17 -0
  137. package/dist/inspector/sketch/geometry.js +98 -0
  138. package/dist/inspector/sketch/handoff.d.ts +21 -0
  139. package/dist/inspector/sketch/handoff.js +72 -0
  140. package/dist/inspector/sketch/interaction.d.ts +21 -0
  141. package/dist/inspector/sketch/interaction.js +98 -0
  142. package/dist/inspector/sketch/model.d.ts +104 -0
  143. package/dist/inspector/sketch/model.js +282 -0
  144. package/dist/inspector/sketch/persistence.d.ts +21 -0
  145. package/dist/inspector/sketch/persistence.js +205 -0
  146. package/dist/inspector/sketch/raster.d.ts +22 -0
  147. package/dist/inspector/sketch/raster.js +203 -0
  148. package/dist/inspector/sketch/sketchNote.d.ts +10 -0
  149. package/dist/inspector/sketch/sketchNote.js +30 -0
  150. package/dist/inspector/sketch/store.d.ts +47 -0
  151. package/dist/inspector/sketch/store.js +409 -0
  152. package/dist/inspector/styleEditors/BorderEditor.js +15 -19
  153. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -6
  154. package/dist/inspector/styleEditors/BoxShadowEditor.js +12 -1
  155. package/dist/inspector/styleEditors/ColorPicker.js +11 -36
  156. package/dist/inspector/styleEditors/GapField.d.ts +3 -2
  157. package/dist/inspector/styleEditors/GapField.js +2 -2
  158. package/dist/inspector/styleEditors/GridSection.js +1 -1
  159. package/dist/inspector/styleEditors/LayoutSection.js +11 -8
  160. package/dist/inspector/styleEditors/SpacingBox.js +23 -30
  161. package/dist/inspector/styleEditors/layoutPreviewState.d.ts +5 -0
  162. package/dist/inspector/styleEditors/layoutPreviewState.js +19 -0
  163. package/dist/inspector/styleEditors/layoutValue.js +3 -0
  164. package/dist/inspector/styleEditors/nudgeValue.d.ts +11 -0
  165. package/dist/inspector/styleEditors/nudgeValue.js +91 -5
  166. package/dist/inspector/styleEditors/stylePresence.d.ts +7 -0
  167. package/dist/inspector/styleEditors/stylePresence.js +22 -0
  168. package/dist/inspector/styleEditors/textRelevance.d.ts +3 -0
  169. package/dist/inspector/styleEditors/textRelevance.js +35 -0
  170. package/dist/inspector/styleEditors/useFieldVisibility.d.ts +6 -0
  171. package/dist/inspector/styleEditors/useFieldVisibility.js +26 -0
  172. package/dist/inspector/tokens/TokenField.js +111 -7
  173. package/dist/inspector/tokens/editActions.d.ts +2 -0
  174. package/dist/inspector/tokens/editActions.js +2 -2
  175. package/dist/inspector/tokens/resolution/cssomCollector.d.ts +1 -1
  176. package/dist/inspector/tokens/resolution/cssomCollector.js +9 -5
  177. package/dist/inspector/tokens/resolution/ruleMatching.d.ts +40 -0
  178. package/dist/inspector/tokens/resolution/ruleMatching.js +423 -0
  179. package/dist/inspector/tokens/resolution/selectorSemantics.d.ts +19 -3
  180. package/dist/inspector/tokens/resolution/selectorSemantics.js +378 -4
  181. package/dist/inspector/tokens/resolution.d.ts +3 -26
  182. package/dist/inspector/tokens/resolution.js +44 -735
  183. package/dist/inspector/tokens/tokenAvailability.d.ts +18 -0
  184. package/dist/inspector/tokens/tokenAvailability.js +204 -0
  185. package/dist/inspector/ui/Button.d.ts +1 -1
  186. package/dist/inspector/ui/SideValuesField.js +2 -2
  187. package/dist/inspector/ui/StatusCallout.d.ts +4 -1
  188. package/dist/inspector/ui/StatusCallout.js +2 -2
  189. package/dist/inspector/ui/styles.js +13 -11
  190. package/dist/internal/agent-protocol.d.ts +65 -1
  191. package/dist/internal/agent-protocol.js +183 -3
  192. package/dist/nudge-ui.mjs +261 -57
  193. package/dist/transport/editor.d.ts +28 -0
  194. package/dist/transport/editor.js +132 -0
  195. package/dist/transport/index.d.ts +1 -0
  196. package/dist/transport/index.js +1 -0
  197. package/dist/transport/routes.d.ts +7 -0
  198. package/dist/transport/routes.js +7 -0
  199. package/package.json +3 -1
  200. package/dist/inspector/shell/panelLayout.d.ts +0 -2
  201. 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, 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 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__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,150 @@ 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("move");
78
+ const presentationCardId = selectedCardId ?? focusedCardId ?? cards[0]?.id ?? null;
79
+ useLayoutEffect(() => {
80
+ const board = boardRef.current;
81
+ const cardElement = presentationCardId
82
+ ? board?.querySelector(`[data-card-id="${presentationCardId}"]`)
83
+ : null;
84
+ const boardContent = board?.querySelector('[data-test="canvas-board-content"]');
85
+ if (presentation === "focus") {
86
+ canvasEntryAnimationRef.current?.cancel();
87
+ canvasEntryAnimationRef.current = null;
88
+ if (boardContent)
89
+ boardContent.style.transition = "";
90
+ if (cardElement)
91
+ cardElement.style.transition = "";
92
+ focusCardRectRef.current = cardElement ? snapshotRect(cardElement.getBoundingClientRect()) : null;
93
+ }
94
+ else if (presentation === "canvas"
95
+ && previousPresentationRef.current === "focus"
96
+ && presentationTransitioning
97
+ && cardElement
98
+ && boardContent
99
+ && focusCardRectRef.current
100
+ && typeof cardElement.animate === "function") {
101
+ // Commit the final layout before measuring it. The old implementation
102
+ // transitioned both the camera and the card's position, which made the
103
+ // nested scale and translation produce a sideways drift.
104
+ boardContent.style.transition = "none";
105
+ cardElement.style.transition = "none";
106
+ const oldRect = focusCardRectRef.current;
107
+ const newRect = snapshotRect(cardElement.getBoundingClientRect());
108
+ if (newRect.width > 0 && newRect.height > 0 && camera.zoom > 0) {
109
+ const oldCenterX = oldRect.left + oldRect.width / 2;
110
+ const oldCenterY = oldRect.top + oldRect.height / 2;
111
+ const newCenterX = newRect.left + newRect.width / 2;
112
+ const newCenterY = newRect.top + newRect.height / 2;
113
+ const localTranslateX = (oldCenterX - newCenterX) / camera.zoom;
114
+ const localTranslateY = (oldCenterY - newCenterY) / camera.zoom;
115
+ const scaleX = oldRect.width / newRect.width;
116
+ const scaleY = oldRect.height / newRect.height;
117
+ const animation = cardElement.animate([
118
+ {
119
+ transformOrigin: "center center",
120
+ transform: `translate(${localTranslateX}px, ${localTranslateY}px) scale(${scaleX}, ${scaleY})`,
121
+ },
122
+ { transformOrigin: "center center", transform: "none" },
123
+ ], { duration: PRESENTATION_TRANSITION_MS, easing: "ease", fill: "both" });
124
+ canvasEntryAnimationRef.current = animation;
125
+ void animation.finished.then(() => {
126
+ if (canvasEntryAnimationRef.current !== animation)
127
+ return;
128
+ canvasEntryAnimationRef.current = null;
129
+ animation.cancel();
130
+ boardContent.style.transition = "";
131
+ cardElement.style.transition = "";
132
+ }).catch(() => {
133
+ if (canvasEntryAnimationRef.current !== animation)
134
+ return;
135
+ canvasEntryAnimationRef.current = null;
136
+ boardContent.style.transition = "";
137
+ cardElement.style.transition = "";
138
+ });
139
+ }
140
+ else {
141
+ boardContent.style.transition = "";
142
+ cardElement.style.transition = "";
143
+ }
144
+ }
145
+ previousPresentationRef.current = presentation;
146
+ }, [camera.zoom, presentation, presentationCardId, presentationTransitioning]);
147
+ useLayoutEffect(() => {
148
+ const board = boardRef.current;
149
+ if (!primaryUrl || !board)
150
+ return;
151
+ if (activatedTargetRef.current === primaryUrl && cards.length > 0)
152
+ return;
153
+ activatedTargetRef.current = primaryUrl;
154
+ const viewport = {
155
+ width: board.clientWidth,
156
+ height: board.clientHeight,
157
+ };
158
+ const primaryCard = activateIframeWorkspace(primaryUrl, viewport);
159
+ if (runtimeConfig.demo === true && primaryCard && !demoSeededRef.current) {
160
+ demoSeededRef.current = true;
161
+ const demoPages = runtimeConfig.demoPages ?? [];
162
+ const demoCardLabels = runtimeConfig.demoCardLabels ?? [];
163
+ const orderedDemoCards = demoCardLabels.length > 0;
164
+ const seededCards = [];
165
+ const demoCardWidth = primaryCard.width;
166
+ let nextX = 0;
167
+ for (const [index, route] of demoPages.entries()) {
168
+ const url = new URL(route, primaryUrl);
169
+ if (url.origin !== window.location.origin)
170
+ continue;
171
+ const card = activateIframeWorkspace(url.href, viewport);
172
+ if (!card)
173
+ continue;
174
+ resizeCard(card.id, demoCardWidth, 900);
175
+ seededCards.push(card);
176
+ if (!orderedDemoCards) {
177
+ setCardPosition(card.id, nextX, 0);
178
+ nextX += demoCardWidth + CARD_GAP;
179
+ }
180
+ if (orderedDemoCards && demoCardLabels[index]) {
181
+ updateCardTitle(card.id, demoCardLabels[index]);
182
+ }
183
+ }
184
+ if (orderedDemoCards) {
185
+ resizeCard(primaryCard.id, demoCardWidth, primaryCard.height);
186
+ let layoutX = 0;
187
+ // Read each card's final width so restored or resized cards cannot overlap.
188
+ const orderedCardIds = new Set([...seededCards.map((card) => card.id), primaryCard.id]);
189
+ for (const cardId of orderedCardIds) {
190
+ const card = getCanvasCards().find((candidate) => candidate.id === cardId);
191
+ if (!card)
192
+ continue;
193
+ setCardPosition(card.id, layoutX, 0);
194
+ layoutX += card.width + CARD_GAP;
195
+ }
196
+ const primaryLabel = demoCardLabels[demoPages.length];
197
+ if (primaryLabel)
198
+ updateCardTitle(primaryCard.id, primaryLabel);
199
+ }
200
+ focusCard(primaryCard.id);
201
+ }
202
+ }, [primaryUrl, cards.length, runtimeConfig.demo, runtimeConfig.demoPages, runtimeConfig.demoCardLabels]);
203
+ useEffect(() => {
204
+ if (primaryUrl && activatedTargetRef.current !== primaryUrl)
205
+ return;
206
+ const activeCardId = selectedCardId ?? focusedCardId ?? cards[0]?.id;
207
+ const activeCard = cards.find((card) => card.id === activeCardId);
208
+ if (!activeCard)
209
+ return;
210
+ const editorUrl = createNudgeUiEditorUrl(activeCard.url);
211
+ if (window.location.href !== editorUrl) {
212
+ window.history.replaceState(window.history.state, "", editorUrl);
213
+ }
214
+ }, [cards, focusedCardId, primaryUrl, selectedCardId]);
36
215
  const sendPanModifier = useCallback((iframe, cardId, spaceHeld) => {
37
216
  iframe.contentWindow?.postMessage({
38
217
  type: "pan-modifier",
@@ -43,6 +222,33 @@ export function CanvasWorkspace() {
43
222
  spaceHeld,
44
223
  }, window.location.origin);
45
224
  }, []);
225
+ const sendInspectorInteractionState = useCallback((iframe, cardId, open) => {
226
+ iframe.contentWindow?.postMessage({
227
+ type: "inspector-interaction-state",
228
+ protocolVersion: PROTOCOL_VERSION,
229
+ projectId: PROJECT_ID,
230
+ workspaceId: WORKSPACE_ID,
231
+ cardId,
232
+ open,
233
+ interactionsEnabled: true,
234
+ }, window.location.origin);
235
+ }, []);
236
+ const sendBoardGestureState = useCallback((iframe, cardId, enabled) => {
237
+ iframe.contentWindow?.postMessage({
238
+ type: "board-gesture-state",
239
+ protocolVersion: PROTOCOL_VERSION,
240
+ projectId: PROJECT_ID,
241
+ workspaceId: WORKSPACE_ID,
242
+ cardId,
243
+ enabled,
244
+ }, window.location.origin);
245
+ }, []);
246
+ useEffect(() => {
247
+ for (const [cardId, iframe] of getRegisteredFrames()) {
248
+ sendInspectorInteractionState(iframe, cardId, inspectorOpen);
249
+ sendBoardGestureState(iframe, cardId, presentation === "canvas");
250
+ }
251
+ }, [inspectorOpen, presentation, sendBoardGestureState, sendInspectorInteractionState]);
46
252
  const broadcastPanModifier = useCallback((spaceHeld) => {
47
253
  for (const [cardId, iframe] of getRegisteredFrames()) {
48
254
  sendPanModifier(iframe, cardId, spaceHeld);
@@ -84,56 +290,176 @@ export function CanvasWorkspace() {
84
290
  panningRef.current = false;
85
291
  setBoardCursorClass(spaceHeldRef.current ? "is-grabbable" : "");
86
292
  }, []);
293
+ const stopPanMode = useCallback(() => {
294
+ spaceHeldRef.current = false;
295
+ broadcastPanModifier(false);
296
+ endPanning();
297
+ setBoardCursorClass("");
298
+ }, [broadcastPanModifier, endPanning]);
299
+ const handleToolChange = useCallback((nextTool) => {
300
+ if (nextTool === "sketch") {
301
+ if (!sketchEnabled)
302
+ return;
303
+ const hostElement = getActiveCanvasFrame();
304
+ if (!hostElement)
305
+ return;
306
+ stopPanMode();
307
+ setInteractionTool(nextTool);
308
+ startSketchCapture(hostElement);
309
+ return;
310
+ }
311
+ if (sketchActive)
312
+ cancelSketchInteraction();
313
+ if (nextTool === "pan") {
314
+ if (presentation === "focus") {
315
+ const board = boardRef.current;
316
+ setCanvasPresentation("canvas", board ? { width: board.clientWidth, height: board.clientHeight } : undefined);
317
+ }
318
+ setInteractionTool(nextTool);
319
+ spaceHeldRef.current = true;
320
+ broadcastPanModifier(true);
321
+ if (!panningRef.current)
322
+ setBoardCursorClass("is-grabbable");
323
+ return;
324
+ }
325
+ setInteractionTool(nextTool);
326
+ stopPanMode();
327
+ }, [broadcastPanModifier, presentation, sketchActive, sketchEnabled, stopPanMode]);
328
+ useEffect(() => {
329
+ if (mode !== "canvas")
330
+ return;
331
+ function onKeyDown(event) {
332
+ if (isEditableEvent(event))
333
+ return;
334
+ const nextTool = canvasToolForEvent(event);
335
+ if (!nextTool || (nextTool === "sketch" && !sketchAvailable))
336
+ return;
337
+ event.preventDefault();
338
+ handleToolChange(nextTool);
339
+ }
340
+ window.addEventListener("keydown", onKeyDown, true);
341
+ return () => window.removeEventListener("keydown", onKeyDown, true);
342
+ }, [handleToolChange, mode, sketchAvailable]);
343
+ const handleZoomStep = useCallback((direction) => {
344
+ if (presentation === "focus") {
345
+ if (direction < 0) {
346
+ const board = boardRef.current;
347
+ setCanvasPresentation("canvas", board ? { width: board.clientWidth, height: board.clientHeight } : undefined);
348
+ }
349
+ return;
350
+ }
351
+ const nextZoom = adjacentCanvasZoomLevel(getBoardCamera().zoom, direction);
352
+ if (nextZoom === null)
353
+ return;
354
+ setBoardCamera({ ...getBoardCamera(), zoom: nextZoom });
355
+ }, [presentation]);
356
+ const showCanvas = useCallback(() => {
357
+ handleZoomStep(-1);
358
+ }, [handleZoomStep]);
359
+ const showFocus = useCallback((cardId) => {
360
+ focusCard(cardId);
361
+ setCanvasPresentation("focus");
362
+ }, []);
87
363
  useEffect(() => {
88
- return subscribeChanges(() => {
364
+ if (!sketchActive && interactionTool === "sketch") {
365
+ setInteractionTool("move");
366
+ }
367
+ }, [interactionTool, sketchActive]);
368
+ useLayoutEffect(() => {
369
+ if (presentation !== "focus")
370
+ return;
371
+ endPanning();
372
+ spaceHeldRef.current = false;
373
+ setBoardCursorClass("");
374
+ broadcastPanModifier(false);
375
+ }, [broadcastPanModifier, endPanning, presentation]);
376
+ useEffect(() => {
377
+ const stopChanges = subscribeChanges(() => {
89
378
  projectToAllReadyCards();
90
379
  });
380
+ const stopPreview = subscribeOriginalPreview(() => {
381
+ projectToAllReadyCards();
382
+ });
383
+ return () => {
384
+ stopChanges();
385
+ stopPreview();
386
+ };
91
387
  }, []);
92
388
  useEffect(() => {
93
- function onMessage(event) {
94
- if (event.origin !== window.location.origin)
95
- return;
96
- if (!event.data || typeof event.data !== "object")
97
- return;
98
- const frame = findCanvasFrameBySource(event.source);
99
- if (!frame)
389
+ return subscribeCanvasRendererMessages(({ cardId, iframe, identity, message: msg }) => {
390
+ if (msg.type === "renderer-hello")
100
391
  return;
101
- if (!isRendererMessageFor(event.data, {
102
- projectId: PROJECT_ID,
103
- workspaceId: WORKSPACE_ID,
104
- cardId: frame.cardId,
105
- }))
392
+ if (isKeyboardShortcutMessage(msg, identity)) {
393
+ if (msg.phase === "keydown") {
394
+ const nextTool = canvasToolForCode(msg.code);
395
+ if (nextTool && (nextTool !== "sketch" || sketchAvailable))
396
+ handleToolChange(nextTool);
397
+ }
106
398
  return;
107
- const identity = {
108
- projectId: PROJECT_ID,
109
- workspaceId: WORKSPACE_ID,
110
- cardId: frame.cardId,
399
+ }
400
+ const frame = { cardId, iframe };
401
+ const syncActiveFrameUrl = (url) => {
402
+ updateCardUrl(frame.cardId, url);
403
+ const activeCardId = getSelectedCardId() ?? getFocusedCardId() ?? getCanvasCards()[0]?.id;
404
+ if (activeCardId !== frame.cardId && getCanvasCards().length !== 1)
405
+ return;
406
+ window.history.replaceState(window.history.state, "", createNudgeUiEditorUrl(url));
111
407
  };
112
- if (isProjectionAppliedMessage(event.data, identity)) {
113
- recordCanvasProjectionApplied(frame.cardId, event.data.revision);
408
+ if (isProjectionAppliedMessage(msg, identity)) {
409
+ recordCanvasProjectionApplied(frame.cardId, msg.revision);
410
+ // While hold-to-view-original is active the frame intentionally shows
411
+ // the page without inspector changes; skip conflict verification.
412
+ if (isOriginalPreviewActive())
413
+ return;
414
+ const frameDocument = frame.iframe.contentDocument;
415
+ if (frameDocument && isCanvasCanonicalProjectionRevisionCurrent(frameDocument, msg.revision)) {
416
+ const attempt = beginPreviewAttempt(getCanvasPreviewDocument(frame.cardId), getWorkspaceChanges().revision);
417
+ if (attempt) {
418
+ for (const change of getChangesList()) {
419
+ if (!isPreviewableChange(change))
420
+ continue;
421
+ const result = verifyManagedStyleProjection(change, null, frameDocument);
422
+ if (result) {
423
+ publishPreviewDiagnostic(attempt, changeKey(change), result);
424
+ if (result.status === "conflict" && result.reason === "animation") {
425
+ window.setTimeout(() => {
426
+ if (!isCanvasCanonicalProjectionRevisionCurrent(frameDocument, msg.revision))
427
+ return;
428
+ const settled = verifyManagedStyleProjection(change, null, frameDocument);
429
+ if (settled)
430
+ publishPreviewDiagnostic(attempt, changeKey(change), settled);
431
+ }, 300);
432
+ }
433
+ }
434
+ }
435
+ }
436
+ }
114
437
  return;
115
438
  }
116
- if (isStructuralProjectionReportMessage(event.data, identity)) {
117
- recordCanvasStructuralProjectionReports(frame.cardId, event.data.revision, event.data.reports);
439
+ if (isStructuralProjectionReportMessage(msg, identity)) {
440
+ recordCanvasStructuralProjectionReports(frame.cardId, msg.revision, msg.reports);
118
441
  return;
119
442
  }
120
- if (isRenderedInstanceProjectionReportMessage(event.data, identity)) {
121
- recordCanvasRenderedInstanceProjectionReports(frame.cardId, event.data.revision, event.data.reports);
443
+ if (isRenderedInstanceProjectionReportMessage(msg, identity)) {
444
+ recordCanvasRenderedInstanceProjectionReports(frame.cardId, msg.revision, msg.reports);
122
445
  return;
123
446
  }
124
- if (isTextProjectionReportMessage(event.data, identity)) {
125
- recordCanvasTextProjectionReports(frame.cardId, event.data.revision, event.data.reports);
447
+ if (isTextProjectionReportMessage(msg, identity)) {
448
+ recordCanvasTextProjectionReports(frame.cardId, msg.revision, msg.reports);
126
449
  return;
127
450
  }
128
- // SAFETY: isRendererMessageFor validated the frame identity and message shape above.
129
- const msg = event.data;
130
451
  if (msg.type === "frame-ready") {
131
452
  acknowledgeAgentRendererReady(frame.cardId);
453
+ syncActiveFrameUrl(msg.url);
132
454
  // A card can finish loading after Space was pressed on the controller.
133
455
  // Seed it with the current modifier state before its first pointer event.
134
456
  sendPanModifier(frame.iframe, frame.cardId, spaceHeldRef.current);
457
+ sendInspectorInteractionState(frame.iframe, frame.cardId, inspectorOpen);
458
+ sendBoardGestureState(frame.iframe, frame.cardId, presentation === "canvas");
135
459
  }
136
460
  if (msg.type === "pan-modifier") {
461
+ if (presentation !== "canvas")
462
+ return;
137
463
  spaceHeldRef.current = msg.spaceHeld;
138
464
  if (!panningRef.current) {
139
465
  setBoardCursorClass(msg.spaceHeld ? "is-grabbable" : "");
@@ -142,6 +468,8 @@ export function CanvasWorkspace() {
142
468
  return;
143
469
  }
144
470
  if (msg.type === "zoom") {
471
+ if (presentation !== "canvas")
472
+ return;
145
473
  const iframeRect = frame.iframe.getBoundingClientRect();
146
474
  const point = iframePointToClientPoint(iframeRect, msg.point, getBoardCamera().zoom);
147
475
  zoomAtPointer(point, msg.deltaY);
@@ -179,12 +507,12 @@ export function CanvasWorkspace() {
179
507
  endPanning();
180
508
  return;
181
509
  }
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;
510
+ if (msg.type === "frame-metadata") {
511
+ const frameDocument = frame.iframe.contentDocument;
512
+ if (frameDocument)
513
+ disposeInlineTextEdit("route-disposed", frameDocument);
514
+ if (msg.url)
515
+ syncActiveFrameUrl(msg.url);
188
516
  return;
189
517
  }
190
518
  if (msg.type !== "navigation-intent")
@@ -192,17 +520,11 @@ export function CanvasWorkspace() {
192
520
  const normalized = normalizeUrl(msg.url);
193
521
  if (!normalized || normalized.origin !== window.location.origin)
194
522
  return;
195
- const existing = findCardByNormalizedUrl(normalized);
196
- if (existing) {
197
- focusCard(existing.id);
198
- }
199
- else {
200
- addCanvasCard(msg.url);
201
- }
202
- }
203
- window.addEventListener("message", onMessage);
204
- return () => window.removeEventListener("message", onMessage);
205
- }, [broadcastPanModifier, endPanning, movePanning, sendPanModifier, startPanning, zoomAtPointer]);
523
+ const frameDocument = frame.iframe.contentDocument;
524
+ if (frameDocument)
525
+ disposeInlineTextEdit("route-disposed", frameDocument);
526
+ });
527
+ }, [broadcastPanModifier, endPanning, handleToolChange, inspectorOpen, movePanning, presentation, sendBoardGestureState, sendInspectorInteractionState, sendPanModifier, sketchAvailable, startPanning, zoomAtPointer]);
206
528
  useEffect(() => {
207
529
  if (mode === "canvas" && !hasFitAllRan()) {
208
530
  if (!fitAllScheduledRef.current) {
@@ -216,7 +538,7 @@ export function CanvasWorkspace() {
216
538
  }, [mode, cards.length, inspectorOpen, fitCanvasToBoard]);
217
539
  useEffect(() => {
218
540
  function onKeyDown(e) {
219
- if (mode !== "canvas")
541
+ if (mode !== "canvas" || presentation !== "canvas" || sketchActive)
220
542
  return;
221
543
  if ((e.key === "Delete" || e.key === "Backspace") && !isEditableEvent(e)) {
222
544
  const selectedCardId = getSelectedCardId();
@@ -227,9 +549,7 @@ export function CanvasWorkspace() {
227
549
  return;
228
550
  }
229
551
  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))
552
+ if (isEditableEvent(e))
233
553
  return;
234
554
  e.preventDefault();
235
555
  spaceHeldRef.current = true;
@@ -241,10 +561,10 @@ export function CanvasWorkspace() {
241
561
  }
242
562
  function onKeyUp(e) {
243
563
  if (e.code === "Space") {
244
- spaceHeldRef.current = false;
245
- broadcastPanModifier(false);
564
+ spaceHeldRef.current = interactionTool === "pan";
565
+ broadcastPanModifier(spaceHeldRef.current);
246
566
  if (!panningRef.current) {
247
- setBoardCursorClass("");
567
+ setBoardCursorClass(spaceHeldRef.current ? "is-grabbable" : "");
248
568
  }
249
569
  }
250
570
  }
@@ -254,8 +574,10 @@ export function CanvasWorkspace() {
254
574
  window.removeEventListener("keydown", onKeyDown, true);
255
575
  window.removeEventListener("keyup", onKeyUp, true);
256
576
  };
257
- }, [broadcastPanModifier, mode]);
577
+ }, [broadcastPanModifier, interactionTool, mode, presentation, sketchActive]);
258
578
  const handleBoardPointerDown = useCallback((e) => {
579
+ if (presentation !== "canvas" || sketchActive)
580
+ return;
259
581
  if (panningRef.current)
260
582
  return;
261
583
  if (!spaceHeldRef.current)
@@ -275,9 +597,9 @@ export function CanvasWorkspace() {
275
597
  }
276
598
  window.addEventListener("pointermove", onMove);
277
599
  window.addEventListener("pointerup", onUp);
278
- }, [endPanning, movePanning, startPanning]);
600
+ }, [endPanning, movePanning, presentation, sketchActive, startPanning]);
279
601
  useEffect(() => {
280
- if (mode !== "canvas")
602
+ if (mode !== "canvas" || presentation !== "canvas" || sketchActive)
281
603
  return;
282
604
  function handleGlobalWheel(e) {
283
605
  if (!e.ctrlKey && !e.metaKey)
@@ -288,14 +610,11 @@ export function CanvasWorkspace() {
288
610
  }
289
611
  window.addEventListener("wheel", handleGlobalWheel, { capture: true, passive: false });
290
612
  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)
613
+ }, [mode, presentation, sketchActive, zoomAtPointer]);
614
+ if (mode !== "canvas")
296
615
  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))) }) }) })] }));
616
+ 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: {
617
+ transform: `translate(${camera.x}px, ${camera.y}px) scale(${camera.zoom})`,
618
+ transformOrigin: "0 0",
619
+ }, "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
620
  }
@@ -0,0 +1,5 @@
1
+ /** Resolves only the selected or focused frame. */
2
+ export declare function resolveActiveCanvasFrame(frames: ReadonlyMap<string, HTMLIFrameElement>, selectedCardId: string | null, focusedCardId: string | null): HTMLIFrameElement | null;
3
+ export declare function getActiveCanvasDocument(): Document | null;
4
+ /** Returns the iframe that owns the active renderer document. */
5
+ export declare function getActiveCanvasFrame(): HTMLIFrameElement | null;