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
@@ -1,5 +1,4 @@
1
1
  import { PROTOCOL_VERSION, getRendererIdentity, sendToParent, isKeyboardShortcutCode, } from "./frameProtocol.js";
2
- import { findClosestAnchor, isEligibleNavigation, hasDifferentRoute, shouldPreserveNativeLinkActivation, } from "./linkEligibility.js";
3
2
  import { readBorderWidths, readMargins } from "../overlay/overlayGeometry.js";
4
3
  import { installInteractionStyles } from "../overlay/interactionStyles.js";
5
4
  import { createFrameThrottle } from "../overlay/frameThrottle.js";
@@ -8,7 +7,7 @@ import { isNudgeUiDev } from "../runtime/devFlag.js";
8
7
  import { isEditableEvent, isInspectorToggleShortcut, isSendPromptShortcut } from "../shell/shortcuts.js";
9
8
  import { resolveSelectionTarget, selectionTargetMode } from "../selection/selectionTarget.js";
10
9
  import { escapeCssString } from "../projection/cssEscapes.js";
11
- import { blockApplicationClick, isApplicationActivationClick } from "../overlay/clickPolicy.js";
10
+ import { APPLICATION_GESTURE_EVENTS, blockApplicationClick, blockApplicationGesture, isInlineEditorTarget, isUserClick, } from "../overlay/clickPolicy.js";
12
11
  import { EMPTY_TEXT_PROJECTION_ATTR } from "../projection/textProjection.js";
13
12
  import { getSpacingAffordanceAtPoint, toSpacingDescriptor, } from "../overlay/spacingGestures.js";
14
13
  const REACT_FIBER_KEY = /^__reactFiber\$/;
@@ -78,19 +77,20 @@ export function buildSelector(el) {
78
77
  }
79
78
  let installed = false;
80
79
  const noopDisposal = () => undefined;
