nudge-ui 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/client.mjs +16 -9
  2. package/dist/hosts/astro/clientTransport.js +3 -0
  3. package/dist/hosts/astro/integration.d.ts +2 -2
  4. package/dist/hosts/astro/integration.js +4 -3
  5. package/dist/hosts/environment.d.ts +15 -0
  6. package/dist/hosts/environment.js +16 -0
  7. package/dist/hosts/next/sidecar.js +7 -1
  8. package/dist/hosts/next/wrapper.d.ts +2 -0
  9. package/dist/hosts/next/wrapper.js +2 -1
  10. package/dist/hosts/projectBridge.d.ts +1 -0
  11. package/dist/hosts/projectBridge.js +1 -0
  12. package/dist/hosts/static/manifest.js +2 -0
  13. package/dist/hosts/static/server.js +1 -0
  14. package/dist/hosts/vite/vite.d.ts +1 -0
  15. package/dist/hosts/vite/vite.js +4 -1
  16. package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
  17. package/dist/inspector/agent/McpConnectionDialog.js +11 -4
  18. package/dist/inspector/agent/client.d.ts +6 -0
  19. package/dist/inspector/agent/client.js +36 -4
  20. package/dist/inspector/agent/connectionStatus.d.ts +3 -3
  21. package/dist/inspector/agent/connectionStatus.js +8 -1
  22. package/dist/inspector/agent/httpTransport.d.ts +6 -0
  23. package/dist/inspector/agent/httpTransport.js +41 -1
  24. package/dist/inspector/agent/protocol.d.ts +3 -0
  25. package/dist/inspector/canvas/CanvasElementOverlay.js +49 -7
  26. package/dist/inspector/canvas/CanvasToolbar.js +4 -4
  27. package/dist/inspector/canvas/CanvasWorkspace.js +30 -11
  28. package/dist/inspector/canvas/frameProtocol.d.ts +16 -3
  29. package/dist/inspector/canvas/frameProtocol.js +9 -2
  30. package/dist/inspector/canvas/keyboardShortcuts.d.ts +2 -1
  31. package/dist/inspector/canvas/keyboardShortcuts.js +3 -1
  32. package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
  33. package/dist/inspector/canvas/linkEligibility.js +0 -29
  34. package/dist/inspector/canvas/rendererBootstrap.js +13 -0
  35. package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
  36. package/dist/inspector/canvas/rendererElementSelector.js +55 -39
  37. package/dist/inspector/changes/workspaceChanges.d.ts +4 -0
  38. package/dist/inspector/changes/workspaceChanges.js +29 -3
  39. package/dist/inspector/clientManifest.d.ts +2 -0
  40. package/dist/inspector/clientManifest.js +1 -0
  41. package/dist/inspector/index.js +6 -8
  42. package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
  43. package/dist/inspector/overlay/DomNavigator.js +40 -0
  44. package/dist/inspector/overlay/InspectorOverlay.js +6 -2
  45. package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
  46. package/dist/inspector/overlay/clickPolicy.js +34 -3
  47. package/dist/inspector/overlay/elementSelector.d.ts +1 -1
  48. package/dist/inspector/overlay/elementSelector.js +18 -7
  49. package/dist/inspector/overlay/spacingGestures.d.ts +3 -3
  50. package/dist/inspector/overlay/spacingGestures.js +15 -17
  51. package/dist/inspector/runtime/runtimeConfig.d.ts +1 -1
  52. package/dist/inspector/selection/hierarchy.d.ts +6 -0
  53. package/dist/inspector/selection/hierarchy.js +8 -0
  54. package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
  55. package/dist/inspector/settings/SettingsDialog.js +2 -2
  56. package/dist/inspector/shell/ChangesLog.js +20 -20
  57. package/dist/inspector/shell/CopyPromptButton.js +9 -2
  58. package/dist/inspector/shell/InspectorShell.js +2 -5
  59. package/dist/inspector/sketch/store.js +48 -23
  60. package/dist/inspector/styleEditors/SpacingBox.js +9 -4
  61. package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
  62. package/dist/inspector/styleEditors/nudgeValue.js +9 -7
  63. package/dist/inspector/tokens/TokenField.js +21 -2
  64. package/dist/inspector/tokens/TokensPanel.js +2 -1
  65. package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
  66. package/dist/inspector/tokens/dragPointerLock.js +70 -0
  67. package/dist/inspector/ui/Disclosure.d.ts +22 -0
  68. package/dist/inspector/ui/Disclosure.js +13 -0
  69. package/dist/inspector/ui/dragCursor.d.ts +8 -0
  70. package/dist/inspector/ui/dragCursor.js +21 -0
  71. package/dist/inspector/ui/styles.js +11 -9
  72. package/dist/nudge-ui.mjs +15 -1
  73. package/dist/transport/editor.d.ts +20 -1
  74. package/dist/transport/editor.js +42 -11
  75. package/dist/transport/routes.d.ts +4 -1
  76. package/dist/transport/routes.js +4 -1
  77. package/package.json +1 -1
  78. package/dist/inspector/shell/DomNavigation.d.ts +0 -8
  79. package/dist/inspector/shell/DomNavigation.js +0 -30
@@ -18,6 +18,7 @@ function endpointValue(value) {
18
18
  statusUrl: stringValue(value.statusUrl),
19
19
  canvasAckUrl: stringValue(value.canvasAckUrl),
20
20
  disconnectUrl: stringValue(value.disconnectUrl),
21
+ takeoverUrl: stringValue(value.takeoverUrl),
21
22
  ...(value.autoConnect === true ? { autoConnect: true } : {}),
22
23
  };
23
24
  return Object.values(config).some((entry) => entry !== undefined) ? config : undefined;
@@ -96,6 +97,16 @@ function headers() {
96
97
  Accept: "application/json",
97
98
  };
98
99
  }
