nudge-ui 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.mjs +16 -9
- package/dist/hosts/astro/clientTransport.js +3 -0
- package/dist/hosts/astro/integration.d.ts +2 -2
- package/dist/hosts/astro/integration.js +4 -3
- package/dist/hosts/environment.d.ts +15 -0
- package/dist/hosts/environment.js +16 -0
- package/dist/hosts/next/sidecar.js +7 -1
- package/dist/hosts/next/wrapper.d.ts +2 -0
- package/dist/hosts/next/wrapper.js +2 -1
- package/dist/hosts/projectBridge.d.ts +1 -0
- package/dist/hosts/projectBridge.js +1 -0
- package/dist/hosts/static/manifest.js +2 -0
- package/dist/hosts/static/server.js +1 -0
- package/dist/hosts/vite/vite.d.ts +1 -0
- package/dist/hosts/vite/vite.js +4 -1
- package/dist/inspector/agent/McpConnectionDialog.d.ts +3 -2
- package/dist/inspector/agent/McpConnectionDialog.js +11 -4
- package/dist/inspector/agent/client.d.ts +6 -0
- package/dist/inspector/agent/client.js +36 -4
- package/dist/inspector/agent/connectionStatus.d.ts +3 -3
- package/dist/inspector/agent/connectionStatus.js +8 -1
- package/dist/inspector/agent/httpTransport.d.ts +6 -0
- package/dist/inspector/agent/httpTransport.js +41 -1
- package/dist/inspector/agent/protocol.d.ts +3 -0
- package/dist/inspector/canvas/CanvasElementOverlay.js +49 -7
- package/dist/inspector/canvas/CanvasToolbar.js +4 -4
- package/dist/inspector/canvas/CanvasWorkspace.js +30 -11
- package/dist/inspector/canvas/frameProtocol.d.ts +16 -3
- package/dist/inspector/canvas/frameProtocol.js +9 -2
- package/dist/inspector/canvas/keyboardShortcuts.d.ts +2 -1
- package/dist/inspector/canvas/keyboardShortcuts.js +3 -1
- package/dist/inspector/canvas/linkEligibility.d.ts +0 -5
- package/dist/inspector/canvas/linkEligibility.js +0 -29
- package/dist/inspector/canvas/rendererBootstrap.js +13 -0
- package/dist/inspector/canvas/rendererElementSelector.d.ts +1 -1
- package/dist/inspector/canvas/rendererElementSelector.js +55 -39
- package/dist/inspector/changes/workspaceChanges.d.ts +4 -0
- package/dist/inspector/changes/workspaceChanges.js +29 -3
- package/dist/inspector/clientManifest.d.ts +2 -0
- package/dist/inspector/clientManifest.js +1 -0
- package/dist/inspector/index.js +6 -8
- package/dist/inspector/overlay/DomNavigator.d.ts +10 -0
- package/dist/inspector/overlay/DomNavigator.js +40 -0
- package/dist/inspector/overlay/InspectorOverlay.js +6 -2
- package/dist/inspector/overlay/clickPolicy.d.ts +18 -2
- package/dist/inspector/overlay/clickPolicy.js +34 -3
- package/dist/inspector/overlay/elementSelector.d.ts +1 -1
- package/dist/inspector/overlay/elementSelector.js +18 -7
- package/dist/inspector/overlay/spacingGestures.d.ts +3 -3
- package/dist/inspector/overlay/spacingGestures.js +15 -17
- package/dist/inspector/runtime/runtimeConfig.d.ts +1 -1
- package/dist/inspector/selection/hierarchy.d.ts +6 -0
- package/dist/inspector/selection/hierarchy.js +8 -0
- package/dist/inspector/settings/SettingsDialog.d.ts +1 -1
- package/dist/inspector/settings/SettingsDialog.js +2 -2
- package/dist/inspector/shell/ChangesLog.js +20 -20
- package/dist/inspector/shell/CopyPromptButton.js +9 -2
- package/dist/inspector/shell/InspectorShell.js +2 -5
- package/dist/inspector/sketch/store.js +48 -23
- package/dist/inspector/styleEditors/SpacingBox.js +9 -4
- package/dist/inspector/styleEditors/nudgeValue.d.ts +4 -4
- package/dist/inspector/styleEditors/nudgeValue.js +9 -7
- package/dist/inspector/tokens/TokenField.js +21 -2
- package/dist/inspector/tokens/TokensPanel.js +2 -1
- package/dist/inspector/tokens/dragPointerLock.d.ts +8 -0
- package/dist/inspector/tokens/dragPointerLock.js +70 -0
- package/dist/inspector/ui/Disclosure.d.ts +22 -0
- package/dist/inspector/ui/Disclosure.js +13 -0
- package/dist/inspector/ui/dragCursor.d.ts +8 -0
- package/dist/inspector/ui/dragCursor.js +21 -0
- package/dist/inspector/ui/styles.js +11 -9
- package/dist/nudge-ui.mjs +15 -1
- package/dist/transport/editor.d.ts +20 -1
- package/dist/transport/editor.js +42 -11
- package/dist/transport/routes.d.ts +4 -1
- package/dist/transport/routes.js +4 -1
- package/package.json +1 -1
- package/dist/inspector/shell/DomNavigation.d.ts +0 -8
- package/dist/inspector/shell/DomNavigation.js +0 -30
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getElementComputedStyle } from "../runtime/domRealm.js";
|
|
2
|
-
const SPACING_DRAG_TARGET_SIZE =
|
|
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,
|
|
91
|
+
function paddingAffordanceForSide(element, side, model = boxModel(element)) {
|
|
91
92
|
const { inner, content, padding } = model;
|
|
92
93
|
const value = padding[side];
|
|
93
|
-
if (
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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: [
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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,
|
|
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: [
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
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
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
11
|
-
*
|
|
12
|
-
* value, the first step snaps to the next large-step boundary in the
|
|
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
|
|
27
|
-
*
|
|
28
|
-
* value, the first step snaps to the next large-step boundary in the
|
|
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 ?
|
|
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,
|
|
56
|
-
|| nudgeCssValueByDrag(property, rawValue, -
|
|
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)
|