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,11 +1,10 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import { CANVAS_RENDERER_ATTR } from "./roleDetection.js";
4
- import { removeCanvasCard, duplicateCard, updateCardTitle, updateCardUrl, resizeCard, setCardPosition, selectCard, getSelectedCardId, useSelectedCardId, useFocusedCardId, useBoardCamera } from "./canvasStore.js";
5
- import { IconRefresh, IconPlayerPlay, IconCopy, IconArrowsDiagonal } from "@tabler/icons-react";
6
- import { PROTOCOL_VERSION, isRendererMessageFor, } from "./frameProtocol.js";
4
+ import { removeCanvasCard, updateCardTitle, updateCardUrl, resizeCard, setCardPosition, selectCard, getSelectedCardId, useSelectedCardId, useFocusedCardId, useBoardCamera } from "./canvasStore.js";
5
+ import { IconRefresh, IconArrowsDiagonal, IconCornerLeftDown } from "@tabler/icons-react";
6
+ import { PROTOCOL_VERSION, } from "./frameProtocol.js";
7
7
  import { registerCardFrame, registerCardFrameSource, unregisterCardFrame, sendProjectionToCard, invalidateCanvasPreviewDocument, PROJECT_ID, WORKSPACE_ID } from "./projection.js";
8
- import { IconButton } from "../ui/IconButton.js";
9
8
  import { Button } from "../ui/Button.js";
10
9
  import { setSelectedElement } from "../selection/selectionStore.js";
11
10
  import { clearCanvasStructuralProjectionReports } from "../projection/structuralProjection.js";
@@ -15,11 +14,36 @@ import { getCanvasToolbarScale } from "./toolbarScale.js";
15
14
  import { getCanvasResizeHandleScale } from "./resizeHandleScale.js";
16
15
  import { disposeBrowserCssInspection } from "../inspection/browserCssInspectionRegistry.js";
17
16
  import { releaseDocumentProjection } from "../projection/structuralProjection.js";