100
+ export class AgentBridgeHttpError extends Error {
101
+ status;
102
+ code;
103
+ constructor(status, message, code) {
104
+ super(message);
105
+ this.name = "AgentBridgeHttpError";
106
+ this.status = status;
107
+ this.code = code;
108
+ }
109
+ }
99
110
  async function readJson(response) {
100
111
  let payload;
101
112
  try {
@@ -107,7 +118,8 @@ async function readJson(response) {
107
118
  if (!response.ok) {
108
119
  const error = isRecord(payload) && isRecord(payload.error) ? payload.error : payload;
109
120
  const message = isRecord(error) ? stringValue(error.message) : undefined;
110
- throw new Error(message ?? `Agent bridge request failed (${response.status}).`);
121
+ const code = isRecord(error) ? stringValue(error.code) : undefined;
122
+ throw new AgentBridgeHttpError(response.status, message ?? `Agent bridge request failed (${response.status}).`, code);
111
123
  }
112
124
  return isRecord(payload) ? payload : {};
113
125
  }
@@ -253,6 +265,34 @@ export class HttpAgentBridgeTransport {
253
265
  status,
254
266
  };
255
267
  }
268
+ async takeOver(request, signal) {
269
+ const url = endpointUrl(this.config, request.projectId, "takeoverUrl", "/takeover", pageOrigin());
270
+ if (!url || typeof fetch !== "function")
271
+ throw new Error("Agent bridge is unavailable.");
272
+ const response = await fetch(url, {
273
+ method: "POST",
274
+ headers: { ...headers(), "Content-Type": "application/json" },
275
+ body: JSON.stringify(request),
276
+ signal,
277
+ });
278
+ const payload = await readJson(response);
279
+ if (payload.protocolVersion !== AGENT_PROTOCOL_VERSION
280
+ || payload.projectId !== request.projectId
281
+ || payload.origin !== request.origin
282
+ || typeof payload.sessionToken !== "string") {
283
+ throw new Error("Agent bridge returned an invalid takeover response.");
284
+ }
285
+ const status = statusPayload(payload.status);
286
+ if (!status)
287
+ throw new Error("Agent bridge returned an invalid status.");
288
+ return {
289
+ protocolVersion: AGENT_PROTOCOL_VERSION,
290
+ projectId: request.projectId,
291
+ origin: request.origin,
292
+ sessionToken: payload.sessionToken,
293
+ status,
294
+ };
295
+ }
256
296
  async restore(request, signal) {
257
297
  const url = endpointUrl(this.config, request.projectId, "statusUrl", "/status", pageOrigin());
258
298
  if (!url || typeof fetch !== "function")
@@ -18,6 +18,7 @@ export interface AgentBridgeEndpointConfig {
18
18
  readonly statusUrl?: string;
19
19
  readonly canvasAckUrl?: string;
20
20
  readonly disconnectUrl?: string;
21
+ readonly takeoverUrl?: string;
21
22
  /** Pair automatically when this endpoint came from the trusted dev host. */
22
23
  readonly autoConnect?: boolean;
23
24
  }
@@ -87,6 +88,8 @@ export interface AgentBridgeTransport {
87
88
  readonly openEvents: (request: AgentEventsRequest, handlers: AgentEventHandlers) => AgentEventSubscription;
88
89
  readonly dispatch: (request: AgentPromptDispatch, signal?: AbortSignal) => Promise<PromptDispatchResponse | null>;
89
90
  readonly disconnect?: (request: AgentDisconnectRequest, signal?: AbortSignal) => Promise<void>;
91
+ /** Deliberately revokes another browser pairing before pairing this page. */
92
+ readonly takeOver?: (request: AgentPairRequest, signal?: AbortSignal) => Promise<PairingResponse>;
90
93
  readonly acknowledgeCanvasCommand?: (request: AgentCanvasAcknowledgementRequest, signal?: AbortSignal) => Promise<void>;
91
94
  }
92
95
  export interface AgentClientOptions {
@@ -3,7 +3,7 @@ import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } fro
3
3
  import { PROTOCOL_VERSION, isElementClickMessage, isInlineTextIntentMessage, } from "./frameProtocol.js";
4
4
  import { useBoardCamera, useCanvasCards, useCanvasPresentation, useCanvasPresentationTransitioning } from "./canvasStore.js";
5
5
  import { handleElementClick } from "./rendererSelectionProxy.js";
6
- import { getSelectedElements, useSelectedElement, useSelectedElements } from "../selection/selectionStore.js";
6
+ import { getSelectedElements, useHierarchy, useSelectedElement, useSelectedElements } from "../selection/selectionStore.js";
7
7
  import { readBorderWidths, toRect, } from "../overlay/overlayGeometry.js";
8
8
  const overlayStyles = ".canvas-element-overlay {\n position: fixed;\n pointer-events: none;\n z-index: 1;\n background: transparent;\n outline: 1px solid var(--accent);\n outline-offset: -1px;\n}\n\n.canvas-spacing-guide {\n position: fixed;\n z-index: 1;\n border-radius: 999px;\n background: var(--accent);\n outline: none;\n box-shadow: none;\n pointer-events: none;\n}\n\n.canvas-spacing-fill {\n position: fixed;\n z-index: 1;\n pointer-events: none;\n background: rgb(59 130 246 / 14%);\n}\n\n.canvas-spacing-fill[data-kind=\"gap\"] {\n background: rgb(236 72 153 / 14%);\n}\n\n.canvas-spacing-guide[data-kind=\"gap\"] {\n background: #ec4899;\n}\n\n.canvas-selected-outline {\n position: fixed;\n pointer-events: none;\n z-index: 1;\n outline: 1px solid var(--accent);\n outline-offset: -1px;\n}\n\n.canvas-dom-drop-line {\n z-index: 2147483647;\n border-radius: 999px;\n background: #3b82f6;\n box-shadow: 0 0 0 1px rgb(255 255 255 / 95%), 0 1px 5px rgb(37 99 235 / 72%);\n}\n\n.canvas-dom-drop-line::before,\n.canvas-dom-drop-line::after {\n position: absolute;\n top: 50%;\n width: 8px;\n height: 8px;\n border: 2px solid #ffffff;\n border-radius: 50%;\n background: #2563eb;\n content: \"\";\n transform: translateY(-50%);\n}\n\n.canvas-dom-drop-line::before { left: -3px; }\n.canvas-dom-drop-line::after { right: -3px; }\n\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::before,\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::after {\n left: 50%;\n right: auto;\n transform: translateX(-50%);\n}\n\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::before { top: -3px; }\n.canvas-dom-drop-line[data-orientation=\"vertical\"]::after { top: auto; bottom: -3px; }\n\n.canvas-dom-drop-target {\n z-index: 2147483646;\n box-sizing: border-box;\n border: 1px solid rgb(59 130 246 / 72%);\n border-radius: 4px;\n background: rgb(59 130 246 / 5%);\n}\n";
9
9
  import { deleteElement, getDropLocationAtPoint, moveElement, nudgeElement } from "../overlay/structuralGestures.js";
@@ -25,6 +25,8 @@ import { subscribeCanvasRendererMessages } from "./rendererMessageRouter.js";
25
25
  import { setStyles } from "../tokens/editActions.js";
26
26
  import { getSpacingAffordanceAtPoint, getSpacingAffordanceForDescriptor, SPACING_GUIDE_HANDLE_LENGTH, spacingValueCss, spacingValueForDrag, } from "../overlay/spacingGestures.js";
27
27
  import { beginLayoutPreview, endLayoutPreview } from "../styleEditors/layoutPreviewState.js";
28
+ import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
29
+ import { DomNavigator } from "../overlay/DomNavigator.js";
28
30
  function projectRect(iframe, rect, zoom) {
29
31
  const frameRect = iframe.getBoundingClientRect();
30
32
  return {
@@ -104,8 +106,8 @@ function clearSpacingDrag(ref) {
104
106
  if (drag)
105
107
  restoreSpacingPreview(drag);
106
108
  }
107
- function applySpacingPreview(drag, point) {
108
- const value = spacingValueForDrag(drag.affordance, drag.start, point);
109
+ function applySpacingPreview(drag, point, shiftKey = false) {
110
+ const value = spacingValueForDrag(drag.affordance, drag.start, point, shiftKey);
109
111
  drag.currentValue = value;
110
112
  beginLayoutPreview(drag.element, drag.affordance.property);
111
113
  drag.element.style.setProperty(drag.affordance.property, spacingValueCss(value));
@@ -148,6 +150,8 @@ export function CanvasElementOverlay() {
148
150
  const [, refreshSelectedGeometry] = useReducer((revision) => revision + 1, 0);
149
151
  const selected = useSelectedElement();
150
152
  const selectedElements = useSelectedElements();
153
+ const hierarchy = useHierarchy();
154
+ const domNavigationEnabled = useNudgeUiRuntimeConfig().capabilities.domNavigation === true;
151
155
  const inlineTextSession = useInlineTextSession();
152
156
  const camera = useBoardCamera();
153
157
  const presentation = useCanvasPresentation();
@@ -242,6 +246,35 @@ export function CanvasElementOverlay() {
242
246
  return elementId ? { elementId } : null;
243
247
  }, [selected?.domElement]);
244
248
  useEffect(() => {
249
+ const refreshSpacingPreviewOverlay = (drag) => {
250
+ const descriptor = {
251
+ kind: drag.affordance.kind,
252
+ property: drag.affordance.property,
253
+ side: drag.affordance.side,
254
+ };
255
+ const affordance = getSpacingAffordanceForDescriptor(drag.element, descriptor);
256
+ const rect = toRect(drag.element.getBoundingClientRect());
257
+ const borders = readBorderWidths(drag.element);
258
+ const elementId = drag.element.getAttribute(RENDERER_ELEMENT_ID_ATTR);
259
+ setHover((current) => {
260
+ if (!current
261
+ || current.iframe !== drag.iframe
262
+ || current.identity.elementId !== elementId
263
+ || !current.spacing
264
+ || current.spacing.kind !== descriptor.kind
265
+ || current.spacing.property !== descriptor.property
266
+ || current.spacing.side !== descriptor.side) {
267
+ return current;
268
+ }
269
+ return {
270
+ ...current,
271
+ rect,
272
+ borders,
273
+ spacingGuides: affordance?.affectedGuides ?? [],
274
+ spacingAreas: affordance?.affectedAreas ?? [],
275
+ };
276
+ });
277
+ };
245
278
  const unsubscribe = subscribeCanvasRendererMessages(({ cardId: sourceCardId, iframe: sourceIframe, identity: frameIdentity, message: data, }) => {
246
279
  if (data.type === "renderer-hello" || data.type === "frame-ready") {
247
280
  sendMeasureModifier(sourceIframe, sourceCardId, measureAltKeyRef.current);
@@ -357,7 +390,8 @@ export function CanvasElementOverlay() {
357
390
  const selectedElement = resolveSelectionFromElement(element);
358
391
  if (selectedElement)
359
392
  setSelectedElement(selectedElement);
360
- applySpacingPreview(spacingDragRef.current, msg.point);
393
+ applySpacingPreview(spacingDragRef.current, msg.point, msg.shiftKey === true);
394
+ refreshSpacingPreviewOverlay(spacingDragRef.current);
361
395
  return;
362
396
  }
363
397
  dragRef.current = { iframe: sourceIframe, element };
@@ -369,7 +403,8 @@ export function CanvasElementOverlay() {
369
403
  else if (data.type === "element-drag-move") {
370
404
  const spacing = spacingDragRef.current;
371
405
  if (spacing && spacing.iframe === sourceIframe) {
372
- applySpacingPreview(spacing, data.point);
406
+ applySpacingPreview(spacing, data.point, data.shiftKey === true);
407
+ refreshSpacingPreviewOverlay(spacing);
373
408
  return;
374
409
  }
375
410
  updateDropGuide(data.point, sourceIframe);
@@ -381,10 +416,12 @@ export function CanvasElementOverlay() {
381
416
  spacingDragRef.current = null;
382
417
  if (msg.cancelled) {
383
418
  restoreSpacingPreview(spacing);
419
+ refreshSpacingPreviewOverlay(spacing);
384
420
  }
385
421
  else {
386
- applySpacingPreview(spacing, msg.point);
422
+ applySpacingPreview(spacing, msg.point, msg.shiftKey === true);
387
423
  commitSpacingDrag(spacing);
424
+ refreshSpacingPreviewOverlay(spacing);
388
425
  }
389
426
  clearDropGuide("canvas");
390
427
  if (!msg.cancelled) {
@@ -479,6 +516,9 @@ export function CanvasElementOverlay() {
479
516
  const isSelfHover = Boolean(hoverInSelectedFrame
480
517
  && selectedIdentity
481
518
  && hoverInSelectedFrame.identity.elementId === selectedIdentity.elementId);
519
+ // Selected elements already have a live geometry outline. The hover outline
520
+ // is based on a cached renderer message and can lag during spacing previews.
521
+ const showHoverOutline = Boolean(projectedHoverRect && !isSelfHover);
482
522
  const showMeasurement = Boolean(showGuideOverlay
483
523
  && selectedLocalRect
484
524
  && hoverInSelectedFrame
@@ -491,5 +531,7 @@ export function CanvasElementOverlay() {
491
531
  : [];
492
532
  if (presentationTransitioning)
493
533
  return _jsx("style", { "data-test": "canvas-element-overlay-styles", children: overlayStyles });
494
- return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "canvas-element-overlay-styles", children: overlayStyles }), showGuideOverlay && selectedRect && selectedFrameRect ? (_jsx(MeasurementGuideOverlay, { testId: "canvas-measurement-overlay", selectedRect: selectedRect, guideViewport: selectedFrameRect, segments: measurementSegments })) : null, projectedHoverRect ? (_jsxs(_Fragment, { children: [hoverSpacing ? projectedSpacingAreas.map((area, index) => (_jsx("div", { className: "canvas-spacing-fill", "data-test": "canvas-spacing-fill", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-area-index": index, style: overlayStyle(area), "aria-hidden": "true" }, `${hoverSpacing.property}-area-${index}`))) : null, _jsx("div", { className: "canvas-element-overlay", "data-test": "canvas-hover-outline", style: overlayStyle(projectedHoverRect), "aria-hidden": "true" }), hoverSpacing ? projectedSpacingGuides.map((guide, index) => (_jsx("div", { className: "canvas-spacing-guide", "data-test": "canvas-spacing-guide", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-guide-index": index, style: spacingGuideStyle(guide), "aria-hidden": "true" }, `${hoverSpacing.property}-${index}`))) : null] })) : null, projectedSelectedGeometry.map(({ element, rect }, index) => (_jsx("div", { className: "canvas-selected-outline", "data-test": "canvas-selected-outline", "data-selected-cid": element.cid, "data-selected-index": index, "data-selected-src": element.src, style: overlayStyle(rect), "aria-hidden": "true" }, `${element.cid}-${index}`))), _jsx(DropGuideOverlay, { guide: projectedDropGuide, lineClassName: "canvas-dom-drop-line", lineTestId: "canvas-dom-drop-line", targetClassName: "canvas-dom-drop-target", targetTestId: "canvas-dom-drop-target" })] }));
534
+ return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "canvas-element-overlay-styles", children: overlayStyles }), showGuideOverlay && selectedRect && selectedFrameRect ? (_jsx(MeasurementGuideOverlay, { testId: "canvas-measurement-overlay", selectedRect: selectedRect, guideViewport: selectedFrameRect, segments: measurementSegments })) : null, projectedHoverRect ? (_jsxs(_Fragment, { children: [hoverSpacing ? projectedSpacingAreas.map((area, index) => (_jsx("div", { className: "canvas-spacing-fill", "data-test": "canvas-spacing-fill", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-area-index": index, style: overlayStyle(area), "aria-hidden": "true" }, `${hoverSpacing.property}-area-${index}`))) : null, showHoverOutline ? (_jsx("div", { className: "canvas-element-overlay", "data-test": "canvas-hover-outline", style: overlayStyle(projectedHoverRect), "aria-hidden": "true" })) : null, hoverSpacing ? projectedSpacingGuides.map((guide, index) => (_jsx("div", { className: "canvas-spacing-guide", "data-test": "canvas-spacing-guide", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-guide-index": index, style: spacingGuideStyle(guide), "aria-hidden": "true" }, `${hoverSpacing.property}-${index}`))) : null] })) : null, projectedSelectedGeometry.map(({ element, rect }, index) => (_jsx("div", { className: "canvas-selected-outline", "data-test": "canvas-selected-outline", "data-selected-cid": element.cid, "data-selected-index": index, "data-selected-src": element.src, style: overlayStyle(rect), "aria-hidden": "true" }, `${element.cid}-${index}`))), domNavigationEnabled && !inlineTextSession && selectedElements.length === 1 && selected && selectedRect && selectedFrame instanceof HTMLIFrameElement ? (_jsx(DomNavigator, { selected: selected, hierarchy: hierarchy, anchor: selectedRect, project: (element) => element.ownerDocument === selected.domElement.ownerDocument
535
+ ? projectRect(selectedFrame, toRect(element.getBoundingClientRect()), projectionZoom)
536
+ : null })) : null, _jsx(DropGuideOverlay, { guide: projectedDropGuide, lineClassName: "canvas-dom-drop-line", lineTestId: "canvas-dom-drop-line", targetClassName: "canvas-dom-drop-target", targetTestId: "canvas-dom-drop-target" })] }));
495
537
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { IconHandStop, IconPointer2, IconSketching, } from "@tabler/icons-react";
2
+ import { IconHandStop, IconPlayerPlay, IconPointer2, IconSketching, } from "@tabler/icons-react";
3
3
  import { IconButton } from "../ui/IconButton.js";
4
4
  export const CANVAS_ZOOM_LEVELS = [0.25, 0.5, 0.9, 1];
5
5
  export function closestCanvasZoomLevel(zoom) {
@@ -16,9 +16,9 @@ export function adjacentCanvasZoomLevel(zoom, direction) {
16
16
  const next = CANVAS_ZOOM_LEVELS[index + direction];
17
17
  return next === undefined ? null : next;
18
18
  }
19
- function ToolButton({ active, disabled, label, icon, onClick, }) {
20
- return (_jsx(IconButton, { variant: "quiet", size: "large", className: "canvas-toolbar__tool", label: label, title: label, "aria-pressed": active, "data-test": `canvas-tool-${label.toLowerCase()}`, "data-active": active ? "true" : "false", disabled: disabled, onClick: onClick, children: icon }));
19
+ function ToolButton({ active, disabled, label, description, icon, onClick, }) {
20
+ return (_jsx(IconButton, { variant: "quiet", size: "large", className: "canvas-toolbar__tool", label: label, title: description ?? label, "aria-pressed": active, "data-test": `canvas-tool-${label.toLowerCase()}`, "data-active": active ? "true" : "false", disabled: disabled, onClick: onClick, children: icon }));
21
21
  }
22
22
  export function CanvasToolbar({ tool, sketchEnabled, onToolChange, }) {
23
- return (_jsx("div", { className: "canvas-toolbar", "data-test": "canvas-toolbar", role: "toolbar", "aria-label": "Canvas tools", children: _jsxs("div", { className: "canvas-toolbar__tools", role: "group", "aria-label": "Interaction tools", children: [_jsx(ToolButton, { active: tool === "move", label: "Move", icon: _jsx(IconPointer2, { size: "var(--icon-size-large)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), onClick: () => onToolChange("move") }), _jsx(ToolButton, { active: tool === "pan", label: "Pan", icon: _jsx(IconHandStop, { size: "var(--icon-size-large)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), onClick: () => onToolChange("pan") }), _jsx(ToolButton, { active: tool === "sketch", label: "Sketch", disabled: !sketchEnabled, icon: _jsx(IconSketching, { size: "var(--icon-size-large)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), onClick: () => onToolChange("sketch") })] }) }));
23
+ return (_jsxs("div", { className: "canvas-toolbar", "data-test": "canvas-toolbar", role: "toolbar", "aria-label": "Canvas tools", children: [_jsxs("div", { className: "canvas-toolbar__tools", role: "group", "aria-label": "Interaction tools", children: [_jsx(ToolButton, { active: tool === "design", label: "Design", description: "Design (V): click elements to inspect and edit them", icon: _jsx(IconPointer2, { size: "var(--icon-size-large)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), onClick: () => onToolChange("design") }), _jsx(ToolButton, { active: tool === "pan", label: "Pan", icon: _jsx(IconHandStop, { size: "var(--icon-size-large)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), onClick: () => onToolChange("pan") }), _jsx(ToolButton, { active: tool === "sketch", label: "Sketch", disabled: !sketchEnabled, icon: _jsx(IconSketching, { size: "var(--icon-size-large)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), onClick: () => onToolChange("sketch") })] }), _jsx("div", { className: "canvas-toolbar__divider", "aria-hidden": "true" }), _jsx("div", { className: "canvas-toolbar__tools", role: "group", "aria-label": "Preview tools", children: _jsx(ToolButton, { active: tool === "select", label: "Select", description: "Select (I): use the app normally", icon: _jsx(IconPlayerPlay, { size: "var(--icon-size-large)", stroke: "var(--icon-stroke-width)", "aria-hidden": "true" }), onClick: () => onToolChange("select") }) })] }));
24
24
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
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
+ import { useCanvasCards, activateIframeWorkspace, removeCanvasCard, getSelectedCardId, getFocusedCardId, getCanvasCards, setBoardCamera, getBoardCamera, fitAllCards, setCanvasPresentation, focusCard, hasFitAllRan, useBoardCamera, useCanvasPresentation, useCanvasPresentationTransitioning, useSelectedCardId, useFocusedCardId, resizeCard, setCardPosition, updateCardTitle, addCanvasCard, findCardByNormalizedUrl, selectCard, CARD_GAP, updateCardUrl, } from "./canvasStore.js";
5
5
  import { CanvasCard } from "./CanvasCard.js";
6
6
  import { Button } from "../ui/Button.js";
7
7
  import { useCanvasMode } from "./canvasStore.js";
@@ -21,7 +21,7 @@ import { createNudgeUiEditorUrl } from "../../transport/editor.js";
21
21
  import { disposeInlineTextEdit } from "../inline-text/inlineTextEditor.js";
22
22
  import { PROTOCOL_VERSION, isProjectionAppliedMessage, isKeyboardShortcutMessage, isRenderedInstanceProjectionReportMessage, isTextProjectionReportMessage, isStructuralProjectionReportMessage, } from "./frameProtocol.js";
23
23
  import { iframePointToClientPoint, zoomCameraAtPointer } from "./canvasGestures.js";
24
- const canvasWorkspaceStyles = ".canvas-workspace {\n position: fixed;\n inset: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface-panel);\n pointer-events: auto;\n}\n\n.canvas-workspace__board {\n flex: 1;\n overflow: hidden;\n position: relative;\n background: var(--surface-panel);\n}\n\n.canvas-workspace__focus-action {\n position: absolute;\n top: max(12px, env(safe-area-inset-top));\n left: max(12px, env(safe-area-inset-left));\n z-index: calc(var(--z-index) - 1);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--space-4);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n pointer-events: auto;\n}\n\n.canvas-workspace__board.is-grabbable {\n cursor: grab;\n}\n\n.canvas-workspace__board.is-grabbing {\n cursor: grabbing;\n}\n\n/* Canvas gestures take precedence over every card interaction while Space is\n held. This also closes the short postMessage gap before an iframe receives\n its mirrored modifier state. */\n.canvas-workspace__board.is-grabbable .canvas-card,\n.canvas-workspace__board.is-grabbing .canvas-card {\n pointer-events: none;\n}\n\n.canvas-workspace__board-content {\n position: absolute;\n width: 100%;\n height: 100%;\n 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";
24
+ const canvasWorkspaceStyles = ".canvas-workspace {\n position: fixed;\n inset: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface-panel);\n pointer-events: auto;\n}\n\n.canvas-workspace__board {\n flex: 1;\n overflow: hidden;\n position: relative;\n background: var(--surface-panel);\n}\n\n.canvas-workspace__focus-action {\n position: absolute;\n top: max(12px, env(safe-area-inset-top));\n left: max(12px, env(safe-area-inset-left));\n z-index: calc(var(--z-index) - 1);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--space-4);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n pointer-events: auto;\n}\n\n.canvas-workspace__board.is-grabbable {\n cursor: grab;\n}\n\n.canvas-workspace__board.is-grabbing {\n cursor: grabbing;\n}\n\n/* Canvas gestures take precedence over every card interaction while Space is\n held. This also closes the short postMessage gap before an iframe receives\n its mirrored modifier state. */\n.canvas-workspace__board.is-grabbable .canvas-card,\n.canvas-workspace__board.is-grabbing .canvas-card {\n pointer-events: none;\n}\n\n.canvas-workspace__board-content {\n position: absolute;\n width: 100%;\n height: 100%;\n /*\n * Leave `will-change` unset so the browser can re-rasterize this subtree at\n * the current camera scale. A permanent `will-change: transform` promotes\n * it to a fixed bitmap, which makes iframe content look soft when zoomed.\n */\n}\n\n.canvas-workspace__board.is-grabbing .canvas-workspace__board-content {\n /* Panning benefits from a cached composited layer; zoom restores the crisp\n * default path as soon as the gesture ends. */\n will-change: transform;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-workspace__board-content {\n transition: transform 200ms ease;\n}\n\n.canvas-workspace[data-presentation=\"focus\"] .canvas-workspace__board-content {\n inset: 0;\n transform: none !important;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-card.is-presentation-card {\n transition: left 200ms ease, top 200ms ease;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-card:not(.is-presentation-card) {\n visibility: hidden !important;\n pointer-events: none !important;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .canvas-workspace.is-presentation-transitioning .canvas-workspace__board-content,\n .canvas-workspace.is-presentation-transitioning .canvas-card.is-presentation-card {\n transition: none;\n }\n}\n\n.canvas-workspace__target-error {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n color: #6b7280;\n font: 500 14px/1.4 system-ui, sans-serif;\n}\n";
25
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
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";
27
27
  import { useInspectorOpen } from "../shell/openStore.js";
@@ -32,7 +32,7 @@ import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
32
32
  import { subscribeCanvasRendererMessages } from "./rendererMessageRouter.js";
33
33
  import { adjacentCanvasZoomLevel, CanvasToolbar, } from "./CanvasToolbar.js";
34
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";
35
+ const canvasToolbarStyles = ".canvas-toolbar {\n position: absolute;\n left: 50%;\n bottom: var(--space-20);\n /* Keep the persistent controls above the live sketch surface. The sketch\n toolbar gets the next layer so both toolbars remain available together. */\n z-index: calc(var(--z-index) - 2);\n display: flex;\n align-items: center;\n gap: var(--space-4);\n padding: var(--space-4);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n transform: translateX(-50%);\n pointer-events: auto;\n}\n\n/* The live sketch surface is a separate stacking context. Keep a copy of the\n toolbar at the root layer while sketching so pointer events reach the\n controls instead of the full-screen SVG drawing surface. */\n.canvas-toolbar__portal {\n position: fixed;\n inset: 0;\n z-index: var(--z-index);\n pointer-events: none;\n}\n\n.canvas-toolbar__portal > .canvas-toolbar {\n pointer-events: auto;\n}\n\n.canvas-toolbar__tools {\n display: flex;\n align-items: center;\n gap: var(--space-4);\n}\n\n.canvas-toolbar__divider {\n flex-shrink: 0;\n width: 1px;\n height: var(--control-height-small);\n margin-inline: var(--space-4);\n background: var(--border-subtle);\n}\n\n.canvas-toolbar__tool[data-active=\"true\"] {\n background: var(--accent-surface);\n color: var(--accent);\n}\n";
36
36
  import { getActiveCanvasFrame } from "./activeCanvasDocument.js";
37
37
  import { startSketchCapture } from "../sketch/SketchWorkspace.js";
38
38
  import { cancelSketchInteraction, useSketchInteractionActive } from "../sketch/interaction.js";
@@ -74,7 +74,9 @@ export function CanvasWorkspace({ primaryUrl }) {
74
74
  const focusCardRectRef = useRef(null);
75
75
  const canvasEntryAnimationRef = useRef(null);
76
76
  const [boardCursorClass, setBoardCursorClass] = useState("");
77
- const [interactionTool, setInteractionTool] = useState("move");
77
+ const [interactionTool, setInteractionTool] = useState("design");
78
+ const rendererInteractionsEnabled = interactionTool !== "select";
79
+ const linksOpenInCards = interactionTool === "select" && presentation === "canvas";
78
80
  const presentationCardId = selectedCardId ?? focusedCardId ?? cards[0]?.id ?? null;
79
81
  useLayoutEffect(() => {
80
82
  const board = boardRef.current;
@@ -222,7 +224,7 @@ export function CanvasWorkspace({ primaryUrl }) {
222
224
  spaceHeld,
223
225
  }, window.location.origin);
224
226
  }, []);
225
- const sendInspectorInteractionState = useCallback((iframe, cardId, open) => {
227
+ const sendInspectorInteractionState = useCallback((iframe, cardId, open, interactionsEnabled) => {
226
228
  iframe.contentWindow?.postMessage({
227
229
  type: "inspector-interaction-state",
228
230
  protocolVersion: PROTOCOL_VERSION,
@@ -230,7 +232,17 @@ export function CanvasWorkspace({ primaryUrl }) {
230
232
  workspaceId: WORKSPACE_ID,
231
233
  cardId,
232
234
  open,
233
- interactionsEnabled: true,
235
+ interactionsEnabled,
236
+ }, window.location.origin);
237
+ }, []);
238
+ const sendLinkTargetState = useCallback((iframe, cardId, openInCard) => {
239
+ iframe.contentWindow?.postMessage({
240
+ type: "link-target-state",
241
+ protocolVersion: PROTOCOL_VERSION,
242
+ projectId: PROJECT_ID,
243
+ workspaceId: WORKSPACE_ID,
244
+ cardId,
245
+ openInCard,
234
246
  }, window.location.origin);
235
247
  }, []);
236
248
  const sendBoardGestureState = useCallback((iframe, cardId, enabled) => {
@@ -245,10 +257,11 @@ export function CanvasWorkspace({ primaryUrl }) {
245
257
  }, []);
246
258
  useEffect(() => {
247
259
  for (const [cardId, iframe] of getRegisteredFrames()) {
248
- sendInspectorInteractionState(iframe, cardId, inspectorOpen);
260
+ sendInspectorInteractionState(iframe, cardId, inspectorOpen, rendererInteractionsEnabled);
249
261
  sendBoardGestureState(iframe, cardId, presentation === "canvas");
262
+ sendLinkTargetState(iframe, cardId, linksOpenInCards);
250
263
  }
251
- }, [inspectorOpen, presentation, sendBoardGestureState, sendInspectorInteractionState]);
264
+ }, [inspectorOpen, linksOpenInCards, presentation, rendererInteractionsEnabled, sendBoardGestureState, sendInspectorInteractionState, sendLinkTargetState]);
252
265
  const broadcastPanModifier = useCallback((spaceHeld) => {
253
266
  for (const [cardId, iframe] of getRegisteredFrames()) {
254
267
  sendPanModifier(iframe, cardId, spaceHeld);
@@ -362,7 +375,7 @@ export function CanvasWorkspace({ primaryUrl }) {
362
375
  }, []);
363
376
  useEffect(() => {
364
377
  if (!sketchActive && interactionTool === "sketch") {
365
- setInteractionTool("move");
378
+ setInteractionTool("design");
366
379
  }
367
380
  }, [interactionTool, sketchActive]);
368
381
  useLayoutEffect(() => {
@@ -454,8 +467,9 @@ export function CanvasWorkspace({ primaryUrl }) {
454
467
  // A card can finish loading after Space was pressed on the controller.
455
468
  // Seed it with the current modifier state before its first pointer event.
456
469
  sendPanModifier(frame.iframe, frame.cardId, spaceHeldRef.current);
457
- sendInspectorInteractionState(frame.iframe, frame.cardId, inspectorOpen);
470
+ sendInspectorInteractionState(frame.iframe, frame.cardId, inspectorOpen, rendererInteractionsEnabled);
458
471
  sendBoardGestureState(frame.iframe, frame.cardId, presentation === "canvas");
472
+ sendLinkTargetState(frame.iframe, frame.cardId, linksOpenInCards);
459
473
  }
460
474
  if (msg.type === "pan-modifier") {
461
475
  if (presentation !== "canvas")
@@ -520,11 +534,16 @@ export function CanvasWorkspace({ primaryUrl }) {
520
534
  const normalized = normalizeUrl(msg.url);
521
535
  if (!normalized || normalized.origin !== window.location.origin)
522
536
  return;
537
+ if (msg.openInCard === true) {
538
+ const card = findCardByNormalizedUrl(normalized) ?? addCanvasCard(msg.url);
539
+ selectCard(card.id);
540
+ return;
541
+ }
523
542
  const frameDocument = frame.iframe.contentDocument;
524
543
  if (frameDocument)
525
544
  disposeInlineTextEdit("route-disposed", frameDocument);
526
545
  });
527
- }, [broadcastPanModifier, endPanning, handleToolChange, inspectorOpen, movePanning, presentation, sendBoardGestureState, sendInspectorInteractionState, sendPanModifier, sketchAvailable, startPanning, zoomAtPointer]);
546
+ }, [broadcastPanModifier, endPanning, handleToolChange, inspectorOpen, linksOpenInCards, movePanning, presentation, rendererInteractionsEnabled, sendBoardGestureState, sendInspectorInteractionState, sendLinkTargetState, sendPanModifier, sketchAvailable, startPanning, zoomAtPointer]);
528
547
  useEffect(() => {
529
548
  if (mode === "canvas" && !hasFitAllRan()) {
530
549
  if (!fitAllScheduledRef.current) {
@@ -2,7 +2,7 @@ import type { ComponentOverride } from "../componentSemantics/types.js";
2
2
  import type { RenderedInstanceOverride } from "../changes/editModel.js";
3
3
  import type { NudgeUiRuntimeConfig } from "../runtime/runtimeConfig.js";
4
4
  import type { SpacingDescriptor } from "../overlay/spacingGestures.js";
5
- export declare const PROTOCOL_VERSION = 19;
5
+ export declare const PROTOCOL_VERSION = 20;
6
6
  export interface FrameMessage {
7
7
  type: string;
8
8
  protocolVersion: number;
@@ -93,6 +93,13 @@ export interface TextProjectionReportMessage extends RendererMessage {
93
93
  export interface NavigationIntentMessage extends RendererMessage {
94
94
  type: "navigation-intent";
95
95
  url: string;
96
+ /** The renderer kept its own route and asks the controller for a card instead. */
97
+ openInCard?: boolean;
98
+ }
99
+ /** Tells a renderer whether ordinary link clicks open a new canvas card. */
100
+ export interface LinkTargetStateMessage extends RendererMessage {
101
+ type: "link-target-state";
102
+ openInCard: boolean;
96
103
  }
97
104
  export interface ElementHoverMessage extends RendererMessage {
98
105
  type: "element-hover";
@@ -179,6 +186,8 @@ export interface ElementDragStartMessage extends RendererMessage {
179
186
  };
180
187
  /** Spacing affordance captured at pointer-down, if this is a spacing drag. */
181
188
  spacing?: SpacingDescriptor | null;
189
+ /** True while Shift is held; spacing drags use this to snap by 8px steps. */
190
+ shiftKey?: boolean;
182
191
  }
183
192
  export interface ElementDragMoveMessage extends RendererMessage {
184
193
  type: "element-drag-move";
@@ -186,6 +195,8 @@ export interface ElementDragMoveMessage extends RendererMessage {
186
195
  x: number;
187
196
  y: number;
188
197
  };
198
+ /** True while Shift is held; spacing drags use this to snap by 8px steps. */
199
+ shiftKey?: boolean;
189
200
  }
190
201
  export interface ElementDragEndMessage extends RendererMessage {
191
202
  type: "element-drag-end";
@@ -193,6 +204,8 @@ export interface ElementDragEndMessage extends RendererMessage {
193
204
  x: number;
194
205
  y: number;
195
206
  };
207
+ /** True while Shift is held; spacing drags use this to snap by 8px steps. */
208
+ shiftKey?: boolean;
196
209
  /** True when the renderer lost the gesture before the pointer was released. */
197
210
  cancelled?: boolean;
198
211
  }
@@ -222,7 +235,7 @@ export interface InspectorToggleRequestMessage extends RendererMessage {
222
235
  export interface InspectorOpenRequestMessage extends RendererMessage {
223
236
  type: "inspector-open-request";
224
237
  }
225
- export type KeyboardShortcutCode = "Backslash" | "KeyV" | "KeyH" | "KeyP" | "KeyS";
238
+ export type KeyboardShortcutCode = "Backslash" | "KeyI" | "KeyV" | "KeyH" | "KeyP" | "KeyS";
226
239
  export type KeyboardShortcutPhase = "keydown" | "keyup";
227
240
  /** Forwards controller-owned keyboard shortcuts from an iframe document. */
228
241
  export interface KeyboardShortcutMessage extends RendererMessage {
@@ -271,7 +284,7 @@ export interface ZoomMessage extends RendererMessage {
271
284
  y: number;
272
285
  };
273
286
  }
274
- export type FrameProtocolMessage = ParentReadyMessage | RendererHelloMessage | FrameReadyMessage | FrameRuntimeMessage | FrameMetadataMessage | FrameLoadError | ReplaceStylesMessage | ProjectionAppliedMessage | StructuralProjectionReportMessage | RenderedInstanceProjectionReportMessage | TextProjectionReportMessage | NavigationIntentMessage | ElementHoverMessage | ElementMeasureStateMessage | ElementClickMessage | InlineTextIntentMessage | ElementDeselectMessage | ElementDragStartMessage | ElementDragMoveMessage | ElementDragEndMessage | ElementDeleteMessage | ElementNudgeMessage | HistoryRequestMessage | InspectorToggleRequestMessage | InspectorOpenRequestMessage | KeyboardShortcutMessage | PanStartMessage | PanMoveMessage | PanEndMessage | PanModifierMessage | MeasureModifierMessage | BoardGestureStateMessage | ZoomMessage;
287
+ export type FrameProtocolMessage = ParentReadyMessage | RendererHelloMessage | FrameReadyMessage | FrameRuntimeMessage | FrameMetadataMessage | FrameLoadError | ReplaceStylesMessage | ProjectionAppliedMessage | StructuralProjectionReportMessage | RenderedInstanceProjectionReportMessage | TextProjectionReportMessage | NavigationIntentMessage | LinkTargetStateMessage | ElementHoverMessage | ElementMeasureStateMessage | ElementClickMessage | InlineTextIntentMessage | ElementDeselectMessage | ElementDragStartMessage | ElementDragMoveMessage | ElementDragEndMessage | ElementDeleteMessage | ElementNudgeMessage | HistoryRequestMessage | InspectorToggleRequestMessage | InspectorOpenRequestMessage | KeyboardShortcutMessage | PanStartMessage | PanMoveMessage | PanEndMessage | PanModifierMessage | MeasureModifierMessage | BoardGestureStateMessage | ZoomMessage;
275
288
  export declare function setRendererIdentity(identity: FrameIdentity): void;
276
289
  export declare function clearRendererIdentity(expected?: FrameIdentity): void;
277
290
  export declare function getRendererIdentity(): FrameIdentity | null;
@@ -1,6 +1,8 @@
1
1
  import { isDocumentProjectionReport } from "../projection/renderedInstance.js";
2
2
  import { isTextProjectionReport } from "../projection/textProjection.js";
3
3
  import { isStructuralProjectionReport } from "../projection/structuralProjectionBoundary.js";
4
+ // v20 adds Shift state to element drag messages so spacing handles can snap
5
+ // changes to 8px increments.
4
6
  // v19 adds spacing drag gestures (hover point, spacing descriptor, start
5
7
  // point, and cancelled drag-end) plus renderer-to-controller keyboard shortcut
6
8
  // forwarding for Canvas frames.
@@ -15,7 +17,7 @@ import { isStructuralProjectionReport } from "../projection/structuralProjection
15
17
  // rereading a Canvas iframe until the renderer has applied its revision. v11
16
18
  // added the renderer-hello handshake solicitation for runtimes whose boot
17
19
  // completes after the controller's load-time parent-ready.
18
- export const PROTOCOL_VERSION = 19;
20
+ export const PROTOCOL_VERSION = 20;
19
21
  let rendererIdentity = null;
20
22
  export function setRendererIdentity(identity) {
21
23
  rendererIdentity = identity;
@@ -37,7 +39,12 @@ export function isRendererMessageFor(value, identity) {
37
39
  && message.cardId === identity.cardId;
38
40
  }
39
41
  export function isKeyboardShortcutCode(value) {
40
- return value === "Backslash" || value === "KeyV" || value === "KeyH" || value === "KeyP" || value === "KeyS";
42
+ return value === "Backslash"
43
+ || value === "KeyI"
44
+ || value === "KeyV"
45
+ || value === "KeyH"
46
+ || value === "KeyP"
47
+ || value === "KeyS";
41
48
  }
42
49
  /** Strict JSON-only schema for renderer-originated global keyboard shortcuts. */
43
50
  export function isKeyboardShortcutMessage(value, identity) {
@@ -1,3 +1,4 @@
1
- export type CanvasInteractionTool = "move" | "pan" | "sketch";
1
+ /** `select` leaves the application interactive; `design` turns clicks into inspector selection. */
2
+ export type CanvasInteractionTool = "select" | "design" | "pan" | "sketch";
2
3
  export declare function canvasToolForCode(code: string): CanvasInteractionTool | null;
3
4
  export declare function canvasToolForEvent(event: KeyboardEvent): CanvasInteractionTool | null;
@@ -1,6 +1,8 @@
1
1
  export function canvasToolForCode(code) {
2
+ if (code === "KeyI")
3
+ return "select";
2
4
  if (code === "KeyV")
3
- return "move";
5
+ return "design";
4
6
  if (code === "KeyH")
5
7
  return "pan";
6
8
  if (code === "KeyP")
@@ -1,8 +1,3 @@
1
1
  export declare function findClosestAnchor(target: EventTarget | null): HTMLAnchorElement | null;
2
2
  export declare function isEligibleNavigation(anchor: HTMLAnchorElement, event: MouseEvent): boolean;
3
3
  export declare function hasDifferentRoute(anchor: HTMLAnchorElement): boolean;
4
- /**
5
- * Returns whether the renderer should leave an anchor's browser behavior
6
- * intact instead of consuming it as an editing click.
7
- */
8
- export declare function shouldPreserveNativeLinkActivation(anchor: HTMLAnchorElement, event: MouseEvent): boolean;
@@ -37,32 +37,3 @@ export function hasDifferentRoute(anchor) {
37
37
  return false;
38
38
  }
39
39
  }
40
- /**
41
- * Returns whether the renderer should leave an anchor's browser behavior
42
- * intact instead of consuming it as an editing click.
43
- */
44
- export function shouldPreserveNativeLinkActivation(anchor, event) {
45
- // Command/Ctrl-click is the deep-selection gesture. Shift makes it the
46
- // explicit application-activation gesture instead.
47
- if (event.metaKey || event.ctrlKey)
48
- return event.shiftKey;
49
- // Shift-click is the additive inspector-selection gesture. Do not let a
50
- // link navigate or preserve its native activation in that mode.
51
- if (event.shiftKey)
52
- return false;
53
- if (anchor.hasAttribute("download"))
54
- return true;
55
- if (anchor.target && anchor.target !== "" && anchor.target !== "_self")
56
- return true;
57
- if (anchor.protocol !== "http:" && anchor.protocol !== "https:")
58
- return true;
59
- try {
60
- const url = new URL(anchor.href);
61
- if (url.origin !== window.location.origin)
62
- return true;
63
- }
64
- catch {
65
- return true;
66
- }
67
- return !hasDifferentRoute(anchor);
68
- }
@@ -155,6 +155,7 @@ export function bootstrapRenderer() {
155
155
  if (disposeSelector)
156
156
  owner.resourceDisposers.push(disposeSelector);
157
157
  installRendererPanProxy(owner);
158
+ let openLinksInCards = false;
158
159
  const onClick = (event) => {
159
160
  if (!ownsRendererBootstrap(owner))
160
161
  return;
@@ -168,10 +169,16 @@ export function bootstrapRenderer() {
168
169
  const identity = getRendererIdentity();
169
170
  if (!identity)
170
171
  return;
172
+ if (openLinksInCards) {
173
+ // Capture on document runs before framework routers, so the card keeps its route.
174
+ event.preventDefault();
175
+ event.stopImmediatePropagation();
176
+ }
171
177
  const msg = {
172
178
  type: "navigation-intent",
173
179
  protocolVersion: PROTOCOL_VERSION,
174
180
  url: anchor.href,
181
+ ...(openLinksInCards ? { openInCard: true } : {}),
175
182
  ...identity,
176
183
  };
177
184
  sendToParent(msg);
@@ -198,6 +205,12 @@ export function bootstrapRenderer() {
198
205
  sendFrameReady(owner);
199
206
  return;
200
207
  }
208
+ const frameIdentity = getRendererIdentity();
209
+ if (frameIdentity && isRendererMessageFor(msg, frameIdentity) && msg.type === "link-target-state") {
210
+ // SAFETY: the identity check proves a protocol message; the discriminator selects this shape.
211
+ openLinksInCards = msg.openInCard === true;
212
+ return;
213
+ }
201
214
  if (msg &&
202
215
  typeof msg === "object" &&
203
216
  msg.type === "replace-styles" &&
@@ -1,2 +1,2 @@
1
1
  export declare function buildSelector(el: HTMLElement): string;
2
- export declare function installRendererElementSelector(): () => void;
2
+ export declare function installRendererElementSelector(acceptsClick?: (event: Event) => boolean): () => void;