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,6 +1,7 @@
1
1
  import { getElementComputedStyle } from "../runtime/domRealm.js";
2
- const SPACING_DRAG_TARGET_SIZE = 24;
2
+ const SPACING_DRAG_TARGET_SIZE = 16;
3
3
  export const SPACING_GUIDE_HANDLE_LENGTH = 40;
4
+ const SPACING_SHIFT_STEP = 8;
4
5
  const EPSILON = 0.01;
5
6
  function rightOf(rect) {
6
7
  return Number.isFinite(rect.right) ? rect.right : rect.left + rect.width;
@@ -87,10 +88,10 @@ const PADDING_CONFIG = {
87
88
  bottom: { axis: "horizontal", dragAxis: "y", direction: 1 },
88
89
  left: { axis: "vertical", dragAxis: "x", direction: 1 },
89
90
  };
90
- function paddingAffordanceForSide(element, side, allowZero, model = boxModel(element)) {
91
+ function paddingAffordanceForSide(element, side, model = boxModel(element)) {
91
92
  const { inner, content, padding } = model;
92
93
  const value = padding[side];
93
- if (!allowZero && value <= EPSILON)
94
+ if (value <= EPSILON)
94
95
  return null;
95
96
  const config = PADDING_CONFIG[side];
96
97
  const width = Math.max(0, inner.right - inner.left);
@@ -138,13 +139,13 @@ function paddingAffordanceForSide(element, side, allowZero, model = boxModel(ele
138
139
  element,
139
140
  };
140
141
  }
141
- function paddingAffordanceAtPoint(element, x, y, allowZero) {
142
+ function paddingAffordanceAtPoint(element, x, y) {
142
143
  const model = boxModel(element);
143
144
  if (!containsPoint(model.outer, x, y))
144
145
  return null;
145
146
  const sides = ["top", "right", "bottom", "left"];
146
147
  for (const side of sides) {
147
- const affordance = paddingAffordanceForSide(element, side, allowZero, model);
148
+ const affordance = paddingAffordanceForSide(element, side, model);
148
149
  if (affordance && containsRectPoint(affordance.hit, x, y))
149
150
  return affordance;
150
151
  }
@@ -301,7 +302,7 @@ function trackedElementsAtPoint(doc, x, y) {
301
302
  export function getSpacingAffordanceAtPoint(doc, x, y) {
302
303
  const candidates = trackedElementsAtPoint(doc, x, y);
303
304
  for (const element of candidates) {
304
- const padding = paddingAffordanceAtPoint(element, x, y, false);
305
+ const padding = paddingAffordanceAtPoint(element, x, y);
305
306
  if (padding)
306
307
  return padding;
307
308
  }
@@ -310,13 +311,6 @@ export function getSpacingAffordanceAtPoint(doc, x, y) {
310
311
  if (gap)
311
312
  return gap;
312
313
  }
313
- // A zero-sized padding region still needs a small draggable edge so users
314
- // can establish padding without first typing a value in the inspector.
315
- for (const element of candidates) {
316
- const padding = paddingAffordanceAtPoint(element, x, y, true);
317
- if (padding)
318
- return padding;
319
- }
320
314
  return null;
321
315
  }
322
316
  /** Re-resolves a renderer-reported spacing descriptor in its owner document. */
@@ -325,7 +319,7 @@ export function getSpacingAffordanceForDescriptor(element, descriptor) {
325
319
  const side = descriptor.side;
326
320
  if (!side || descriptor.property !== `padding-${side}`)
327
321
  return null;
328
- return paddingAffordanceForSide(element, side, true);
322
+ return paddingAffordanceForSide(element, side);
329
323
  }
330
324
  if (descriptor.property !== "row-gap" && descriptor.property !== "column-gap")
331
325
  return null;
@@ -335,10 +329,14 @@ export function getSpacingAffordanceForDescriptor(element, descriptor) {
335
329
  const guide = gapGuide(segment);
336
330
  return gapAffordanceAtPoint(element, guide.left + guide.width / 2, guide.top + guide.height / 2, descriptor.property);
337
331
  }
338
- /** Calculates the pixel value represented by a direct-manipulation drag. */
339
- export function spacingValueForDrag(affordance, start, point) {
332
+ /** Calculates a direct-manipulation value, optionally stepping changes by 8px. */
333
+ export function spacingValueForDrag(affordance, start, point, shiftKey = false) {
340
334
  const delta = affordance.dragAxis === "x" ? point.x - start.x : point.y - start.y;
341
- return Math.max(0, Math.round(affordance.value + delta * affordance.direction));
335
+ const signedDelta = delta * affordance.direction;
336
+ const adjustedDelta = shiftKey
337
+ ? Math.trunc(signedDelta / SPACING_SHIFT_STEP) * SPACING_SHIFT_STEP
338
+ : signedDelta;
339
+ return Math.max(0, Math.round(affordance.value + adjustedDelta));
342
340
  }
343
341
  export function spacingValueCss(value) {
344
342
  return `${Math.max(0, Math.round(value))}px`;
@@ -24,7 +24,7 @@ export interface NudgeUiRuntimeCapabilities {
24
24
  readonly canvas: boolean;
25
25
  /** Whether framework component inspection and prop overrides are available. */
26
26
  readonly componentSemantics: boolean;
27
- /** Whether the experimental DOM navigation section is available. */
27
+ /** Whether the DOM navigator overlay is available. */
28
28
  readonly domNavigation?: boolean;
29
29
  /**
30
30
  * Source-coordinate precision policy for prompts; every source site is
@@ -3,5 +3,11 @@ export interface DescendantNode {
3
3
  element: HTMLElement;
4
4
  depth: number;
5
5
  }
6
+ export interface NavigationNode {
7
+ element: HTMLElement;
8
+ direction: "up" | "down";
9
+ depth: number;
10
+ }
6
11
  /** Returns the nearest tracked descendants, breadth-first and document-ordered. */
7
12
  export declare function computeDescendants(el: HTMLElement, maxDepth?: number, maxNodes?: number): DescendantNode[];
13
+ export declare function computeNavigationNodes(el: HTMLElement, hierarchy?: HTMLElement[]): NavigationNode[];
@@ -40,3 +40,11 @@ export function computeDescendants(el, maxDepth = 2, maxNodes = 2) {
40
40
  .sort((left, right) => left.depth - right.depth || left.order - right.order);
41
41
  return descendants.slice(0, maxNodes).map(({ element, depth }) => ({ element, depth }));
42
42
  }
43
+ export function computeNavigationNodes(el, hierarchy = computeHierarchy(el)) {
44
+ const children = computeDescendants(el, 2, 2);
45
+ const parents = hierarchy.slice(1, (children.length > 0 ? 2 : 4) + 1);
46
+ return [
47
+ ...parents.map((element, index) => ({ element, direction: "up", depth: index + 1 })),
48
+ ...children.map(({ element, depth }) => ({ element, direction: "down", depth })),
49
+ ];
50
+ }
@@ -9,4 +9,4 @@ export interface SettingsDialogProps extends McpConnectionContentProps {
9
9
  readonly onOpenChange: (open: boolean) => void;
10
10
  }
11
11
  /** Provides one settings surface for prompt, MCP, and token configuration. */
12
- export declare function SettingsDialog({ open, initialSection, value, onValueChange, onOpenChange, projectId, origin, snapshot, onConnect, onDisconnect, onCheckAgain, }: SettingsDialogProps): ReactElement;
12
+ export declare function SettingsDialog({ open, initialSection, value, onValueChange, onOpenChange, projectId, origin, snapshot, onConnect, onTakeOver, onDisconnect, onCheckAgain, }: SettingsDialogProps): ReactElement;
@@ -33,7 +33,7 @@ function portalContainer() {
33
33
  : null;
34
34
  }
35
35
  /** Provides one settings surface for prompt, MCP, and token configuration. */
36
- export function SettingsDialog({ open, initialSection, value, onValueChange, onOpenChange, projectId, origin, snapshot, onConnect, onDisconnect, onCheckAgain, }) {
36
+ export function SettingsDialog({ open, initialSection, value, onValueChange, onOpenChange, projectId, origin, snapshot, onConnect, onTakeOver, onDisconnect, onCheckAgain, }) {
37
37
  const [activeSection, setActiveSection] = useState(initialSection);
38
38
  const closeRef = useRef(null);
39
39
  const instructionsRef = useRef(null);
@@ -52,5 +52,5 @@ export function SettingsDialog({ open, initialSection, value, onValueChange, onO
52
52
  if (open)
53
53
  setActiveSection(initialSection);
54
54
  }, [initialSection, open]);
55
- return (_jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Portal, { container: portalContainer(), children: [_jsx(Dialog.Backdrop, { className: "settings__backdrop", "data-test": "settings-backdrop" }), _jsxs(Dialog.Popup, { className: "settings__popup", "data-test": "settings-dialog", initialFocus: closeRef, children: [_jsx(Dialog.Title, { className: "settings__visually-hidden", children: "Settings" }), _jsx(Dialog.Description, { className: "settings__visually-hidden", children: "Configure how Nudge connects to your coding agent and uses your design system." }), _jsx(Dialog.Close, { ref: closeRef, className: "icon-button icon-button--quiet settings__close", "aria-label": "Close settings", "data-test": "settings-close", type: "button", children: _jsx(IconX, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) }), _jsxs("div", { className: "settings__body", children: [_jsxs("nav", { className: "settings__nav", "aria-label": "Settings sections", children: [_jsx("span", { className: "settings__nav-label", children: "Settings" }), _jsx("div", { className: "settings__nav-items", children: SETTINGS_SECTIONS.map((section) => (_jsxs("button", { className: `settings__nav-item${activeSection === section.id ? " settings__nav-item--active" : ""}`, "data-test": `settings-nav-${section.id}`, type: "button", "aria-current": activeSection === section.id ? "page" : undefined, onClick: () => setActiveSection(section.id), children: [section.icon, _jsx("span", { className: "settings__nav-item-label", children: section.label })] }, section.id))) })] }), _jsxs("main", { className: "settings__content", "data-test": "settings-content", children: [activeSection === "instructions" ? (_jsxs("section", { className: "settings__section", "data-test": "prompt-settings-dialog", "data-settings-section": "instructions", children: [_jsx("h2", { className: "settings__section-title", children: activeDefinition.label }), _jsx("p", { className: "settings__section-description", children: "These instructions are added to the end of every copied prompt." }), _jsx(PromptSettingsFields, { value: value, onChange: onValueChange, instructionsRef: instructionsRef })] })) : null, activeSection === "mcp" ? (_jsxs("section", { className: "settings__section", "data-test": "settings-section-mcp", children: [_jsx("h2", { className: "settings__section-title", children: activeDefinition.label }), _jsx("div", { "data-test": "mcp-connection-dialog", children: _jsx(McpConnectionContent, { projectId: projectId, origin: origin, snapshot: snapshot, onConnect: onConnect, onDisconnect: onDisconnect, onCheckAgain: onCheckAgain }) })] })) : null, activeSection === "tokens" ? (_jsxs("section", { className: "settings__section", "data-test": "settings-section-tokens", children: [_jsx("h2", { className: "settings__section-title", children: activeDefinition.label }), _jsx("p", { className: "settings__section-description", children: "Inspect and edit the design tokens available in this page." }), _jsx(TokensPanel, { rows: tokenRows })] })) : null] })] })] })] }) }));
55
+ return (_jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Portal, { container: portalContainer(), children: [_jsx(Dialog.Backdrop, { className: "settings__backdrop", "data-test": "settings-backdrop" }), _jsxs(Dialog.Popup, { className: "settings__popup", "data-test": "settings-dialog", initialFocus: closeRef, children: [_jsx(Dialog.Title, { className: "settings__visually-hidden", children: "Settings" }), _jsx(Dialog.Description, { className: "settings__visually-hidden", children: "Configure how Nudge connects to your coding agent and uses your design system." }), _jsx(Dialog.Close, { ref: closeRef, className: "icon-button icon-button--quiet settings__close", "aria-label": "Close settings", "data-test": "settings-close", type: "button", children: _jsx(IconX, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) }), _jsxs("div", { className: "settings__body", children: [_jsxs("nav", { className: "settings__nav", "aria-label": "Settings sections", children: [_jsx("span", { className: "settings__nav-label", children: "Settings" }), _jsx("div", { className: "settings__nav-items", children: SETTINGS_SECTIONS.map((section) => (_jsxs("button", { className: `settings__nav-item${activeSection === section.id ? " settings__nav-item--active" : ""}`, "data-test": `settings-nav-${section.id}`, type: "button", "aria-current": activeSection === section.id ? "page" : undefined, onClick: () => setActiveSection(section.id), children: [section.icon, _jsx("span", { className: "settings__nav-item-label", children: section.label })] }, section.id))) })] }), _jsxs("main", { className: "settings__content", "data-test": "settings-content", children: [activeSection === "instructions" ? (_jsxs("section", { className: "settings__section", "data-test": "prompt-settings-dialog", "data-settings-section": "instructions", children: [_jsx("h2", { className: "settings__section-title", children: activeDefinition.label }), _jsx("p", { className: "settings__section-description", children: "These instructions are added to the end of every copied prompt." }), _jsx(PromptSettingsFields, { value: value, onChange: onValueChange, instructionsRef: instructionsRef })] })) : null, activeSection === "mcp" ? (_jsxs("section", { className: "settings__section", "data-test": "settings-section-mcp", children: [_jsx("h2", { className: "settings__section-title", children: activeDefinition.label }), _jsx("div", { "data-test": "mcp-connection-dialog", children: _jsx(McpConnectionContent, { projectId: projectId, origin: origin, snapshot: snapshot, onConnect: onConnect, onTakeOver: onTakeOver, onDisconnect: onDisconnect, onCheckAgain: onCheckAgain }) })] })) : null, activeSection === "tokens" ? (_jsxs("section", { className: "settings__section", "data-test": "settings-section-tokens", children: [_jsx("h2", { className: "settings__section-title", children: activeDefinition.label }), _jsx("p", { className: "settings__section-description", children: "Inspect and edit the design tokens available in this page." }), _jsx(TokensPanel, { rows: tokenRows })] })) : null] })] })] })] }) }));
56
56
  }
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useMemo, useSyncExternalStore } from "react";
3
- import { IconChevronDown } from "@tabler/icons-react";
4
3
  import { isElementChange, useChanges, revertChange } from "../changes/changesLog.js";
5
4
  import { StaleChangeIndicator } from "../canvas/StaleChangeIndicator.js";
6
5
  import { Button } from "../ui/Button.js";
6
+ import { Disclosure } from "../ui/Disclosure.js";
7
7
  import { presentChange } from "../changes/presentation.js";
8
8
  import { getStructuralChanges, getStructuralChangeDiagnostics, getStructuralDiagnosticRevision, revertStructuralChange, subscribeStructuralChanges, subscribeStructuralDiagnostics, } from "../projection/structuralProjection.js";
9
9
  import { getRenderedInstanceChangeDiagnostics, getRenderedInstanceDiagnosticRevision, subscribeRenderedInstanceDiagnostics, } from "../projection/renderedInstance.js";
@@ -74,23 +74,23 @@ export function ChangesLog({ onClearSession }) {
74
74
  const total = changes.length + structuralChanges.length + sketches.length;
75
75
  if (total === 0 && sketchStore.error === null)
76
76
  return null;
77
- return (_jsxs(_Fragment, { children: [_jsxs("details", { className: "changes", "data-test": "changes-log", children: [_jsxs("summary", { className: "changes__title", "data-test": "changes-toggle", children: [_jsx("span", { className: "changes__title-label", children: "Changes" }), total > 0 ? _jsx("span", { className: "changes__count", children: total }) : null, _jsx(IconChevronDown, { className: "changes__toggle-icon", size: 15, stroke: 2, "aria-hidden": "true" })] }), _jsx("div", { className: "changes__content", children: groups.length === 0 && structuralChanges.length === 0 && sketches.length === 0 && sketchStore.error === null ? (_jsx("div", { className: "changes__empty", "data-test": "changes-empty", children: "No changes yet" })) : (_jsxs(_Fragment, { children: [groups.map((group) => (_jsxs("div", { className: "changes__group", "data-test": "changes-group", "data-cid": group.label, children: [_jsxs("div", { className: "changes__group-title", children: [_jsx("span", { children: group.label }), _jsx("span", { className: "changes__group-file", children: group.file })] }), group.changes.map((change, i) => {
78
- const presentation = presentChange(change);
79
- const instanceDiagnostics = isElementChange(change) && change.scope === "rendered-instance"
80
- && change.instanceOverride
81
- ? getRenderedInstanceChangeDiagnostics(change.instanceOverride.id)
82
- : [];
83
- const textDiagnostics = change.kind === "text-content"
84
- ? getTextContentChangeDiagnostics(change.id)
85
- : [];
86
- return (_jsxs("div", { className: "changes__row", "data-test": "change-row", "data-property": presentation.property, children: [_jsx("span", { className: "changes__prop", children: presentation.propertyLabel }), _jsxs("span", { className: "changes__value", children: [_jsx("span", { className: "changes__before", children: presentation.before }), _jsx("span", { className: "changes__arrow", children: "\u2192" }), _jsx("span", { className: "changes__after", children: presentation.after })] }), presentation.scope ? (_jsx("span", { className: "changes__scope", "data-test": "change-scope", children: presentation.scope === "source-site" ? "All outputs at source site" : "This rendered item only" })) : null, presentation.evidence ? (_jsx("span", { className: "changes__evidence", "data-test": "change-evidence", children: presentation.evidence })) : null, _jsx(StaleChangeIndicator, { change: change }), instanceDiagnostics.map((diagnostic) => (_jsxs("span", { className: "changes__diagnostic", "data-test": "instance-diagnostic", "data-document": diagnostic.document, "data-status": diagnostic.status, children: [diagnostic.document, ": ", diagnostic.status] }, `${diagnostic.document}:${diagnostic.status}`))), textDiagnostics.map((diagnostic) => (_jsxs("span", { className: "changes__diagnostic", "data-test": "text-projection-diagnostic", "data-document": diagnostic.document, "data-status": diagnostic.status, children: [diagnostic.document, ": ", diagnostic.status] }, `${diagnostic.document}:${diagnostic.status}`))), _jsx(Button, { size: "compact", className: "changes__revert", "data-test": "change-revert", "data-property": presentation.property, onClick: () => revertChange(change), children: "Revert" })] }, `${group.key}\u0000${presentation.property}\u0000${i}`));
87
- })] }, group.key))), structuralChanges.map((change) => (_jsxs("div", { className: "changes__group", "data-test": "dom-change", "data-cid": change.target.sourceSite.cid, children: [_jsxs("div", { className: "changes__group-title", children: [_jsx("span", { children: change.target.sourceSite.cid }), _jsx("span", { className: "changes__group-file", children: sourceFile(change) })] }), _jsxs("div", { className: "changes__row", "data-test": "dom-change-row", "data-action": change.kind, children: [_jsxs("span", { className: "changes__source-site", "data-test": "structural-source-site", children: ["Source site: ", change.target.sourceSite.cid] }), _jsx("span", { className: "changes__prop", children: change.kind === "move" ? "Move in DOM" : "Delete from DOM" }), _jsxs("span", { className: "changes__value", children: [_jsx("span", { className: "changes__before", children: change.kind === "move"
88
- ? structuralParentIdentityKey(change.source.parent) === structuralParentIdentityKey(change.destination.parent)
89
- ? `${change.presentation.sourceParentTag} position ${change.presentation.fromIndex + 1}`
90
- : `${structuralParentLabel(change, "source")} position ${change.presentation.fromIndex + 1}`
91
- : "Visible" }), _jsx("span", { className: "changes__arrow", children: "\u2192" }), _jsx("span", { className: "changes__after", children: change.kind === "move"
92
- ? structuralParentIdentityKey(change.source.parent) === structuralParentIdentityKey(change.destination.parent)
93
- ? `${change.presentation.destinationParentTag} position ${change.presentation.toIndex + 1}`
94
- : `${structuralParentLabel(change, "destination")} position ${change.presentation.toIndex + 1}`
95
- : "Removed" })] }), _jsx("span", { className: "changes__scope", "data-test": "structural-scope", children: "This rendered item only" }), getStructuralChangeDiagnostics(change.id).map((diagnostic) => (_jsx("span", { className: "changes__diagnostic", "data-test": "structural-diagnostic", "data-document": diagnostic.document, "data-status": diagnostic.status, "data-reason": diagnostic.reason, children: structuralDiagnosticText(diagnostic) }, `${diagnostic.document}:${diagnostic.status}:${diagnostic.reason ?? ""}`))), _jsx(Button, { size: "compact", className: "changes__revert", "data-test": "dom-change-revert", onClick: () => revertStructuralChange(change.id), children: "Revert" })] })] }, change.id))), _jsx(SketchChanges, {})] })) })] }), onClearSession ? (_jsx("div", { className: "changes__session-action", "data-test": "session-actions", children: _jsx(Button, { variant: "secondary", type: "button", "data-test": "clear-session", onClick: onClearSession, children: "Clear Changes" }) })) : null] }));
77
+ return (_jsxs(_Fragment, { children: [_jsx(Disclosure, { className: "changes", "data-test": "changes-log", triggerDataTest: "changes-toggle", title: "Changes", badge: total > 0 ? _jsx("span", { className: "changes__count", children: total }) : null, panelClassName: "changes__content", children: groups.length === 0 && structuralChanges.length === 0 && sketches.length === 0 && sketchStore.error === null ? (_jsx("div", { className: "changes__empty", "data-test": "changes-empty", children: "No changes yet" })) : (_jsxs(_Fragment, { children: [groups.map((group) => (_jsxs("div", { className: "changes__group", "data-test": "changes-group", "data-cid": group.label, children: [_jsxs("div", { className: "changes__group-title", children: [_jsx("span", { children: group.label }), _jsx("span", { className: "changes__group-file", children: group.file })] }), group.changes.map((change, i) => {
78
+ const presentation = presentChange(change);
79
+ const instanceDiagnostics = isElementChange(change) && change.scope === "rendered-instance"
80
+ && change.instanceOverride
81
+ ? getRenderedInstanceChangeDiagnostics(change.instanceOverride.id)
82
+ : [];
83
+ const textDiagnostics = change.kind === "text-content"
84
+ ? getTextContentChangeDiagnostics(change.id)
85
+ : [];
86
+ return (_jsxs("div", { className: "changes__row", "data-test": "change-row", "data-property": presentation.property, children: [_jsx("span", { className: "changes__prop", children: presentation.propertyLabel }), _jsxs("span", { className: "changes__value", children: [_jsx("span", { className: "changes__before", children: presentation.before }), _jsx("span", { className: "changes__arrow", children: "\u2192" }), _jsx("span", { className: "changes__after", children: presentation.after })] }), presentation.scope ? (_jsx("span", { className: "changes__scope", "data-test": "change-scope", children: presentation.scope === "source-site" ? "All outputs at source site" : "This rendered item only" })) : null, presentation.evidence ? (_jsx("span", { className: "changes__evidence", "data-test": "change-evidence", children: presentation.evidence })) : null, _jsx(StaleChangeIndicator, { change: change }), instanceDiagnostics.map((diagnostic) => (_jsxs("span", { className: "changes__diagnostic", "data-test": "instance-diagnostic", "data-document": diagnostic.document, "data-status": diagnostic.status, children: [diagnostic.document, ": ", diagnostic.status] }, `${diagnostic.document}:${diagnostic.status}`))), textDiagnostics.map((diagnostic) => (_jsxs("span", { className: "changes__diagnostic", "data-test": "text-projection-diagnostic", "data-document": diagnostic.document, "data-status": diagnostic.status, children: [diagnostic.document, ": ", diagnostic.status] }, `${diagnostic.document}:${diagnostic.status}`))), _jsx(Button, { size: "compact", className: "changes__revert", "data-test": "change-revert", "data-property": presentation.property, onClick: () => revertChange(change), children: "Revert" })] }, `${group.key}\u0000${presentation.property}\u0000${i}`));
87
+ })] }, group.key))), structuralChanges.map((change) => (_jsxs("div", { className: "changes__group", "data-test": "dom-change", "data-cid": change.target.sourceSite.cid, children: [_jsxs("div", { className: "changes__group-title", children: [_jsx("span", { children: change.target.sourceSite.cid }), _jsx("span", { className: "changes__group-file", children: sourceFile(change) })] }), _jsxs("div", { className: "changes__row", "data-test": "dom-change-row", "data-action": change.kind, children: [_jsxs("span", { className: "changes__source-site", "data-test": "structural-source-site", children: ["Source site: ", change.target.sourceSite.cid] }), _jsx("span", { className: "changes__prop", children: change.kind === "move" ? "Move in DOM" : "Delete from DOM" }), _jsxs("span", { className: "changes__value", children: [_jsx("span", { className: "changes__before", children: change.kind === "move"
88
+ ? structuralParentIdentityKey(change.source.parent) === structuralParentIdentityKey(change.destination.parent)
89
+ ? `${change.presentation.sourceParentTag} position ${change.presentation.fromIndex + 1}`
90
+ : `${structuralParentLabel(change, "source")} position ${change.presentation.fromIndex + 1}`
91
+ : "Visible" }), _jsx("span", { className: "changes__arrow", children: "\u2192" }), _jsx("span", { className: "changes__after", children: change.kind === "move"
92
+ ? structuralParentIdentityKey(change.source.parent) === structuralParentIdentityKey(change.destination.parent)
93
+ ? `${change.presentation.destinationParentTag} position ${change.presentation.toIndex + 1}`
94
+ : `${structuralParentLabel(change, "destination")} position ${change.presentation.toIndex + 1}`
95
+ : "Removed" })] }), _jsx("span", { className: "changes__scope", "data-test": "structural-scope", children: "This rendered item only" }), getStructuralChangeDiagnostics(change.id).map((diagnostic) => (_jsx("span", { className: "changes__diagnostic", "data-test": "structural-diagnostic", "data-document": diagnostic.document, "data-status": diagnostic.status, "data-reason": diagnostic.reason, children: structuralDiagnosticText(diagnostic) }, `${diagnostic.document}:${diagnostic.status}:${diagnostic.reason ?? ""}`))), _jsx(Button, { size: "compact", className: "changes__revert", "data-test": "dom-change-revert", onClick: () => revertStructuralChange(change.id), children: "Revert" })] })] }, change.id))), _jsx(SketchChanges, {})] })) }), onClearSession ? (_jsx("div", { className: "changes__session-action", "data-test": "session-actions", children: _jsx(Button, { variant: "secondary", type: "button", "data-test": "clear-session", onClick: onClearSession, children: "Clear Changes" }) })) : null] }));
96
96
  }
@@ -224,12 +224,19 @@ export function CopyPromptButton({ settingsOpen: controlledSettingsOpen, setting
224
224
  function openMcpConnection() {
225
225
  openSettings("mcp");
226
226
  }
227
- return (_jsxs("div", { className: "copy-prompt__stack", "data-test": "copy-prompt-control", children: [_jsxs(Button, { variant: "primary", className: "copy-prompt__main", "data-test": "copy-prompt", type: "button", disabled: disabled, "data-copied": copied ? "true" : "false", "data-agent-state": agent.state, "data-agent-listening": agentStatus.kind === "listening" ? "true" : undefined, "aria-busy": working || connecting ? "true" : undefined, title: agent.error, onClick: onClick, children: [icon, label, changeCount > 0 ? (_jsx("span", { className: "copy-prompt__change-count", "data-test": "copy-prompt-change-count", children: changeCount })) : null, agentStatus.kind === "listening" ? (_jsx("span", { className: "copy-prompt__agent-listening", "data-test": "agent-listening-indicator", role: "status", "aria-label": "Agent listening" })) : null] }), _jsx(SketchLayersPanel, {}), statusAction ? (_jsx(StatusCallout, { className: "copy-prompt__agent-status", tone: agentStatus.tone, "data-test": "agent-connection-status", children: _jsxs("div", { className: "copy-prompt__agent-status-content", children: [_jsx("span", { role: "status", children: agentStatus.label }), _jsx(Button, { size: "compact", variant: "quiet", "data-test": "agent-status-action", "data-action": statusAction.kind, type: "button", onClick: openMcpConnection, children: statusAction.label })] }) })) : null, reconciledCount > 0 ? (_jsxs("p", { className: "copy-prompt__hint copy-prompt__hint--centered", "data-test": "clipboard-reconciled-hint", role: "status", children: [reconciledCount, " Implemented changes"] })) : null, agentCompletionStatus === "verified" ? (_jsx("p", { className: "copy-prompt__hint", "data-test": "agent-verified-hint", role: "status", children: "Agent changes verified." })) : null, sketchFallback ? (_jsxs(StatusCallout, { className: "copy-prompt__sketch-fallback", tone: "warning", "data-test": "sketch-dispatch-error", children: [_jsx("span", { children: sketchFallback.error }), _jsx(Button, { size: "compact", variant: "secondary", type: "button", "data-test": "sketch-copy-fallback", onClick: () => {
227
+ async function handleAgentStatusAction() {
228
+ if (statusAction?.kind === "takeover") {
229
+ await agentClient.takeOver();
230
+ return;
231
+ }
232
+ openMcpConnection();
233
+ }
234
+ return (_jsxs("div", { className: "copy-prompt__stack", "data-test": "copy-prompt-control", children: [_jsxs(Button, { variant: "primary", className: "copy-prompt__main", "data-test": "copy-prompt", type: "button", disabled: disabled, "data-copied": copied ? "true" : "false", "data-agent-state": agent.state, "data-agent-listening": agentStatus.kind === "listening" ? "true" : undefined, "aria-busy": working || connecting ? "true" : undefined, title: agent.error, onClick: onClick, children: [icon, label, changeCount > 0 ? (_jsx("span", { className: "copy-prompt__change-count", "data-test": "copy-prompt-change-count", children: changeCount })) : null, agentStatus.kind === "listening" ? (_jsx("span", { className: "copy-prompt__agent-listening", "data-test": "agent-listening-indicator", role: "status", "aria-label": "Agent listening" })) : null] }), _jsx(SketchLayersPanel, {}), statusAction || agentStatus.kind === "connected-not-listening" ? (_jsx(StatusCallout, { className: "copy-prompt__agent-status", tone: agentStatus.tone, "data-test": "agent-connection-status", children: _jsxs("div", { className: "copy-prompt__agent-status-content", children: [_jsx("span", { role: "status", children: agentStatus.label }), statusAction ? (_jsx(Button, { size: "compact", variant: "quiet", "data-test": "agent-status-action", "data-action": statusAction.kind, type: "button", onClick: () => void handleAgentStatusAction(), children: statusAction.label })) : null] }) })) : null, reconciledCount > 0 ? (_jsxs("p", { className: "copy-prompt__hint copy-prompt__hint--centered", "data-test": "clipboard-reconciled-hint", role: "status", children: [reconciledCount, " Implemented changes"] })) : null, agentCompletionStatus === "verified" ? (_jsx("p", { className: "copy-prompt__hint", "data-test": "agent-verified-hint", role: "status", children: "Agent changes verified." })) : null, sketchFallback ? (_jsxs(StatusCallout, { className: "copy-prompt__sketch-fallback", tone: "warning", "data-test": "sketch-dispatch-error", children: [_jsx("span", { children: sketchFallback.error }), _jsx(Button, { size: "compact", variant: "secondary", type: "button", "data-test": "sketch-copy-fallback", onClick: () => {
228
235
  void copyToClipboard(sketchFallback.prompt).then(() => {
229
236
  if (changes.length > 0 || structuralChanges.length > 0) {
230
237
  recordClipboardHandoff(changes, structuralChanges);
231
238
  }
232
239
  setSketchFallback(null);
233
240
  }).catch(() => undefined);
234
- }, children: "Copy prompt" })] })) : null, _jsx(SettingsDialog, { open: settingsOpen, initialSection: settingsSection, value: customInstructions, onValueChange: handleCustomInstructionsChange, onOpenChange: setSettingsOpen, projectId: runtimeConfig.projectId, origin: window.location.origin, snapshot: agent, onConnect: () => agentClient.connect(), onDisconnect: () => agentClient.disconnect(), onCheckAgain: () => agentClient.checkConnection() })] }));
241
+ }, children: "Copy prompt" })] })) : null, _jsx(SettingsDialog, { open: settingsOpen, initialSection: settingsSection, value: customInstructions, onValueChange: handleCustomInstructionsChange, onOpenChange: setSettingsOpen, projectId: runtimeConfig.projectId, origin: window.location.origin, snapshot: agent, onConnect: () => agentClient.connect(), onTakeOver: () => agentClient.takeOver(), onDisconnect: () => agentClient.disconnect(), onCheckAgain: () => agentClient.checkConnection() })] }));
235
242
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useEffect, useMemo, useState } from "react";
3
3
  import { IconLayoutSidebarRight, IconSettings } from "@tabler/icons-react";
4
4
  import { useInspectorOpen, toggleInspector, setInspectorOpen } from "./openStore.js";
5
- import { useSelectedElement, useSelectedElements, useHierarchy, setSelectedElement, removeSelectedElement, } from "../selection/selectionStore.js";
5
+ import { useSelectedElement, useSelectedElements, setSelectedElement, removeSelectedElement, } from "../selection/selectionStore.js";
6
6
  import { findTokenRow } from "../styleEditors/rowLookup.js";
7
7
  import { useBrowserCssInspection } from "../inspection/useBrowserCssInspection.js";
8
8
  import { resolveSelectionFromElement } from "../selection/resolveSelection.js";
@@ -40,7 +40,6 @@ import { ComponentPropsSection } from "../componentSemantics/ComponentPropsSecti
40
40
  import { cancelInlineTextEdit, isInlineTextEditingActive, useInlineTextDiagnostic, } from "../inline-text/inlineTextEditor.js";
41
41
  import { getInlineTextFeedback } from "../inline-text/inlineTextFeedback.js";
42
42
  import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
43
- import { DomNavigation } from "./DomNavigation.js";
44
43
  import { EmptyState } from "./EmptyState.js";
45
44
  import { createStyleSelection } from "../selection/styleSelection.js";
46
45
  import { intersectTokenEntries } from "../inspection/selectionProperty.js";
@@ -90,7 +89,6 @@ function scopeMutationAffectsSelection(records, selected) {
90
89
  export function InspectorShell() {
91
90
  const isOpen = useInspectorOpen();
92
91
  const runtimeConfig = useNudgeUiRuntimeConfig();
93
- const domNavigationEnabled = runtimeConfig.capabilities.domNavigation === true;
94
92
  const sketchEnabled = isNudgeUiDev() && !isDemoRuntime();
95
93
  const sketchActive = useSketchInteractionActive();
96
94
  const selectedCardId = useSelectedCardId();
@@ -98,7 +96,6 @@ export function InspectorShell() {
98
96
  const sketchHost = selectedCardId || focusedCardId ? getActiveCanvasFrame() : null;
99
97
  const selected = useSelectedElement();
100
98
  const selectedElements = useSelectedElements();
101
- const hierarchy = useHierarchy();
102
99
  const inlineTextFeedback = getInlineTextFeedback(useInlineTextDiagnostic());
103
100
  const [settingsOpen, setSettingsOpen] = useState(false);
104
101
  const [settingsSection, setSettingsSection] = useState("instructions");
@@ -246,7 +243,7 @@ export function InspectorShell() {
246
243
  return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "inspector-styles", children: UI_STYLES }), _jsxs("div", { className: "panel", "data-open": isOpen ? "true" : "false", children: [_jsxs("div", { className: "panel__tabs", "aria-label": "Inspector controls", children: [_jsxs("div", { className: "panel__header-row", "data-test": "inspect-tab", children: [_jsx(IconButton, { variant: "quiet", label: "Collapse inspector", "data-test": "collapse-inspector", disabled: sketchActive, style: { marginLeft: "-8px" }, onClick: () => {
247
244
  cancelInlineTextEdit();
248
245
  setInspectorOpen(false);
249
- }, children: _jsx(IconLayoutSidebarRight, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) }), _jsxs("div", { className: "panel__header-actions", children: [_jsx(PeekOriginalButton, {}), _jsx(IconButton, { variant: "quiet", label: "Settings", title: "Settings", "data-test": "settings-button", onClick: () => openSettings("instructions"), children: _jsx(IconSettings, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) })] })] }), _jsx("div", { className: "panel__copy-row", children: _jsx(CopyPromptButton, { settingsOpen: settingsOpen, settingsSection: settingsSection, onOpenSettings: openSettings, onSettingsOpenChange: setSettingsOpen }) })] }), _jsxs("div", { className: "panel__body", children: [inlineTextFeedback ? (_jsx(StatusCallout, { tone: "danger", className: "inline-text-diagnostic", "data-test": "inline-text-diagnostic", role: "status", "aria-live": "polite", "aria-atomic": "true", children: _jsx("span", { children: inlineTextFeedback.message }) })) : null, selected ? (_jsxs(_Fragment, { children: [domNavigationEnabled && !isMultiSelection ? _jsx(DomNavigation, { selected: selected, hierarchy: hierarchy }) : null, isMultiSelection || showInteractionState || hasEditScopeCallout ? (_jsxs("div", { className: `selection${hasEditScopeCallout ? "" : " selection--without-scope-callout"}`, "data-test": "selection", "data-selected-cid": selected.cid, "data-selected-src": selected.src, "data-selected-count": selectedElements.length, children: [isMultiSelection ? (_jsx("div", { className: "selection__summary", "data-test": "multi-selection-summary", children: _jsxs("span", { className: "selection__count", children: [selectedElements.length, " elements selected"] }) })) : null, hasEditScopeCallout ? (_jsx(StatusCallout, { tone: editScope === "rendered-instance" ? "neutral" : "accent", "data-test": "edit-scope", "data-lost": "false", children: editScope === "rendered-instance" ? (_jsxs("div", { className: "scope__unlinked", children: [_jsx("span", { children: "Element unlinked" }), _jsx(Button, { size: "compact", className: "scope__action", "data-test": "relink-element", onClick: () => {
246
+ }, children: _jsx(IconLayoutSidebarRight, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) }), _jsxs("div", { className: "panel__header-actions", children: [_jsx(PeekOriginalButton, {}), _jsx(IconButton, { variant: "quiet", label: "Settings", title: "Settings", "data-test": "settings-button", onClick: () => openSettings("instructions"), children: _jsx(IconSettings, { size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true" }) })] })] }), _jsx("div", { className: "panel__copy-row", children: _jsx(CopyPromptButton, { settingsOpen: settingsOpen, settingsSection: settingsSection, onOpenSettings: openSettings, onSettingsOpenChange: setSettingsOpen }) })] }), _jsxs("div", { className: "panel__body", children: [inlineTextFeedback ? (_jsx(StatusCallout, { tone: "danger", className: "inline-text-diagnostic", "data-test": "inline-text-diagnostic", role: "status", "aria-live": "polite", "aria-atomic": "true", children: _jsx("span", { children: inlineTextFeedback.message }) })) : null, selected ? (_jsxs(_Fragment, { children: [isMultiSelection || showInteractionState || hasEditScopeCallout ? (_jsxs("div", { className: `selection${hasEditScopeCallout ? "" : " selection--without-scope-callout"}`, "data-test": "selection", "data-selected-cid": selected.cid, "data-selected-src": selected.src, "data-selected-count": selectedElements.length, children: [isMultiSelection ? (_jsx("div", { className: "selection__summary", "data-test": "multi-selection-summary", children: _jsxs("span", { className: "selection__count", children: [selectedElements.length, " elements selected"] }) })) : null, hasEditScopeCallout ? (_jsx(StatusCallout, { tone: editScope === "rendered-instance" ? "neutral" : "accent", "data-test": "edit-scope", "data-lost": "false", children: editScope === "rendered-instance" ? (_jsxs("div", { className: "scope__unlinked", children: [_jsx("span", { children: "Element unlinked" }), _jsx(Button, { size: "compact", className: "scope__action", "data-test": "relink-element", onClick: () => {
250
247
  const overrideId = relinkElement(selected.domElement);
251
248
  if (overrideId)
252
249
  discardChangesForInstanceOverride(overrideId);
@@ -291,34 +291,54 @@ export function markSketchesHandingOff(entries, batchRevision, localBatchId, age
291
291
  }));
292
292
  }
293
293
  export function settleSketchDispatch(batchRevision, state, agentRequestId, localBatchId) {
294
- if (batchRevision === undefined)
295
- return Promise.resolve(false);
296
- const changes = snapshot.handoffs
297
- .filter((handoff) => handoff.batchRevision === batchRevision
298
- && (localBatchId === undefined || handoff.localBatchId === localBatchId)
299
- && (handoff.state === "dispatching" || handoff.state === "handing-off"));
300
- if (changes.length === 0)
294
+ const projectId = activeProjectId;
295
+ const expectedGeneration = generation;
296
+ const hydration = initialization;
297
+ if (batchRevision === undefined || !projectId)
301
298
  return Promise.resolve(false);
302
- return updateHandoffs(changes, (handoff) => {
303
- if (state === "working") {
304
- return updateHandoff(handoff, {
305
- state: "handing-off",
306
- transport: "direct",
307
- ...(agentRequestId ? { agentRequestId } : {}),
308
- });
309
- }
299
+ return enqueue(async () => {
300
+ if (hydration)
301
+ await hydration;
302
+ assertWritable(projectId, expectedGeneration);
303
+ // Resolve the current revisions inside the write queue so a pending save
304
+ // or requeue cannot be removed by an older completion event.
305
+ const changes = snapshot.items.flatMap(({ document, handoff }) => {
306
+ if (!handoff || handoff.sketchRevision !== document.revision
307
+ || handoff.transport !== "direct" || handoff.batchRevision !== batchRevision)
308
+ return [];
309
+ // The client batch ID stays stable when the server assigns a request ID.
310
+ const matchesRequest = localBatchId !== undefined
311
+ ? handoff.localBatchId === localBatchId
312
+ : agentRequestId === undefined || handoff.agentRequestId === agentRequestId;
313
+ if (!matchesRequest)
314
+ return [];
315
+ return handoff.state === "dispatching" || handoff.state === "handing-off" || handoff.state === "unknown"
316
+ || (state === "completed" && handoff.state === "needs-review") ? [handoff] : [];
317
+ });
318
+ if (changes.length === 0)
319
+ return false;
310
320
  if (state === "completed") {
311
- return updateHandoff(handoff, {
312
- state: "needs-review",
313
- transport: "direct",
314
- ...(agentRequestId ? { agentRequestId } : {}),
315
- });
321
+ for (const handoff of changes) {
322
+ await deleteSketchPersistence(projectId, handoff.sketchId);
323
+ assertWritable(projectId, expectedGeneration);
324
+ publish(projectId, snapshot.documents.filter((document) => document.id !== handoff.sketchId), snapshot.handoffs.filter((candidate) => candidate.sketchId !== handoff.sketchId), null);
325
+ }
326
+ return true;
316
327
  }
317
- return updateHandoff(handoff, {
328
+ const nextHandoffs = changes.map((handoff) => updateHandoff(handoff, state === "working" ? {
329
+ state: "handing-off",
330
+ transport: "direct",
331
+ ...(agentRequestId ? { agentRequestId } : {}),
332
+ } : {
318
333
  state: "pending",
319
334
  transport: "direct",
320
335
  clearDelivery: true,
321
- });
336
+ }));
337
+ await writeSketchHandoffs(projectId, nextHandoffs);
338
+ assertWritable(projectId, expectedGeneration);
339
+ const changedIds = new Set(nextHandoffs.map((handoff) => handoff.id));
340
+ publish(projectId, snapshot.documents, [...snapshot.handoffs.filter((handoff) => !changedIds.has(handoff.id)), ...nextHandoffs], null);
341
+ return true;
322
342
  });
323
343
  }
324
344
  export function markSketchesHandedOff(entries, localBatchId) {
@@ -386,9 +406,14 @@ function updateHandoffs(handoffs, update) {
386
406
  const expectedGeneration = generation;
387
407
  if (!projectId)
388
408
  return Promise.resolve(false);
389
- const nextHandoffs = handoffs.map(update);
390
409
  return enqueue(async () => {
391
410
  assertWritable(projectId, expectedGeneration);
411
+ // Completion may delete a sketch before its dispatch response is handled.
412
+ const nextHandoffs = handoffs
413
+ .filter((handoff) => currentDocument(handoff.sketchId)?.revision === handoff.sketchRevision)
414
+ .map(update);
415
+ if (nextHandoffs.length === 0)
416
+ return false;
392
417
  await writeSketchHandoffs(projectId, nextHandoffs);
393
418
  assertWritable(projectId, expectedGeneration);
394
419
  const changedIds = new Set(nextHandoffs.map((handoff) => handoff.id));
@@ -35,10 +35,11 @@ export function SpacingField({ property, projection: spacingProjection, domEleme
35
35
  const sideSlots = SIDE_NAMES.map((side) => {
36
36
  const icon = property === "padding" ? _jsx(PaddingSideIndicator, { side: side }) : _jsx(MarginSideIndicator, { side: side });
37
37
  const sideName = sideProperty(property, side);
38
+ const field = spacingProjection.fields[side];
38
39
  return {
39
40
  side,
40
41
  icon: null,
41
- control: (_jsx(TokenField, { property: sideName, selection: selection, tokenRow: findTokenRow(tokenRows, sideName), domElement: el, editTarget: editTarget, entries: entries, suggestions: suggestions, editMetadata: metadataFor(findTokenRow(tokenRows, sideName)), onAfterEdit: onAfterEdit, chipVariant: "small", leading: icon })),
42
+ control: (_jsx(TokenField, { property: sideName, selection: selection, tokenRow: findTokenRow(tokenRows, sideName), domElement: el, editTarget: editTarget, entries: entries, suggestions: suggestions, displayValue: isCalcValue(field.authoredValue) && field.value ? field.value : undefined, editMetadata: metadataFor(findTokenRow(tokenRows, sideName)), onAfterEdit: onAfterEdit, chipVariant: "small", leading: icon })),
42
43
  };
43
44
  });
44
45
  const hasSpacing = SIDE_NAMES.some((side) => {
@@ -89,7 +90,8 @@ function PairedTokenField({ displayProperty, sideProperties, domElement: el, edi
89
90
  const expression = Boolean(row && (row.capability === "raw" || row.capability === "composite"
90
91
  || row.modifiers?.some((modifier) => modifier.kind === "alpha")));
91
92
  const calcAuthored = row?.authored ?? row?.declaredValue ?? "";
92
- const activeTokenName = !valuesMatch || expression || /\bcalc\s*\(/i.test(calcAuthored) ? null : row.tokenName;
93
+ const isCalcAuthored = isCalcValue(calcAuthored);
94
+ const activeTokenName = !valuesMatch || expression || isCalcAuthored ? null : row.tokenName;
93
95
  // Some browsers expose the used pixel size for horizontal auto margins.
94
96
  // Keep the authored keyword visible because replacing it with that transient
95
97
  // size would misrepresent the declaration and change the edit semantics.
@@ -98,7 +100,7 @@ function PairedTokenField({ displayProperty, sideProperties, domElement: el, edi
98
100
  : valuesMatch
99
101
  ? calcAuthored.trim().toLowerCase() === "auto"
100
102
  ? calcAuthored.trim()
101
- : expression ? row.authored || row.declaredValue || row.resolvedValue : row.resolvedValue
103
+ : isCalcAuthored ? row.resolvedValue : expression ? row.authored || row.declaredValue || row.resolvedValue : row.resolvedValue
102
104
  : axisProjection.fields.map(fieldDisplayValue).join(", ");
103
105
  const resolvedValue = valuesMatch
104
106
  ? row.resolvedValue
@@ -126,7 +128,10 @@ function PairedTokenField({ displayProperty, sideProperties, domElement: el, edi
126
128
  }, onUnlink: commitAxisValue, leading: leading }));
127
129
  }
128
130
  function fieldDisplayValue(field) {
129
- return field.authoredValue.trim() || field.value;
131
+ return isCalcValue(field.authoredValue) ? field.value : field.authoredValue.trim() || field.value;
132
+ }
133
+ function isCalcValue(value) {
134
+ return /\bcalc\s*\(/i.test(value);
130
135
  }
131
136
  function splitAxisValue(value) {
132
137
  let depth = 0;
@@ -7,10 +7,10 @@ export declare function nudgeCssValue(property: string, rawValue: string, direct
7
7
  /** Returns whether a style field exposes the horizontal drag nudge affordance. */
8
8
  export declare function supportsDragNudge(property: string): boolean;
9
9
  /**
10
- * Applies one nudge per two horizontal pointer pixels. Large nudges still use
11
- * one 8px step per pointer pixel. When a large drag starts from an off-grid
12
- * value, the first step snaps to the next large-step boundary in the drag
13
- * direction.
10
+ * Applies one regular nudge per four horizontal pointer pixels and one large
11
+ * nudge per two horizontal pointer pixels. When a large drag starts from an
12
+ * off-grid value, the first step snaps to the next large-step boundary in the
13
+ * drag direction.
14
14
  */
15
15
  export declare function nudgeCssValueByDrag(property: string, rawValue: string, deltaX: number, large?: boolean, snapToLargeStep?: boolean): string | null;
16
16
  /** Returns whether a field has a numeric value that can respond to dragging. */
@@ -2,6 +2,8 @@ import { completeCssValue, CSS_NUMBER_SOURCE } from "./completeCssValue.js";
2
2
  import { valuePolicyFor } from "./valuePolicy.js";
3
3
  const CSS_NUMBER = new RegExp(`^(?:${CSS_NUMBER_SOURCE})$`, "i");
4
4
  const CSS_NUMERIC_LITERAL = new RegExp(`^(${CSS_NUMBER_SOURCE})(px|rem|em|%)?$`, "i");
5
+ const DRAG_PIXELS_PER_STEP = 4;
6
+ const LARGE_DRAG_PIXELS_PER_STEP = 2;
5
7
  const DRAG_NUDGE_PROPERTY = /^(?:(?:padding|margin)-(?:top|right|bottom|left|horizontal|vertical)|(?:top|right|bottom|left|inset|inset-(?:horizontal|vertical))|(?:row|column)-gap|border-radius|border-(?:top-left|top-right|bottom-right|bottom-left)-radius)$/;
6
8
  /**
7
9
  * Returns a nudged CSS numeric literal, or null when the value is not a
@@ -23,13 +25,13 @@ export function supportsDragNudge(property) {
23
25
  return DRAG_NUDGE_PROPERTY.test(property);
24
26
  }
25
27
  /**
26
- * Applies one nudge per two horizontal pointer pixels. Large nudges still use
27
- * one 8px step per pointer pixel. When a large drag starts from an off-grid
28
- * value, the first step snaps to the next large-step boundary in the drag
29
- * direction.
28
+ * Applies one regular nudge per four horizontal pointer pixels and one large
29
+ * nudge per two horizontal pointer pixels. When a large drag starts from an
30
+ * off-grid value, the first step snaps to the next large-step boundary in the
31
+ * drag direction.
30
32
  */
31
33
  export function nudgeCssValueByDrag(property, rawValue, deltaX, large = false, snapToLargeStep = false) {
32
- const steps = Math.trunc(Math.abs(deltaX) / (large ? 1 : 2));
34
+ const steps = Math.trunc(Math.abs(deltaX) / (large ? LARGE_DRAG_PIXELS_PER_STEP : DRAG_PIXELS_PER_STEP));
33
35
  if (steps === 0)
34
36
  return null;
35
37
  const direction = deltaX < 0 ? -1 : 1;
@@ -52,8 +54,8 @@ export function nudgeCssValueByDrag(property, rawValue, deltaX, large = false, s
52
54
  }
53
55
  /** Returns whether a field has a numeric value that can respond to dragging. */
54
56
  export function canNudgeCssValueByDrag(property, rawValue) {
55
- return nudgeCssValueByDrag(property, rawValue, 2) !== null
56
- || nudgeCssValueByDrag(property, rawValue, -2) !== null;
57
+ return nudgeCssValueByDrag(property, rawValue, DRAG_PIXELS_PER_STEP) !== null
58
+ || nudgeCssValueByDrag(property, rawValue, -DRAG_PIXELS_PER_STEP) !== null;
57
59
  }
58
60
  /** Returns a clamped opacity percentage nudged by 1%, or 10% with Shift. */
59
61
  export function nudgeOpacityValue(rawValue, direction, large = false) {
@@ -13,6 +13,8 @@ import { ColorSwatch } from "../ui/ColorSwatch.js";
13
13
  import { getStateStyleValue } from "../shell/stateValue.js";
14
14
  import { AtRuleIndicator, useFieldAtRules } from "../ui/AtRuleContext.js";
15
15
  import { TokenChip } from "./TokenChip.js";
16
+ import { startDragPointerLock } from "./dragPointerLock.js";
17
+ import { createChangeHistoryGroup } from "../changes/workspaceChanges.js";
16
18
  import { isMultiTarget } from "../selection/editTarget.js";
17
19
  const NON_COLOR_FACTS = { hasEmbeddedAlpha: false, isExpression: false, opacityEditable: false };
18
20
  function colorFacts(value) {
@@ -318,8 +320,9 @@ export function TokenValueField(props) {
318
320
  const drag = dragNudgeRef.current;
319
321
  if (!drag)
320
322
  return;
321
- releasePointerCapture(drag);
322
323
  dragNudgeRef.current = null;
324
+ drag.pointerLock?.stop();
325
+ releasePointerCapture(drag);
323
326
  setIsDraggingNudge(false);
324
327
  }, []);
325
328
  useEffect(() => {
@@ -331,6 +334,8 @@ export function TokenValueField(props) {
331
334
  };
332
335
  }, [stopDragNudge]);
333
336
  function handleDragNudgePointerDown(event) {
337
+ if (event.button !== 0)
338
+ return;
334
339
  if (disabled || !supportsDragNudge(property))
335
340
  return;
336
341
  if (!canNudgeCssValueByDrag(property, rawValue))
@@ -338,6 +343,7 @@ export function TokenValueField(props) {
338
343
  stopDragNudge();
339
344
  const handle = event.currentTarget;
340
345
  const drag = {
346
+ commit: createChangeHistoryGroup(),
341
347
  handle,
342
348
  pointerId: event.pointerId,
343
349
  lastClientX: event.clientX,
@@ -350,6 +356,11 @@ export function TokenValueField(props) {
350
356
  dragNudgeRef.current = drag;
351
357
  if (typeof handle.setPointerCapture === "function")
352
358
  handle.setPointerCapture(event.pointerId);
359
+ if (event.pointerType === "mouse") {
360
+ drag.pointerLock = startDragPointerLock(handle, { x: event.clientX, y: event.clientY }, (mouseEvent) => {
361
+ updateDragNudge(mouseEvent, mouseEvent.movementX);
362
+ }, stopDragNudge);
363
+ }
353
364
  setIsDraggingNudge(true);
354
365
  event.preventDefault();
355
366
  event.stopPropagation();
@@ -358,8 +369,16 @@ export function TokenValueField(props) {
358
369
  const drag = dragNudgeRef.current;
359
370
  if (!drag || drag.pointerId !== event.pointerId)
360
371
  return;
372
+ if (drag.pointerLock?.isLocked())
373
+ return;
361
374
  const movementX = event.clientX - drag.lastClientX;
362
375
  drag.lastClientX = event.clientX;
376
+ updateDragNudge(event, movementX);
377
+ }
378
+ function updateDragNudge(event, movementX) {
379
+ const drag = dragNudgeRef.current;
380
+ if (!drag)
381
+ return;
363
382
  if (event.shiftKey !== drag.shiftActive) {
364
383
  drag.shiftActive = event.shiftKey;
365
384
  drag.startValue = drag.lastValue;
@@ -380,7 +399,7 @@ export function TokenValueField(props) {
380
399
  event.stopPropagation();
381
400
  setRawValue(next);
382
401
  setActiveTokenName(null);
383
- onCommitRaw(next);
402
+ drag.commit(() => onCommitRaw(next));
384
403
  }
385
404
  function handleDragNudgePointerEnd(event) {
386
405
  if (dragNudgeRef.current?.pointerId !== event.pointerId)