81
- export function installRendererElementSelector() {
80
+ export function installRendererElementSelector(acceptsClick = isUserClick) {
82
81
  if (!isNudgeUiDev())
83
82
  return noopDisposal;
84
83
  if (installed)
85
84
  return noopDisposal;
86
85
  installed = true;
87
- const removeInteractionStyles = installInteractionStyles();
86
+ let removeInteractionStyles = installInteractionStyles();
88
87
  const listenerCleanup = [];
89
88
  let disposed = false;
90
89
  let interactionsSuspended = false;
91
90
  let scrolling = false;
92
91
  let scrollSettleTimer = null;
93
92
  let activeHoverElement = null;
93
+ let suppressHoverDuringSpacingDrag = false;
94
94
  let lastHoverPoint = null;
95
95
  function trackListener(target, type, listener, options) {
96
96
  const eventListener = (event) => listener(event);
@@ -187,7 +187,7 @@ export function installRendererElementSelector() {
187
187
  sendToParent(msg);
188
188
  }
189
189
  const hoverUpdate = createFrameThrottle((pending) => {
190
- if (disposed || scrolling)
190
+ if (disposed || scrolling || suppressHoverDuringSpacingDrag)
191
191
  return;
192
192
  reportHover(pending);
193
193
  });
@@ -202,7 +202,7 @@ export function installRendererElementSelector() {
202
202
  function resumeHoverAfterScroll() {
203
203
  scrollSettleTimer = null;
204
204
  scrolling = false;
205
- if (disposed || interactionsSuspended || !lastHoverPoint)
205
+ if (disposed || interactionsSuspended || suppressHoverDuringSpacingDrag || !lastHoverPoint)
206
206
  return;
207
207
  const target = document.elementFromPoint?.(lastHoverPoint.x, lastHoverPoint.y);
208
208
  const element = resolveSelectionTarget(target, lastHoverPoint.mode);
@@ -248,6 +248,8 @@ export function installRendererElementSelector() {
248
248
  if (interactionsSuspended)
249
249
  return;
250
250
  updateMeasureState(event.altKey, true);
251
+ if (suppressHoverDuringSpacingDrag)
252
+ return;
251
253
  const target = event.target;
252
254
  if (!(target instanceof Element))
253
255
  return;
@@ -304,26 +306,35 @@ export function installRendererElementSelector() {
304
306
  }
305
307
  if (interactionsSuspended) {
306
308
  cancelActiveDrag();
307
- hoverUpdate.cancel();
308
- clearSpacingCursor();
309
+ clearActiveHover();
309
310
  if (scrollSettleTimer !== null)
310
311
  window.clearTimeout(scrollSettleTimer);
311
312
  scrollSettleTimer = null;
312
313
  scrolling = false;
313
- activeHoverElement = null;
314
314
  updateMeasureState(false, false);
315
+ removeInteractionStyles?.();
316
+ removeInteractionStyles = null;
317
+ }
318
+ else {
319
+ removeInteractionStyles ??= installInteractionStyles();
315
320
  }
316
321
  });
317
- const dragMoveUpdate = createFrameThrottle((point) => {
322
+ const dragMoveUpdate = createFrameThrottle((drag) => {
318
323
  if (disposed)
319
324
  return;
320
325
  const identity = getRendererIdentity();
321
- if (!point || !identity)
326
+ if (!drag || !identity)
322
327
  return;
323
- const msg = { type: "element-drag-move", protocolVersion: PROTOCOL_VERSION, point, ...identity };
328
+ const msg = {
329
+ type: "element-drag-move",
330
+ protocolVersion: PROTOCOL_VERSION,
331
+ point: drag.point,
332
+ shiftKey: drag.shiftKey,
333
+ ...identity,
334
+ };
324
335
  sendToParent(msg);
325
336
  });
326
- function sendDragEnd(point, cancelled) {
337
+ function sendDragEnd(point, cancelled, shiftKey = false) {
327
338
  if (!dragging || !point)
328
339
  return;
329
340
  const identity = getRendererIdentity();
@@ -333,6 +344,7 @@ export function installRendererElementSelector() {
333
344
  type: "element-drag-end",
334
345
  protocolVersion: PROTOCOL_VERSION,
335
346
  point,
347
+ shiftKey,
336
348
  ...(cancelled ? { cancelled: true } : {}),
337
349
  ...identity,
338
350
  };
@@ -343,6 +355,7 @@ export function installRendererElementSelector() {
343
355
  sendDragEnd(lastDragPoint, true);
344
356
  pendingDrag = null;
345
357
  dragging = false;
358
+ suppressHoverDuringSpacingDrag = false;
346
359
  }
347
360
  trackListener(document, "mousedown", (event) => {
348
361
  if (interactionsSuspended)
@@ -356,6 +369,11 @@ export function installRendererElementSelector() {
356
369
  const spacing = spacingAffordance?.element === element
357
370
  ? toSpacingDescriptor(spacingAffordance)
358
371
  : null;
372
+ if (spacing) {
373
+ suppressHoverDuringSpacingDrag = true;
374
+ hoverUpdate.cancel();
375
+ lastHoverPoint = null;
376
+ }
359
377
  const identity = getRendererIdentity();
360
378
  // A spacing drag owns the pointer gesture. Do not start an inline-text
361
379
  // attempt for the same down event; padding and gaps are intentionally not
@@ -386,6 +404,13 @@ export function installRendererElementSelector() {
386
404
  };
387
405
  lastDragPoint = { x: event.clientX, y: event.clientY };
388
406
  }, true);
407
+ for (const type of APPLICATION_GESTURE_EVENTS) {
408
+ trackListener(document, type, (event) => {
409
+ if (interactionsSuspended || !acceptsClick(event) || isInlineEditorTarget(event.target))
410
+ return;
411
+ blockApplicationGesture(event);
412
+ }, true);
413
+ }
389
414
  trackListener(document, "mousemove", (event) => {
390
415
  if (!pendingDrag) {
391
416
  if (interactionsSuspended)
@@ -419,12 +444,13 @@ export function installRendererElementSelector() {
419
444
  elementId: cidIndex.elementId(pendingDrag.element), point,
420
445
  startPoint: pendingDrag.point,
421
446
  spacing: pendingDrag.spacing,
447
+ shiftKey: event.shiftKey,
422
448
  ...identity,
423
449
  };
424
450
  sendToParent(msg);
425
451
  }
426
452
  else {
427
- dragMoveUpdate.schedule(point);
453
+ dragMoveUpdate.schedule({ point, shiftKey: event.shiftKey });
428
454
  }
429
455
  }, true);
430
456
  function finishDrag(event) {
@@ -433,10 +459,11 @@ export function installRendererElementSelector() {
433
459
  event.preventDefault();
434
460
  const point = { x: event.clientX, y: event.clientY };
435
461
  lastDragPoint = point;
436
- sendDragEnd(point, false);
462
+ sendDragEnd(point, false, event.shiftKey);
437
463
  }
438
464
  pendingDrag = null;
439
465
  dragging = false;
466
+ suppressHoverDuringSpacingDrag = false;
440
467
  }
441
468
  trackListener(document, "mouseup", finishDrag, true);
442
469
  function forwardKeyboardShortcut(event, phase) {
@@ -470,7 +497,10 @@ export function installRendererElementSelector() {
470
497
  sendToParent(message);
471
498
  return phase === "keydown";
472
499
  }
473
- trackListener(document, "keydown", (event) => {
500
+ // Global tool shortcuts need to run before application capture handlers on
501
+ // the iframe document. A page-level window handler can otherwise stop the
502
+ // event before it reaches the document listener.
503
+ trackListener(window, "keydown", (event) => {
474
504
  if (isEditableEvent(event))
475
505
  return;
476
506
  if (forwardKeyboardShortcut(event, "keydown"))
@@ -554,7 +584,7 @@ export function installRendererElementSelector() {
554
584
  };
555
585
  sendToParent(msg);
556
586
  }, true);
557
- trackListener(document, "keyup", (event) => {
587
+ trackListener(window, "keyup", (event) => {
558
588
  forwardKeyboardShortcut(event, "keyup");
559
589
  if (event.key === "Alt")
560
590
  updateMeasureState(false, measurePointerOverPage);
@@ -612,36 +642,20 @@ export function installRendererElementSelector() {
612
642
  sendToParent(message);
613
643
  }, true);
614
644
  trackListener(document, "click", (event) => {
615
- if (interactionsSuspended)
645
+ if (interactionsSuspended || !acceptsClick(event))
616
646
  return;
617
647
  const target = event.target;
618
648
  if (!(target instanceof Element))
619
649
  return;
620
- // Defer to navigation-intent when the user clicked a navigable same-origin
621
- // anchor that points to a different route. The later listener reports the
622
- // intent to the controller while leaving native or framework navigation in
623
- // charge of the live application document.
624
- const anchor = findClosestAnchor(target);
625
- if (anchor && isEligibleNavigation(anchor, event) && hasDifferentRoute(anchor)) {
626
- return;
627
- }
628
- // Command/Ctrl+Shift-click is the deliberate escape hatch for running
629
- // the application action. All other selected canvas clicks are editing
630
- // gestures and must not reach the rendered application.
631
- if (isApplicationActivationClick(event))
632
- return;
633
- const preserveNativeLink = anchor
634
- ? shouldPreserveNativeLinkActivation(anchor, event)
635
- : false;
650
+ // Design-tool clicks are editing gestures and must not reach the
651
+ // rendered application; the Select tool is how the app receives them.
636
652
  const el = resolveSelectionTarget(target, selectionTargetMode(event));
637
653
  if (!el) {
638
- if (!preserveNativeLink)
639
- blockApplicationClick(event);
654
+ blockApplicationClick(event);
640
655
  return;
641
656
  }
642
657
  lastSelected = el;
643
- if (!preserveNativeLink)
644
- blockApplicationClick(event);
658
+ blockApplicationClick(event);
645
659
  const cid = el.getAttribute("data-cid");
646
660
  const selector = buildSelector(el);
647
661
  const { file, line, component, src } = getFiberInfo(el);
@@ -676,11 +690,13 @@ export function installRendererElementSelector() {
676
690
  for (const cleanup of listenerCleanup)
677
691
  cleanup();
678
692
  listenerCleanup.length = 0;
679
- removeInteractionStyles();
693
+ removeInteractionStyles?.();
694
+ removeInteractionStyles = null;
680
695
  lastSelected = null;
681
696
  activeHoverElement = null;
682
697
  lastHoverPoint = null;
683
698
  scrolling = false;
699
+ suppressHoverDuringSpacingDrag = false;
684
700
  measurePointerOverPage = false;
685
701
  measureAltKey = false;
686
702
  document.documentElement.removeAttribute("data-nudge-ui-panel");
@@ -31,6 +31,10 @@ export interface WorkspaceChangeStore {
31
31
  restoreWorkspaceChanges(next: WorkspaceContents): void;
32
32
  clearWorkspaceChanges(): void;
33
33
  }
34
+ /** Groups consecutive synchronous updates from one gesture into one undo step.
35
+ * Updates outside the callback keep their own history entries.
36
+ */
37
+ export declare function createChangeHistoryGroup(): (update: () => void) => void;
34
38
  export declare const workspaceChangeStore: WorkspaceChangeStore;
35
39
  /** Compatibility accessors retained for callers that have not adopted the store seam. */
36
40
  export declare function getWorkspaceChanges(): WorkspaceChangesSnapshot;
@@ -5,6 +5,7 @@ let contents = { changes: [], structuralChanges: [] };
5
5
  let revision = 0;
6
6
  let undoStack = [];
7
7
  let redoStack = [];
8
+ let activeHistoryGroup;
8
9
  let snapshot = createSnapshot();
9
10
  const listeners = new Set();
10
11
  function cloneContents(value) {
@@ -60,11 +61,36 @@ function commit(next) {
60
61
  return false;
61
62
  const before = cloneContents(contents);
62
63
  contents = cloneContents(next);
63
- undoStack.push({ before, after: cloneContents(contents) });
64
+ const previous = undoStack.at(-1);
65
+ const entry = {
66
+ before: activeHistoryGroup && previous?.group === activeHistoryGroup ? previous.before : before,
67
+ after: cloneContents(contents),
68
+ group: activeHistoryGroup,
69
+ };
70
+ if (activeHistoryGroup && previous?.group === activeHistoryGroup)
71
+ undoStack.pop();
72
+ if (!sameWorkspaceContents(entry.before, entry.after))
73
+ undoStack.push(entry);
64
74
  redoStack = [];
65
75
  publish();
66
76
  return true;
67
77
  }
78
+ /** Groups consecutive synchronous updates from one gesture into one undo step.
79
+ * Updates outside the callback keep their own history entries.
80
+ */
81
+ export function createChangeHistoryGroup() {
82
+ const group = Symbol("change-history-group");
83
+ return (update) => {
84
+ const previous = activeHistoryGroup;
85
+ activeHistoryGroup = group;
86
+ try {
87
+ update();
88
+ }
89
+ finally {
90
+ activeHistoryGroup = previous;
91
+ }
92
+ };
93
+ }
68
94
  function getSnapshot() {
69
95
  return snapshot;
70
96
  }
@@ -123,7 +149,7 @@ function reconcileWorkspaceChangesImpl(verifiedKeys, verifiedStructuralIds) {
123
149
  return 0;
124
150
  contents = next;
125
151
  const prune = (entries) => entries
126
- .map((entry) => ({ before: retain(entry.before), after: retain(entry.after) }))
152
+ .map((entry) => ({ group: entry.group, before: retain(entry.before), after: retain(entry.after) }))
127
153
  .filter((entry) => !sameWorkspaceContents(entry.before, entry.after));
128
154
  undoStack = prune(undoStack);
129
155
  redoStack = prune(redoStack);
@@ -159,7 +185,7 @@ function redoWorkspaceChangeImpl() {
159
185
  return false;
160
186
  redoStack.pop();
161
187
  contents = cloneContents(entry.after);
162
- undoStack.push(entry);
188
+ undoStack.push({ before: entry.before, after: entry.after });
163
189
  publish();
164
190
  return true;
165
191
  }
@@ -24,6 +24,8 @@ export interface NudgeUiClientManifest {
24
24
  readonly strategy: "refresh-manifest" | "reload-document";
25
25
  readonly events?: readonly string[];
26
26
  };
27
+ /** Set by `NUDGE_UI=1`: automated browsers open the editor instead of the plain app. */
28
+ readonly inspectAutomatedBrowsers?: true;
27
29
  }
28
30
  /** Parses and normalizes an external manifest before it replaces inspector state. */
29
31
  export declare function parseNudgeUiClientManifest(value: unknown): NudgeUiClientManifest | null;
@@ -23,6 +23,7 @@ value) {
23
23
  ...(isAgentBridge(value.agentBridge) ? { agentBridge: value.agentBridge } : {}),
24
24
  ...(value.document === undefined ? {} : { document: value.document }),
25
25
  ...(value.reload === undefined ? {} : { reload: value.reload }),
26
+ ...(value.inspectAutomatedBrowsers === true ? { inspectAutomatedBrowsers: true } : {}),
26
27
  };
27
28
  }
28
29
  catch {
@@ -208,14 +208,12 @@ export function mountInspector(host) {
208
208
  const demo = isDemoRuntime();
209
209
  if (!demo && !hasWriteLease())
210
210
  return;
211
- if (host.dataset.nudgeUiDebug === "true") {
212
- const runtimeConfig = getNudgeUiRuntimeConfig();
213
- if (runtimeConfig.capabilities.domNavigation !== true) {
214
- configureNudgeUiRuntime({
215
- ...runtimeConfig,
216
- capabilities: { ...runtimeConfig.capabilities, domNavigation: true },
217
- });
218
- }
211
+ const runtimeConfig = getNudgeUiRuntimeConfig();
212
+ if (runtimeConfig.capabilities.domNavigation !== true) {
213
+ configureNudgeUiRuntime({
214
+ ...runtimeConfig,
215
+ capabilities: { ...runtimeConfig.capabilities, domNavigation: true },
216
+ });
219
217
  }
220
218
  if (lockedRoot) {
221
219
  lockedRoot.unmount();
@@ -0,0 +1,10 @@
1
+ import { type ReactElement } from "react";
2
+ import type { SelectedElement } from "../selection/selectionStore.js";
3
+ import type { Rect } from "./overlayGeometry.js";
4
+ export interface DomNavigatorProps {
5
+ selected: SelectedElement;
6
+ hierarchy: HTMLElement[];
7
+ anchor: Rect;
8
+ project: (element: HTMLElement) => Rect | null;
9
+ }
10
+ export declare function DomNavigator({ selected, hierarchy, anchor, project }: DomNavigatorProps): ReactElement | null;
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { setSelectedElement } from "../selection/selectionStore.js";
4
+ import { computeNavigationNodes } from "../selection/hierarchy.js";
5
+ import { resolveSelectionFromElement } from "../selection/resolveSelection.js";
6
+ function nodeLabel(node) {
7
+ return node.getAttribute("data-cid") ?? node.localName;
8
+ }
9
+ function nodeDescription(node) {
10
+ const id = node.id ? `#${node.id}` : "";
11
+ const className = typeof node.className === "string"
12
+ ? node.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((name) => `.${name}`).join("")
13
+ : "";
14
+ const source = node.getAttribute("data-src");
15
+ return `${node.localName}${id}${className}${source ? ` — ${source}` : ""}`;
16
+ }
17
+ function selectNode(node) {
18
+ const resolved = resolveSelectionFromElement(node);
19
+ if (resolved)
20
+ setSelectedElement(resolved);
21
+ }
22
+ export function DomNavigator({ selected, hierarchy, anchor, project }) {
23
+ const nodes = computeNavigationNodes(selected.domElement, hierarchy);
24
+ const [expanded, setExpanded] = useState(false);
25
+ const [hovered, setHovered] = useState(null);
26
+ if (nodes.length === 0)
27
+ return null;
28
+ const preview = hovered ? project(hovered) : null;
29
+ const close = () => {
30
+ setExpanded(false);
31
+ setHovered(null);
32
+ };
33
+ return (_jsxs(_Fragment, { children: [preview ? _jsx("div", { className: "dom-navigator-preview", "data-test": "dom-navigator-preview", style: preview, "aria-hidden": "true" }) : null, _jsxs("div", { className: "dom-navigator", "data-test": "dom-navigator", "aria-label": "DOM navigator", style: { left: Math.max(4, anchor.left), top: Math.max(4, anchor.top) }, onMouseEnter: () => setExpanded(true), onMouseLeave: close, onFocus: () => setExpanded(true), onBlur: (event) => {
34
+ if (!(event.relatedTarget instanceof Node) || !event.currentTarget.contains(event.relatedTarget))
35
+ close();
36
+ }, children: [_jsxs("button", { type: "button", className: "dom-navigator__trigger", "data-test": "dom-navigator-trigger", "aria-expanded": expanded, "aria-haspopup": "menu", title: nodeDescription(selected.domElement), onMouseEnter: () => setHovered(null), children: [_jsx("span", { children: nodeLabel(selected.domElement) }), _jsx("span", { "aria-hidden": "true", children: "\u2304" })] }), expanded ? (_jsx("div", { className: "dom-navigator__menu", role: "menu", children: nodes.map((node) => (_jsxs("button", { type: "button", className: "dom-navigator__item", "data-test": "dom-navigator-item", "data-direction": node.direction, "data-depth": node.depth, "data-cid": node.element.getAttribute("data-cid") ?? undefined, title: nodeDescription(node.element), role: "menuitem", onMouseEnter: () => setHovered(node.element), onClick: () => {
37
+ selectNode(node.element);
38
+ close();
39
+ }, children: [_jsx("span", { "aria-hidden": "true", children: node.direction === "up" ? "↑" : "↓" }), _jsx("span", { children: nodeLabel(node.element) })] }, `${node.direction}-${node.depth}-${node.element.getAttribute("data-cid") ?? node.element.localName}`))) })) : null] })] }));
40
+ }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from "react";
3
3
  import { useInspectorOpen } from "../shell/openStore.js";
4
- import { getSelectedElements, useSelectedElement, useSelectedElements } from "../selection/selectionStore.js";
4
+ import { getSelectedElements, useHierarchy, useSelectedElement, useSelectedElements } from "../selection/selectionStore.js";
5
5
  import { installElementSelector } from "./elementSelector.js";
6
6
  import { getMarginFills, getMarginGuides, readBorderWidths, readMargins, toRect, } from "./overlayGeometry.js";
7
7
  import { observeSelectedGeometry } from "./selectedGeometry.js";
@@ -17,6 +17,8 @@ import { MeasurementGuideOverlay } from "./MeasurementGuideOverlay.js";
17
17
  import { resolveSelectionTarget, selectionTargetMode } from "../selection/selectionTarget.js";
18
18
  import { isInlineTextEditingActive, useInlineTextSession } from "../inline-text/inlineTextEditor.js";
19
19
  import { EMPTY_TEXT_PROJECTION_ATTR } from "../projection/textProjection.js";
20
+ import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
21
+ import { DomNavigator } from "./DomNavigator.js";
20
22
  export { getMarginFills, getMarginGuides, } from "./overlayGeometry.js";
21
23
  function overlayStyle(rect) {
22
24
  return {
@@ -32,6 +34,8 @@ export function InspectorOverlay({ host }) {
32
34
  const open = useInspectorOpen();
33
35
  const selected = useSelectedElement();
34
36
  const selectedElements = useSelectedElements();
37
+ const hierarchy = useHierarchy();
38
+ const domNavigationEnabled = useNudgeUiRuntimeConfig().capabilities.domNavigation === true;
35
39
  const inlineTextSession = useInlineTextSession();
36
40
  const [hoverRect, setHoverRect] = useState(null);
37
41
  const [hoverMargins, setHoverMargins] = useState(null);
@@ -322,7 +326,7 @@ export function InspectorOverlay({ host }) {
322
326
  width: guide.width,
323
327
  height: guide.height,
324
328
  pointerEvents: "none",
325
- }, "aria-hidden": "true" }, guide.side)))] })) : null, open && !inlineTextSession ? selectedGeometry.map(({ element, rect }, index) => (_jsx("div", { className: "selected-outline", "data-test": "selected-outline", "data-selected-cid": element.cid, "data-selected-index": index, "data-selected-src": element.src, style: overlayStyle(rect), "aria-hidden": "true" }, `${element.domElement.getAttribute("data-cid") ?? "element"}-${index}`))) : null, open && selectedElements.length === 1 ? _jsx(DropGuideOverlay, { guide: dropGuide?.document === document
329
+ }, "aria-hidden": "true" }, guide.side)))] })) : null, open && !inlineTextSession ? selectedGeometry.map(({ element, rect }, index) => (_jsx("div", { className: "selected-outline", "data-test": "selected-outline", "data-selected-cid": element.cid, "data-selected-index": index, "data-selected-src": element.src, style: overlayStyle(rect), "aria-hidden": "true" }, `${element.domElement.getAttribute("data-cid") ?? "element"}-${index}`))) : null, open && domNavigationEnabled && !inlineTextSession && selectedElements.length === 1 && selected && selectedRect ? (_jsx(DomNavigator, { selected: selected, hierarchy: hierarchy, anchor: selectedRect, project: (element) => toRect(element.getBoundingClientRect()) })) : null, open && selectedElements.length === 1 ? _jsx(DropGuideOverlay, { guide: dropGuide?.document === document
326
330
  ? { orientation: dropGuide.orientation, line: dropGuide.line, target: dropGuide.target }
327
331
  : null, lineClassName: "dom-drop-line", lineTestId: "dom-drop-line", targetClassName: "dom-drop-target", targetTestId: "dom-drop-target" }) : null] }));
328
332
  }
@@ -1,7 +1,23 @@
1
1
  export type ClickModifiers = Pick<MouseEvent, "ctrlKey" | "metaKey" | "shiftKey">;
2
+ /**
3
+ * Press and release gestures that application widgets use to open menus,
4
+ * dialogs, and popovers before any `click` fires. The Design tool keeps them
5
+ * from reaching the application.
6
+ */
7
+ export declare const APPLICATION_GESTURE_EVENTS: readonly ["pointerdown", "pointerup", "mousedown", "mouseup", "touchstart", "touchend", "auxclick", "contextmenu"];
2
8
  /** Command/Ctrl-click selects the deepest tracked element unless Shift is held. */
3
9
  export declare function isDeepSelectionClick(event: ClickModifiers): boolean;
4
- /** Command/Ctrl+Shift-click intentionally lets the application receive its click. */
5
- export declare function isApplicationActivationClick(event: ClickModifiers): boolean;
10
+ /**
11
+ * Only input from a person becomes inspector selection. Script events such
12
+ * as `element.click()` keep reaching the application.
13
+ */
14
+ export declare function isUserClick(event: Pick<Event, "isTrusted">): boolean;
15
+ /** The active inline text editor needs native press events for caret placement. */
16
+ export declare function isInlineEditorTarget(target: EventTarget | null): boolean;
17
+ /**
18
+ * Keeps an application press gesture from reaching application listeners.
19
+ * Listeners on the document itself, including the inspector's own, still run.
20
+ */
21
+ export declare function blockApplicationGesture(event: Event): void;
6
22
  /** Prevent an inspected application click from reaching the host application. */
7
23
  export declare function blockApplicationClick(event: MouseEvent): void;
@@ -1,10 +1,41 @@
1
+ /**
2
+ * Press and release gestures that application widgets use to open menus,
3
+ * dialogs, and popovers before any `click` fires. The Design tool keeps them
4
+ * from reaching the application.
5
+ */
6
+ export const APPLICATION_GESTURE_EVENTS = [
7
+ "pointerdown",
8
+ "pointerup",
9
+ "mousedown",
10
+ "mouseup",
11
+ "touchstart",
12
+ "touchend",
13
+ "auxclick",
14
+ "contextmenu",
15
+ ];
1
16
  /** Command/Ctrl-click selects the deepest tracked element unless Shift is held. */
2
17
  export function isDeepSelectionClick(event) {
3
18
  return (event.ctrlKey || event.metaKey) && !event.shiftKey;
4
19
  }
5
- /** Command/Ctrl+Shift-click intentionally lets the application receive its click. */
6
- export function isApplicationActivationClick(event) {
7
- return (event.ctrlKey || event.metaKey) && event.shiftKey;
20
+ /**
21
+ * Only input from a person becomes inspector selection. Script events such
22
+ * as `element.click()` keep reaching the application.
23
+ */
24
+ export function isUserClick(event) {
25
+ return event.isTrusted;
26
+ }
27
+ /** The active inline text editor needs native press events for caret placement. */
28
+ export function isInlineEditorTarget(target) {
29
+ return typeof Element !== "undefined"
30
+ && target instanceof Element
31
+ && target.closest('[data-inline-editor="true"]') !== null;
32
+ }
33
+ /**
34
+ * Keeps an application press gesture from reaching application listeners.
35
+ * Listeners on the document itself, including the inspector's own, still run.
36
+ */
37
+ export function blockApplicationGesture(event) {
38
+ event.stopPropagation();
8
39
  }
9
40
  /** Prevent an inspected application click from reaching the host application. */
10
41
  export function blockApplicationClick(event) {
@@ -1 +1 @@
1
- export declare function installElementSelector(inspectorHost: HTMLElement): () => void;
1
+ export declare function installElementSelector(inspectorHost: HTMLElement, acceptsClick?: (event: Event) => boolean): () => void;
@@ -3,8 +3,8 @@ import { getOpen } from "../shell/openStore.js";
3
3
  import { resolveSelectionFromEvent } from "../selection/resolveSelection.js";
4
4
  import { handleInlineTextEditIntent, isInlineTextEditingActive, } from "../inline-text/inlineTextEditor.js";
5
5
  import { EMPTY_TEXT_PROJECTION_ATTR } from "../projection/textProjection.js";
6
- import { blockApplicationClick, isApplicationActivationClick } from "./clickPolicy.js";
7
- export function installElementSelector(inspectorHost) {
6
+ import { APPLICATION_GESTURE_EVENTS, blockApplicationClick, blockApplicationGesture, isInlineEditorTarget, isUserClick, } from "./clickPolicy.js";
7
+ export function installElementSelector(inspectorHost, acceptsClick = isUserClick) {
8
8
  function applyInlineTextDisposition(event, disposition) {
9
9
  if (disposition === "pass-through")
10
10
  return;
@@ -43,7 +43,7 @@ export function installElementSelector(inspectorHost) {
43
43
  return root instanceof ShadowRoot && root.host === inspectorHost;
44
44
  }
45
45
  function onClick(e) {
46
- if (!getOpen())
46
+ if (!getOpen() || !acceptsClick(e))
47
47
  return;
48
48
  if (isInsideInspectorUi(e)) {
49
49
  // The selector guards the inspected application only. Clicks on the
@@ -61,13 +61,10 @@ export function installElementSelector(inspectorHost) {
61
61
  blockApplicationClick(e);
62
62
  return;
63
63
  }
64
- if (isApplicationActivationClick(e))
65
- return;
66
64
  // The inspector is an editing surface while open. Capture every ordinary
67
65
  // application click so buttons, links, and untracked controls cannot run
68
66
  // alongside selection. Shift-click toggles the primary target in the
69
- // ordered group; Command/Ctrl-click still selects the deepest tracked
70
- // element, and Command/Ctrl+Shift-click remains the activation escape hatch.
67
+ // ordered group; Command/Ctrl-click selects the deepest tracked element.
71
68
  const sel = resolveSelectionFromEvent(e, inspectorHost);
72
69
  blockApplicationClick(e);
73
70
  if (sel) {
@@ -89,12 +86,26 @@ export function installElementSelector(inspectorHost) {
89
86
  clickCount: e.detail,
90
87
  }));
91
88
  }
89
+ function onApplicationGesture(e) {
90
+ if (!getOpen() || !acceptsClick(e) || isInlineEditorTarget(e.target))
91
+ return;
92
+ if (e.target instanceof Element
93
+ && (e.target === inspectorHost || inspectorHost.contains(e.target)))
94
+ return;
95
+ blockApplicationGesture(e);
96
+ }
92
97
  document.addEventListener("mousedown", onMouseDown, true);
93
98
  document.addEventListener("dblclick", onDoubleClick, true);
94
99
  document.addEventListener("click", onClick, true);
100
+ for (const type of APPLICATION_GESTURE_EVENTS) {
101
+ document.addEventListener(type, onApplicationGesture, true);
102
+ }
95
103
  return () => {
96
104
  document.removeEventListener("mousedown", onMouseDown, true);
97
105
  document.removeEventListener("dblclick", onDoubleClick, true);
98
106
  document.removeEventListener("click", onClick, true);
107
+ for (const type of APPLICATION_GESTURE_EVENTS) {
108
+ document.removeEventListener(type, onApplicationGesture, true);
109
+ }
99
110
  };
100
111
  }
@@ -16,7 +16,7 @@ export interface SpacingGuideData {
16
16
  guide: Rect;
17
17
  affectedGuides: Rect[];
18
18
  affectedAreas: Rect[];
19
- /** A 24px-thick pointer target centred on the visible, up-to-40px guide handle. */
19
+ /** A 16px-thick pointer target centred on the visible, up-to-40px guide handle. */
20
20
  hit: Rect;
21
21
  }
22
22
  export interface SpacingAffordance extends SpacingGuideData {
@@ -32,14 +32,14 @@ export declare const SPACING_GUIDE_HANDLE_LENGTH = 40;
32
32
  export declare function getSpacingAffordanceAtPoint(doc: Document, x: number, y: number): SpacingAffordance | null;
33
33
  /** Re-resolves a renderer-reported spacing descriptor in its owner document. */
34
34
  export declare function getSpacingAffordanceForDescriptor(element: HTMLElement, descriptor: SpacingDescriptor): SpacingAffordance | null;
35
- /** Calculates the pixel value represented by a direct-manipulation drag. */
35
+ /** Calculates a direct-manipulation value, optionally stepping changes by 8px. */
36
36
  export declare function spacingValueForDrag(affordance: Pick<SpacingAffordance, "dragAxis" | "direction" | "value">, start: {
37
37
  x: number;
38
38
  y: number;
39
39
  }, point: {
40
40
  x: number;
41
41
  y: number;
42
- }): number;
42
+ }, shiftKey?: boolean): number;
43
43
  export declare function spacingValueCss(value: number): string;
44
44
  /** Serializes the stable part of an affordance for renderer-to-controller messages. */
45
45
  export declare function toSpacingDescriptor(affordance: SpacingAffordance): SpacingDescriptor;