18
- const MIN_CARD_WIDTH = 200;
19
- const MIN_CARD_HEIGHT = 150;
20
- export function CanvasCard({ card, onEdit, documentOwner }) {
17
+ import { startClipboardHandoffController } from "../prompt/clipboardHandoff.js";
18
+ import { disposeInlineTextEdit } from "../inline-text/inlineTextEditor.js";
19
+ import { configureNudgeUiRuntime, getNudgeUiRuntimeConfig } from "../runtime/runtimeConfig.js";
20
+ import { reconcileRuntimeWithDocumentStylesheets } from "../runtime/documentStylesheetOrder.js";
21
+ import { subscribeCanvasRendererMessages } from "./rendererMessageRouter.js";
22
+ import { SketchFrameOverlay } from "../sketch/SketchFrameOverlay.js";
23
+ import { getCanvasResizeEdges, resizeCanvasRect, } from "./canvasResize.js";
24
+ const RESIZE_HANDLES = [
25
+ { direction: "top-left", cursor: "nwse-resize", transformOrigin: "left top", label: "Resize card from the top-left corner" },
26
+ { direction: "top", cursor: "ns-resize", transformOrigin: "center top", label: "Resize card from the top edge" },
27
+ { direction: "top-right", cursor: "nesw-resize", transformOrigin: "right top", label: "Resize card from the top-right corner" },
28
+ { direction: "right", cursor: "ew-resize", transformOrigin: "right center", label: "Resize card from the right edge" },
29
+ { direction: "bottom-right", cursor: "nwse-resize", transformOrigin: "right bottom", label: "Resize card from the bottom-right corner" },
30
+ { direction: "bottom", cursor: "ns-resize", transformOrigin: "center bottom", label: "Resize card from the bottom edge" },
31
+ { direction: "bottom-left", cursor: "nesw-resize", transformOrigin: "left bottom", label: "Resize card from the bottom-left corner" },
32
+ { direction: "left", cursor: "ew-resize", transformOrigin: "left center", label: "Resize card from the left edge" },
33
+ ];
34
+ function resizeHandleTransform(direction, scale) {
35
+ if (direction === "top" || direction === "bottom")
36
+ return `scale(1, ${scale})`;
37
+ if (direction === "left" || direction === "right")
38
+ return `scale(${scale}, 1)`;
39
+ return `scale(${scale})`;
40
+ }
41
+ export function CanvasCard({ card, presentation = "canvas", presentationCard = true, onShowFocus, documentOwner }) {
21
42
  const iframeRef = useRef(null);
43
+ const initialUrlRef = useRef(card.navigationUrl ?? card.url);
44
+ const navigationUrlRef = useRef(card.navigationUrl);
22
45
  const documentSessionRef = useRef(null);
46
+ const frameRuntimeRef = useRef(null);
23
47
  const [loadState, setLoadState] = useState("loading");
24
48
  const [errorMessage, setErrorMessage] = useState(null);
25
49
  const camera = useBoardCamera();
@@ -27,12 +51,25 @@ export function CanvasCard({ card, onEdit, documentOwner }) {
27
51
  const focusedCardId = useFocusedCardId();
28
52
  const isSelected = selectedCardId === card.id;
29
53
  const isFocused = focusedCardId === card.id;
54
+ const isFocusPresentationCard = presentation === "focus" && presentationCard;
30
55
  const toolbarScale = getCanvasToolbarScale(camera.zoom);
31
56
  const resizeHandleScale = getCanvasResizeHandleScale(camera.zoom);
32
57
  const disposeDocumentSession = useCallback(() => {
33
58
  documentSessionRef.current?.session.dispose();
34
59
  documentSessionRef.current = null;
35
60
  }, []);
61
+ const loadFrame = useCallback((url) => {
62
+ const iframe = iframeRef.current;
63
+ if (!iframe)
64
+ return;
65
+ if (getSelectedCardId() === card.id)
66
+ setSelectedElement(null);
67
+ disposeDocumentSession();
68
+ invalidateCanvasPreviewDocument(card.id);
69
+ setLoadState("loading");
70
+ setErrorMessage(null);
71
+ iframe.setAttribute("src", url ?? iframe.src);
72
+ }, [card.id, disposeDocumentSession]);
36
73
  const bindDocumentSession = useCallback(() => {
37
74
  const frameDocument = iframeRef.current?.contentDocument;
38
75
  if (!documentOwner || !frameDocument)
@@ -43,6 +80,8 @@ export function CanvasCard({ card, onEdit, documentOwner }) {
43
80
  const session = documentOwner.createDocumentSession(frameDocument);
44
81
  session.registerCleanup(() => disposeBrowserCssInspection(frameDocument));
45
82
  session.registerCleanup(() => releaseDocumentProjection(frameDocument));
83
+ session.registerCleanup(() => disposeInlineTextEdit("frame-disposed", frameDocument));
84
+ session.registerCleanup(startClipboardHandoffController(frameDocument));
46
85
  documentSessionRef.current = { document: frameDocument, session };
47
86
  }, [disposeDocumentSession, documentOwner]);
48
87
  useEffect(() => {
@@ -57,27 +96,20 @@ export function CanvasCard({ card, onEdit, documentOwner }) {
57
96
  };
58
97
  }, [bindDocumentSession, card.id, disposeDocumentSession]);
59
98
  function handleReload() {
60
- if (iframeRef.current) {
61
- if (getSelectedCardId() === card.id)
62
- setSelectedElement(null);
63
- disposeDocumentSession();
64
- invalidateCanvasPreviewDocument(card.id);
65
- setLoadState("loading");
66
- setErrorMessage(null);
67
- const currentSrc = iframeRef.current.src;
68
- iframeRef.current.setAttribute("src", currentSrc);
69
- }
99
+ loadFrame();
70
100
  }
71
101
  function handleRemove() {
72
102
  if (getSelectedCardId() === card.id)
73
103
  setSelectedElement(null);
74
104
  removeCanvasCard(card.id);
75
105
  }
76
- function handleEdit() {
77
- onEdit?.(card);
78
- }
79
- function handleDuplicate() {
80
- duplicateCard(card.id);
106
+ function updateFrameTitle(title) {
107
+ if (!title)
108
+ return;
109
+ const runtime = getNudgeUiRuntimeConfig();
110
+ if (runtime.demo === true && (runtime.demoCardLabels?.length ?? 0) > 0)
111
+ return;
112
+ updateCardTitle(card.id, title);
81
113
  }
82
114
  /** The controller half of the handshake: announce workspace identity. */
83
115
  function sendParentReady() {
@@ -89,39 +121,44 @@ export function CanvasCard({ card, onEdit, documentOwner }) {
89
121
  cardId: card.id,
90
122
  }, window.location.origin);
91
123
  }
124
+ function adoptFrameRuntime(runtime) {
125
+ frameRuntimeRef.current = runtime;
126
+ if (getSelectedCardId() !== card.id)
127
+ return;
128
+ const frameDocument = iframeRef.current?.contentDocument;
129
+ if (!frameDocument)
130
+ return;
131
+ try {
132
+ configureNudgeUiRuntime(runtime);
133
+ const configured = getNudgeUiRuntimeConfig();
134
+ const reconciled = reconcileRuntimeWithDocumentStylesheets(configured, frameDocument);
135
+ if (reconciled !== configured)
136
+ configureNudgeUiRuntime(reconciled);
137
+ }
138
+ catch {
139
+ // Ignore malformed same-origin renderer metadata and keep the last valid runtime.
140
+ }
141
+ }
92
142
  useEffect(() => {
93
- function onMessage(event) {
94
- if (event.origin !== window.location.origin)
95
- return;
96
- if (event.source !== iframeRef.current?.contentWindow)
97
- return;
98
- const msg = event.data;
99
- if (!msg || typeof msg !== "object")
143
+ if (!isSelected || !frameRuntimeRef.current)
144
+ return;
145
+ adoptFrameRuntime(frameRuntimeRef.current);
146
+ }, [isSelected]);
147
+ useEffect(() => {
148
+ return subscribeCanvasRendererMessages(({ cardId, message: data }) => {
149
+ if (cardId !== card.id)
100
150
  return;
101
151
  // A renderer whose runtime finished booting after the iframe load event
102
152
  // asks for the identity announcement it may have missed.
103
- // SAFETY: Renderer messages arrive as unvalidated structured clones, so the discriminant must be read structurally.
104
- if (msg.type === "renderer-hello") {
105
- // SAFETY: The `type === "renderer-hello"` branch above selects exactly the hello payload shape.
106
- const hello = msg;
107
- if (hello.protocolVersion !== PROTOCOL_VERSION)
108
- return;
153
+ if (data.type === "renderer-hello") {
109
154
  sendParentReady();
110
155
  return;
111
156
  }
112
- if (!isRendererMessageFor(msg, {
113
- projectId: PROJECT_ID,
114
- workspaceId: WORKSPACE_ID,
115
- cardId: card.id,
116
- }))
117
- return;
118
- // SAFETY: isRendererMessageFor validated the frame identity and message shape above.
119
- const data = msg;
120
157
  if (data.type === "frame-ready") {
158
+ adoptFrameRuntime(data.runtime);
121
159
  setLoadState("ready");
122
160
  setErrorMessage(null);
123
- if (data.title)
124
- updateCardTitle(card.id, data.title);
161
+ updateFrameTitle(data.title);
125
162
  if (data.url)
126
163
  updateCardUrl(card.id, data.url);
127
164
  if (iframeRef.current) {
@@ -130,9 +167,12 @@ export function CanvasCard({ card, onEdit, documentOwner }) {
130
167
  }
131
168
  return;
132
169
  }
170
+ if (data.type === "frame-runtime") {
171
+ adoptFrameRuntime(data.runtime);
172
+ return;
173
+ }
133
174
  if (data.type === "frame-metadata") {
134
- if (data.title)
135
- updateCardTitle(card.id, data.title);
175
+ updateFrameTitle(data.title);
136
176
  if (data.url)
137
177
  updateCardUrl(card.id, data.url);
138
178
  return;
@@ -140,14 +180,15 @@ export function CanvasCard({ card, onEdit, documentOwner }) {
140
180
  if (data.type === "frame-error") {
141
181
  setLoadState("error");
142
182
  setErrorMessage(data.message || "Frame failed to load");
143
- return;
144
183
  }
145
- }
146
- window.addEventListener("message", onMessage);
147
- return () => {
148
- window.removeEventListener("message", onMessage);
149
- };
184
+ });
150
185
  }, [card.id]);
186
+ useLayoutEffect(() => {
187
+ if (!card.navigationUrl || navigationUrlRef.current === card.navigationUrl)
188
+ return;
189
+ navigationUrlRef.current = card.navigationUrl;
190
+ loadFrame(card.navigationUrl);
191
+ }, [card.navigationUrl, loadFrame]);
151
192
  useEffect(() => {
152
193
  if (loadState !== "loading")
153
194
  return;
@@ -182,6 +223,10 @@ export function CanvasCard({ card, onEdit, documentOwner }) {
182
223
  }, [bindDocumentSession, card.id, disposeDocumentSession]);
183
224
  const [isDragging, setIsDragging] = useState(false);
184
225
  const dragRef = useRef({ startX: 0, startY: 0, cardX: 0, cardY: 0 });
226
+ const resizeCleanupRef = useRef(null);
227
+ useEffect(() => {
228
+ return () => resizeCleanupRef.current?.();
229
+ }, []);
185
230
  const handleToolbarPointerDown = useCallback((e) => {
186
231
  // SAFETY: pointer events on the card target are HTMLElements in this DOM context.
187
232
  const target = e.target;
@@ -219,57 +264,102 @@ export function CanvasCard({ card, onEdit, documentOwner }) {
219
264
  setSelectedElement(null);
220
265
  selectCard(card.id);
221
266
  }, [card.id]);
222
- const handleResizeStart = useCallback((e) => {
267
+ const handleResizeStart = useCallback((e, direction) => {
223
268
  e.stopPropagation();
224
269
  e.preventDefault();
270
+ resizeCleanupRef.current?.();
271
+ const target = e.currentTarget;
272
+ const pointerId = e.pointerId;
225
273
  const startX = e.clientX;
226
274
  const startY = e.clientY;
227
- const startWidth = card.width;
228
- const startHeight = card.height;
229
- // SAFETY: pointerdown targets are HTMLElements in the card DOM.
230
- e.target.setPointerCapture(e.pointerId);
275
+ const start = {
276
+ x: card.x,
277
+ y: card.y,
278
+ width: card.width,
279
+ height: card.height,
280
+ };
231
281
  function onMove(ev) {
232
- const dx = (ev.clientX - startX) / camera.zoom;
233
- const dy = (ev.clientY - startY) / camera.zoom;
234
- const newWidth = Math.max(MIN_CARD_WIDTH, startWidth + dx);
235
- const newHeight = Math.max(MIN_CARD_HEIGHT, startHeight + dy);
236
- resizeCard(card.id, newWidth, newHeight);
282
+ if (ev.pointerId !== pointerId)
283
+ return;
284
+ const delta = {
285
+ x: (ev.clientX - startX) / camera.zoom,
286
+ y: (ev.clientY - startY) / camera.zoom,
287
+ };
288
+ const next = resizeCanvasRect(start, direction, delta);
289
+ resizeCard(card.id, next.width, next.height, { x: next.x, y: next.y });
237
290
  }
238
- function onUp() {
291
+ function cleanup() {
239
292
  window.removeEventListener("pointermove", onMove);
240
293
  window.removeEventListener("pointerup", onUp);
294
+ window.removeEventListener("pointercancel", onCancel);
295
+ target.removeEventListener("lostpointercapture", onLostPointerCapture);
296
+ if (target.hasPointerCapture?.(pointerId))
297
+ target.releasePointerCapture(pointerId);
298
+ if (resizeCleanupRef.current === cleanup)
299
+ resizeCleanupRef.current = null;
300
+ }
301
+ function onUp(ev) {
302
+ if (ev.pointerId !== pointerId)
303
+ return;
304
+ cleanup();
305
+ }
306
+ function onCancel(ev) {
307
+ if (ev.pointerId !== pointerId)
308
+ return;
309
+ cleanup();
310
+ }
311
+ function onLostPointerCapture(ev) {
312
+ if (ev.pointerId !== pointerId)
313
+ return;
314
+ cleanup();
241
315
  }
316
+ resizeCleanupRef.current = cleanup;
317
+ // SAFETY: the resize handle is an HTMLDivElement in this DOM context.
318
+ target.setPointerCapture?.(pointerId);
242
319
  window.addEventListener("pointermove", onMove);
243
320
  window.addEventListener("pointerup", onUp);
244
- }, [card.id, card.width, card.height, camera.zoom]);
245
- function handleResizeKeyDown(event) {
246
- const step = event.shiftKey ? 40 : 20;
247
- if (event.key === "ArrowRight")
248
- resizeCard(card.id, card.width + step, card.height);
249
- else if (event.key === "ArrowLeft")
250
- resizeCard(card.id, Math.max(MIN_CARD_WIDTH, card.width - step), card.height);
251
- else if (event.key === "ArrowDown")
252
- resizeCard(card.id, card.width, card.height + step);
253
- else if (event.key === "ArrowUp")
254
- resizeCard(card.id, card.width, Math.max(MIN_CARD_HEIGHT, card.height - step));
255
- else
321
+ window.addEventListener("pointercancel", onCancel);
322
+ target.addEventListener("lostpointercapture", onLostPointerCapture);
323
+ }, [card, camera.zoom]);
324
+ function handleResizeKeyDown(event, direction) {
325
+ const isArrowKey = event.key === "ArrowRight"
326
+ || event.key === "ArrowLeft"
327
+ || event.key === "ArrowDown"
328
+ || event.key === "ArrowUp";
329
+ if (!isArrowKey)
256
330
  return;
257
331
  event.preventDefault();
332
+ event.stopPropagation();
333
+ const step = event.shiftKey ? 40 : 20;
334
+ const delta = {
335
+ x: event.key === "ArrowRight" ? step : event.key === "ArrowLeft" ? -step : 0,
336
+ y: event.key === "ArrowDown" ? step : event.key === "ArrowUp" ? -step : 0,
337
+ };
338
+ const edges = getCanvasResizeEdges(direction);
339
+ if ((delta.x !== 0 && edges.horizontal === null) || (delta.y !== 0 && edges.vertical === null))
340
+ return;
341
+ const next = resizeCanvasRect(card, direction, delta);
342
+ resizeCard(card.id, next.width, next.height, { x: next.x, y: next.y });
258
343
  }
259
- return (_jsxs("div", { className: `canvas-card${isDragging ? " is-dragging" : ""}${isSelected ? " is-selected" : ""}${isFocused ? " is-focused" : ""}`, "data-card-id": card.id, style: {
344
+ return (_jsxs("div", { className: `canvas-card${isDragging ? " is-dragging" : ""}${isSelected ? " is-selected" : ""}${isFocused ? " is-focused" : ""}${isFocusPresentationCard ? " is-focus-presentation" : ""}${presentationCard ? " is-presentation-card" : ""}`, "data-card-id": card.id, style: {
260
345
  position: "absolute",
261
- left: card.x,
262
- top: card.y,
263
- width: card.width,
264
- height: card.height,
265
- }, onPointerDown: handleCardPointerDown, children: [_jsxs("div", { className: "canvas-card__toolbar", onPointerDown: handleToolbarPointerDown, children: [_jsx("div", { className: "canvas-card__drag-surface", "data-test": `canvas-card-drag-${card.id}`, children: _jsxs("span", { className: "canvas-card__dimensions", "data-test": `canvas-card-dimensions-${card.id}`, style: {
346
+ visibility: presentation === "focus" && !presentationCard ? "hidden" : undefined,
347
+ pointerEvents: presentation === "focus" && !presentationCard ? "none" : undefined,
348
+ left: isFocusPresentationCard ? 0 : card.x,
349
+ top: isFocusPresentationCard ? 0 : card.y,
350
+ width: isFocusPresentationCard ? "100%" : card.width,
351
+ height: isFocusPresentationCard ? "100%" : card.height,
352
+ }, onPointerDown: presentation === "canvas" ? handleCardPointerDown : undefined, children: [presentation === "canvas" ? _jsx("div", { className: "canvas-card__toolbar", onPointerDown: handleToolbarPointerDown, children: _jsxs("div", { className: "canvas-card__drag-surface", "data-test": `canvas-card-drag-${card.id}`, children: [onShowFocus ? (_jsxs(Button, { title: "Focus", variant: "primary", size: "default", "data-test": `canvas-card-focus-${card.id}`, style: {
266
353
  transform: `scale(${toolbarScale})`,
267
354
  transformOrigin: "left bottom",
268
- }, children: [Math.round(card.width), " \u00D7 ", Math.round(card.height), " px"] }) }), _jsxs("div", { className: "canvas-card__actions", style: {
269
- transform: `scale(${toolbarScale})`,
270
- transformOrigin: "right bottom",
271
- }, children: [_jsxs(Button, { variant: "secondary", size: "default", "data-test": `canvas-card-preview-${card.id}`, onClick: handleEdit, children: [_jsx(IconPlayerPlay, { size: 14, stroke: 1.8, "aria-hidden": "true" }), "Page view"] }), _jsx(IconButton, { label: "Duplicate card", variant: "secondary", size: "default", "data-test": `canvas-card-duplicate-${card.id}`, onClick: handleDuplicate, children: _jsx(IconCopy, { size: 14, stroke: 1.8, "aria-hidden": "true" }) }), _jsx(IconButton, { label: "Reload card", variant: "secondary", size: "default", "data-test": `canvas-card-reload-${card.id}`, onClick: handleReload, children: _jsx(IconRefresh, { size: 14, stroke: 1.8, "aria-hidden": "true" }) })] })] }), _jsxs("div", { className: "canvas-card__frame", children: [loadState === "loading" ? (_jsx("div", { className: "canvas-card__loading", "data-test": `canvas-card-loading-${card.id}`, children: "Loading preview..." })) : loadState === "error" ? (_jsxs("div", { className: "canvas-card__error", "data-test": `canvas-card-error-${card.id}`, children: [_jsx("p", { children: errorMessage || "Frame could not be loaded" }), _jsxs("div", { className: "canvas-card__error-actions", children: [_jsx(Button, { size: "compact", variant: "secondary", onClick: handleReload, children: "Retry" }), _jsx(Button, { size: "compact", variant: "danger", onClick: handleRemove, children: "Remove" })] })] })) : null, _jsx("iframe", { ref: iframeRef, className: "canvas-card__iframe", src: card.url, title: card.title || card.url, [CANVAS_RENDERER_ATTR]: "", sandbox: "allow-scripts allow-same-origin allow-forms allow-popups", "data-test": `canvas-card-iframe-${card.id}` })] }), _jsx("div", { className: "canvas-card__resize-handle", "data-test": `canvas-card-resize-${card.id}`, style: {
272
- transform: `scale(${resizeHandleScale})`,
273
- transformOrigin: "right bottom",
274
- }, onPointerDown: handleResizeStart, onKeyDown: handleResizeKeyDown, role: "button", "aria-label": "Resize card", tabIndex: 0, children: _jsx(IconArrowsDiagonal, { size: 12, "aria-hidden": "true" }) })] }));
355
+ }, onClick: () => onShowFocus(card.id), children: [_jsx(IconCornerLeftDown, { size: "var(--icon-size-small)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), "Focus"] })) : null, _jsx("span", { className: "canvas-card__dimensions", "data-test": `canvas-card-dimensions-${card.id}`, style: {
356
+ transform: `translateX(-50%) scale(${toolbarScale})`,
357
+ transformOrigin: "center bottom",
358
+ }, children: card.title || `${Math.round(card.width)} × ${Math.round(card.height)} px` })] }) }) : null, _jsxs("div", { className: "canvas-card__frame", children: [loadState === "loading" ? (_jsx("div", { className: "canvas-card__loading", "data-test": `canvas-card-loading-${card.id}`, children: "Loading preview..." })) : loadState === "error" ? (_jsxs("div", { className: "canvas-card__error", "data-test": `canvas-card-error-${card.id}`, children: [_jsx("p", { children: errorMessage || "Frame could not be loaded" }), _jsxs("div", { className: "canvas-card__error-actions", children: [_jsx(Button, { size: "compact", variant: "secondary", onClick: handleReload, children: "Retry" }), _jsx(Button, { size: "compact", variant: "danger", onClick: handleRemove, children: "Remove" })] })] })) : null, _jsx("iframe", { ref: iframeRef, className: "canvas-card__iframe", src: initialUrlRef.current, title: card.title || card.url, [CANVAS_RENDERER_ATTR]: "", sandbox: "allow-scripts allow-same-origin allow-forms allow-popups", "data-test": `canvas-card-iframe-${card.id}` }), _jsx(SketchFrameOverlay, { iframe: iframeRef.current, cardUrl: card.url, ready: loadState === "ready" })] }), presentation === "canvas" ? RESIZE_HANDLES.map((handle) => (_jsx("div", { className: `canvas-card__resize-handle canvas-card__resize-handle--${handle.direction}`, "data-test": `canvas-card-resize-${card.id}${handle.direction === "bottom-right" ? "" : `-${handle.direction}`}`, "data-resize-direction": handle.direction, style: {
359
+ // Keep the edge's long axis aligned with the zoomed card. Only
360
+ // the thickness needs inverse scaling to remain screen-sized.
361
+ transform: resizeHandleTransform(handle.direction, resizeHandleScale),
362
+ transformOrigin: handle.transformOrigin,
363
+ cursor: handle.cursor,
364
+ }, onPointerDown: (event) => handleResizeStart(event, handle.direction), onKeyDown: (event) => handleResizeKeyDown(event, handle.direction), role: "button", "aria-label": handle.direction === "bottom-right" ? "Resize card" : handle.label, tabIndex: 0, children: handle.direction === "bottom-right" ? _jsx(IconArrowsDiagonal, { size: 12, "aria-hidden": "true" }) : null }, handle.direction))) : null] }));
275
365
  }