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.
- package/dist/client.mjs +16 -9
- package/dist/hosts/astro/clientTransport.js +3 -0
- package/dist/hosts/astro/integration.d.ts +2 -2
- package/dist/hosts/astro/integration.js +4 -3
- package/dist/hosts/environment.d.ts +15 -0
- package/dist/hosts/environment.js +16 -0
- package/dist/hosts/next/sidecar.js +7 -1
- package/dist/hosts/next/wrapper.d.ts +2 -0
- package/dist/hosts/next/wrapper.js +2 -1
- package/dist/hosts/projectBridge.d.ts +1 -0
- package/dist/hosts/projectBridge.js +1 -0
- package/dist/hosts/static/manifest.js +2 -0
- package/dist/hosts/static/server.js +1 -0
- package/dist/hosts/vite/vite.d.ts +1 -0
- package/dist/hosts/vite/vite.js +4 -1
- package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
- package/dist/inspector/agent/McpConnectionDialog.js +11 -4
- package/dist/inspector/agent/client.d.ts +6 -0
- package/dist/inspector/agent/client.js +36 -4
- package/dist/inspector/agent/connectionStatus.d.ts +3 -3
- package/dist/inspector/agent/connectionStatus.js +8 -1
- package/dist/inspector/agent/httpTransport.d.ts +6 -0
- package/dist/inspector/agent/httpTransport.js +41 -1
- package/dist/inspector/agent/protocol.d.ts +3 -0
- package/dist/inspector/canvas/CanvasElementOverlay.js +49 -7
- package/dist/inspector/canvas/CanvasToolbar.js +4 -4
- package/dist/inspector/canvas/CanvasWorkspace.js +30 -11
- package/dist/inspector/canvas/frameProtocol.d.ts +16 -3
- package/dist/inspector/canvas/frameProtocol.js +9 -2
- package/dist/inspector/canvas/keyboardShortcuts.d.ts +2 -1
- package/dist/inspector/canvas/keyboardShortcuts.js +3 -1
- package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
- package/dist/inspector/canvas/linkEligibility.js +0 -29
- package/dist/inspector/canvas/rendererBootstrap.js +13 -0
- package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
- package/dist/inspector/canvas/rendererElementSelector.js +55 -39
- package/dist/inspector/changes/workspaceChanges.d.ts +4 -0
- package/dist/inspector/changes/workspaceChanges.js +29 -3
- package/dist/inspector/clientManifest.d.ts +2 -0
- package/dist/inspector/clientManifest.js +1 -0
- package/dist/inspector/index.js +6 -8
- package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
- package/dist/inspector/overlay/DomNavigator.js +40 -0
- package/dist/inspector/overlay/InspectorOverlay.js +6 -2
- package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
- package/dist/inspector/overlay/clickPolicy.js +34 -3
- package/dist/inspector/overlay/elementSelector.d.ts +1 -1
- package/dist/inspector/overlay/elementSelector.js +18 -7
- package/dist/inspector/overlay/spacingGestures.d.ts +3 -3
- package/dist/inspector/overlay/spacingGestures.js +15 -17
- package/dist/inspector/runtime/runtimeConfig.d.ts +1 -1
- package/dist/inspector/selection/hierarchy.d.ts +6 -0
- package/dist/inspector/selection/hierarchy.js +8 -0
- package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
- package/dist/inspector/settings/SettingsDialog.js +2 -2
- package/dist/inspector/shell/ChangesLog.js +20 -20
- package/dist/inspector/shell/CopyPromptButton.js +9 -2
- package/dist/inspector/shell/InspectorShell.js +2 -5
- package/dist/inspector/sketch/store.js +48 -23
- package/dist/inspector/styleEditors/SpacingBox.js +9 -4
- package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
- package/dist/inspector/styleEditors/nudgeValue.js +9 -7
- package/dist/inspector/tokens/TokenField.js +21 -2
- package/dist/inspector/tokens/TokensPanel.js +2 -1
- package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
- package/dist/inspector/tokens/dragPointerLock.js +70 -0
- package/dist/inspector/ui/Disclosure.d.ts +22 -0
- package/dist/inspector/ui/Disclosure.js +13 -0
- package/dist/inspector/ui/dragCursor.d.ts +8 -0
- package/dist/inspector/ui/dragCursor.js +21 -0
- package/dist/inspector/ui/styles.js +11 -9
- package/dist/nudge-ui.mjs +15 -1
- package/dist/transport/editor.d.ts +20 -1
- package/dist/transport/editor.js +42 -11
- package/dist/transport/routes.d.ts +4 -1
- package/dist/transport/routes.js +4 -1
- package/package.json +1 -1
- package/dist/inspector/shell/DomNavigation.d.ts +0 -8
- 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
|
-
|
|
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(
|
|
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 (
|
|
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("
|
|
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
|
|
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("
|
|
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 =
|
|
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 =
|
|
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"
|
|
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
|
-
|
|
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,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;
|