nudge-ui 0.2.3 → 0.2.4
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 +17 -9
- package/dist/inspector/agent/verification.d.ts +4 -6
- package/dist/inspector/agent/verification.js +60 -16
- package/dist/inspector/canvas/CanvasCard.js +2 -0
- package/dist/inspector/canvas/CanvasElementOverlay.js +17 -3
- package/dist/inspector/canvas/CanvasWorkspace.js +46 -66
- package/dist/inspector/canvas/frameScrollbarStyles.d.ts +3 -0
- package/dist/inspector/canvas/frameScrollbarStyles.js +21 -0
- package/dist/inspector/changes/changesLog.d.ts +3 -1
- package/dist/inspector/changes/changesLog.js +8 -0
- package/dist/inspector/changes/codecs.d.ts +1 -0
- package/dist/inspector/changes/codecs.js +10 -1
- package/dist/inspector/changes/draftChanges.d.ts +2 -0
- package/dist/inspector/changes/draftChanges.js +4 -0
- package/dist/inspector/changes/editModel.d.ts +10 -0
- package/dist/inspector/changes/editModel.js +9 -2
- package/dist/inspector/changes/types.d.ts +7 -0
- package/dist/inspector/comments/element.js +10 -3
- package/dist/inspector/componentSemantics/adapterRegistry.d.ts +6 -1
- package/dist/inspector/componentSemantics/adapterRegistry.js +23 -1
- package/dist/inspector/componentSemantics/reactRuntime.d.ts +2 -1
- package/dist/inspector/componentSemantics/reactRuntime.js +38 -0
- package/dist/inspector/componentSemantics/runtimeBridge.d.ts +4 -2
- package/dist/inspector/componentSemantics/runtimeBridge.js +17 -0
- package/dist/inspector/componentSemantics/types.d.ts +11 -0
- package/dist/inspector/iterations/capture.js +2 -1
- package/dist/inspector/projection/renderedInstance.d.ts +15 -2
- package/dist/inspector/projection/renderedInstance.js +53 -6
- package/dist/inspector/projection/structuralProjection.js +2 -2
- package/dist/inspector/projection/textProjection.js +56 -11
- package/dist/inspector/prompt/clipboardHandoff.d.ts +5 -0
- package/dist/inspector/prompt/clipboardHandoff.js +5 -0
- package/dist/inspector/prompt/generatePrompt.js +5 -1
- package/dist/inspector/selection/sourceSite.d.ts +12 -0
- package/dist/inspector/selection/sourceSite.js +28 -0
- package/dist/inspector/shell/CopyPromptButton.js +49 -7
- package/dist/inspector/styleEditors/BackgroundEditor.js +1 -1
- package/dist/inspector/tokens/TokenField.js +6 -4
- package/dist/inspector/tokens/editActions.js +6 -0
- package/dist/inspector/ui/styles.js +1 -1
- package/dist/inspector/workspace/sentItems.d.ts +19 -0
- package/dist/inspector/workspace/sentItems.js +51 -0
- package/package.json +2 -2
|
@@ -13,12 +13,6 @@ export interface HandoffSnapshot {
|
|
|
13
13
|
readonly changes: readonly ChangeRecord[];
|
|
14
14
|
readonly structuralChanges: readonly StructuralChange[];
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
* User-facing agent completion state, named once.
|
|
18
|
-
* "completed" means the agent finished; "verified" means completed work was
|
|
19
|
-
* positively reconciled against source-rendered output.
|
|
20
|
-
*/
|
|
21
|
-
export type AgentCompletionStatus = "completed" | "verified";
|
|
22
16
|
/** Stable, preview-metadata-free identity for one prompt handoff record. */
|
|
23
17
|
export declare function handoffChangeFingerprint(change: ChangeRecord): string;
|
|
24
18
|
/** Stable identity for one structural record included in a prompt handoff. */
|
|
@@ -43,6 +37,10 @@ export declare function getAgentDispatchSize(revision: number): number;
|
|
|
43
37
|
*/
|
|
44
38
|
export declare function verifyAndReconcileHandoff(snapshot: HandoffSnapshot, doc?: Document | null): Promise<number>;
|
|
45
39
|
export declare function documentBelongsToHandoff(doc: Document, owner: HandoffOwner): boolean;
|
|
40
|
+
/** Returns the records captured for one dispatch until it is reconciled. */
|
|
41
|
+
export declare function getAgentDispatch(revision: number): HandoffSnapshot | null;
|
|
42
|
+
/** Records in the owner draft that still equal what a handoff sent. */
|
|
43
|
+
export declare function unchangedHandoffRecords(snapshot: HandoffSnapshot): Pick<HandoffSnapshot, "changes" | "structuralChanges">;
|
|
46
44
|
/** Reconciles the captured records for one completed connected-agent request. */
|
|
47
45
|
export declare function verifyAndReconcileAgentDispatch(revision: number): Promise<number>;
|
|
48
46
|
/** Test/session reset for in-memory dispatch snapshots. */
|
|
@@ -7,7 +7,9 @@ import { selectorForManagedChange } from "../changes/managedStyleProjection.js";
|
|
|
7
7
|
import { getActiveDraftChanges, getDraftChanges, reconcileDraftChanges } from "../changes/draftChanges.js";
|
|
8
8
|
import { isComponentChange, isTextContentChange, isTokenChange, } from "../changes/changesLog.js";
|
|
9
9
|
import { verifyPreview } from "../projection/managedStylesheet.js";
|
|
10
|
-
import {
|
|
10
|
+
import { resolveMovedRenderedInstance } from "../projection/renderedInstance.js";
|
|
11
|
+
import { relocateSourceSite, sourceSiteSelector } from "../selection/sourceSite.js";
|
|
12
|
+
import { boundRuntimeEvidence, normalizeRuntimeTag } from "../runtime/staticHtmlRuntimeIdentity.js";
|
|
11
13
|
import { documentStateKey, } from "../projection/structuralProjection.js";
|
|
12
14
|
import { resolveTextProjectionTarget, resolveTextProjectionTextNode, } from "../projection/textProjection.js";
|
|
13
15
|
import { applyHostDraftProjection, compileDraftProjection, } from "../projection/draftProjection.js";
|
|
@@ -48,32 +50,60 @@ function requestedStyleValue(change) {
|
|
|
48
50
|
return tokenReference(change.newToken);
|
|
49
51
|
return change.rawValue ?? "";
|
|
50
52
|
}
|
|
51
|
-
function
|
|
52
|
-
if (!isTokenChange(change) && (change.state ?? "base") !== "base")
|
|
53
|
-
return false;
|
|
54
|
-
const selector = selectorForManagedChange(change);
|
|
53
|
+
function queryTargets(doc, selector) {
|
|
55
54
|
if (!selector)
|
|
56
|
-
return
|
|
57
|
-
let targets;
|
|
55
|
+
return [];
|
|
58
56
|
try {
|
|
59
|
-
|
|
57
|
+
return Array.from(doc.querySelectorAll(selector));
|
|
60
58
|
}
|
|
61
59
|
catch {
|
|
62
|
-
return
|
|
60
|
+
return [];
|
|
63
61
|
}
|
|
62
|
+
}
|
|
63
|
+
/** Resolves current style targets, following a source site whose position shifted. */
|
|
64
|
+
function styleTargets(change, doc) {
|
|
65
|
+
if (isTokenChange(change))
|
|
66
|
+
return queryTargets(doc, change.selector);
|
|
67
|
+
// The projection marker is lifted for verification; evidence names the instance.
|
|
68
|
+
if (change.scope === "rendered-instance") {
|
|
69
|
+
const resolved = change.instanceOverride
|
|
70
|
+
? resolveMovedRenderedInstance(doc, change.instanceOverride.target)
|
|
71
|
+
: null;
|
|
72
|
+
return resolved?.status === "resolved" ? [resolved.element] : [];
|
|
73
|
+
}
|
|
74
|
+
const targets = queryTargets(doc, selectorForManagedChange(change));
|
|
75
|
+
const evidence = change.sourceEvidence;
|
|
76
|
+
if (targets.length > 0 || !evidence || change.column === undefined)
|
|
77
|
+
return targets;
|
|
78
|
+
const site = relocateSourceSite(doc, { cid: change.cid, src: `${change.file}:${change.line}:${change.column}` }, (element) => normalizeRuntimeTag(element.tagName) === evidence.tagName
|
|
79
|
+
&& boundRuntimeEvidence(element.getAttribute("data-cprops")) === evidence.props);
|
|
80
|
+
return site ? queryTargets(doc, sourceSiteSelector(site.cid, site.src)) : [];
|
|
81
|
+
}
|
|
82
|
+
function verifyStyle(change, doc) {
|
|
83
|
+
if (!isTokenChange(change) && (change.state ?? "base") !== "base")
|
|
84
|
+
return false;
|
|
85
|
+
const targets = styleTargets(change, doc);
|
|
64
86
|
const requested = requestedStyleValue(change);
|
|
65
87
|
return requested.length > 0
|
|
66
88
|
&& targets.length > 0
|
|
67
89
|
&& targets.every((target) => verifyPreview(target, change.property, requested).status === "applied");
|
|
68
90
|
}
|
|
69
91
|
function verifyText(change, doc) {
|
|
70
|
-
|
|
92
|
+
let target = change.target;
|
|
93
|
+
let resolved = resolveTextProjectionTarget(doc, target, change.after);
|
|
94
|
+
if (resolved.status === "missing") {
|
|
95
|
+
const sourceSite = relocateSourceSite(doc, target.sourceSite, (element) => element.getAttribute("data-cprops") === target.props && element.getAttribute("aria-label") === target.ariaLabel);
|
|
96
|
+
if (sourceSite) {
|
|
97
|
+
target = { ...target, sourceSite };
|
|
98
|
+
resolved = resolveTextProjectionTarget(doc, target, change.after);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
71
101
|
if (resolved.status !== "resolved")
|
|
72
102
|
return false;
|
|
73
|
-
const node = resolveTextProjectionTextNode(resolved.element,
|
|
103
|
+
const node = resolveTextProjectionTextNode(resolved.element, target, change.after);
|
|
74
104
|
if (node)
|
|
75
105
|
return node.nodeValue === change.after;
|
|
76
|
-
return
|
|
106
|
+
return target.textNodePath === undefined
|
|
77
107
|
&& change.after.length === 0
|
|
78
108
|
&& (resolved.element.textContent ?? "") === "";
|
|
79
109
|
}
|
|
@@ -97,7 +127,7 @@ function verifyStructuralDelete(change, doc) {
|
|
|
97
127
|
}
|
|
98
128
|
return currentRoute === change.route
|
|
99
129
|
&& documentStateKey(doc) === change.state
|
|
100
|
-
&&
|
|
130
|
+
&& resolveMovedRenderedInstance(doc, change.target).status === "missing";
|
|
101
131
|
}
|
|
102
132
|
/**
|
|
103
133
|
* A move verifies when the element sits at its recorded destination without
|
|
@@ -106,17 +136,17 @@ function verifyStructuralDelete(change, doc) {
|
|
|
106
136
|
* stay unverified instead of trusting rendered placement alone.
|
|
107
137
|
*/
|
|
108
138
|
function verifyStructuralMove(change, doc) {
|
|
109
|
-
const target =
|
|
139
|
+
const target = resolveMovedRenderedInstance(doc, change.target);
|
|
110
140
|
if (target.status !== "resolved")
|
|
111
141
|
return false;
|
|
112
|
-
const parent =
|
|
142
|
+
const parent = resolveMovedRenderedInstance(doc, change.destination.parent);
|
|
113
143
|
if (parent.status !== "resolved")
|
|
114
144
|
return false;
|
|
115
145
|
if (target.element.parentElement !== parent.element)
|
|
116
146
|
return false;
|
|
117
147
|
if (!change.destination.before)
|
|
118
148
|
return target.element.nextElementSibling === null;
|
|
119
|
-
const before =
|
|
149
|
+
const before = resolveMovedRenderedInstance(doc, change.destination.before);
|
|
120
150
|
return before.status === "resolved" && target.element.nextElementSibling === before.element;
|
|
121
151
|
}
|
|
122
152
|
function verifiedStructuralChangeIds(eligible, doc) {
|
|
@@ -278,6 +308,20 @@ function handoffDocument(owner) {
|
|
|
278
308
|
}
|
|
279
309
|
return owner.target.kind === "application" && !isEditorShellDocument() ? document : null;
|
|
280
310
|
}
|
|
311
|
+
/** Returns the records captured for one dispatch until it is reconciled. */
|
|
312
|
+
export function getAgentDispatch(revision) {
|
|
313
|
+
return dispatches.get(revision) ?? null;
|
|
314
|
+
}
|
|
315
|
+
/** Records in the owner draft that still equal what a handoff sent. */
|
|
316
|
+
export function unchangedHandoffRecords(snapshot) {
|
|
317
|
+
const sent = new Set(snapshot.changes.map(handoffChangeFingerprint));
|
|
318
|
+
const sentStructural = new Set(snapshot.structuralChanges.map(handoffStructuralFingerprint));
|
|
319
|
+
const current = getDraftChanges(snapshot.owner.draftId);
|
|
320
|
+
return {
|
|
321
|
+
changes: current.changes.filter((change) => sent.has(handoffChangeFingerprint(change))),
|
|
322
|
+
structuralChanges: current.structuralChanges.filter((change) => sentStructural.has(handoffStructuralFingerprint(change))),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
281
325
|
/** Reconciles the captured records for one completed connected-agent request. */
|
|
282
326
|
export async function verifyAndReconcileAgentDispatch(revision) {
|
|
283
327
|
const snapshot = dispatches.get(revision);
|
|
@@ -27,6 +27,7 @@ import { useNudgeUiRuntimeConfig } from "../runtime/useRuntimeConfig.js";
|
|
|
27
27
|
import { reconcileRuntimeWithDocumentStylesheets } from "../runtime/documentStylesheetOrder.js";
|
|
28
28
|
import { subscribeCanvasRendererMessages } from "./rendererMessageRouter.js";
|
|
29
29
|
import { SketchFrameOverlay } from "../sketch/SketchFrameOverlay.js";
|
|
30
|
+
import { installFrameScrollbarStyles } from "./frameScrollbarStyles.js";
|
|
30
31
|
import { getCanvasResizeEdges, resizeCanvasRect, } from "./canvasResize.js";
|
|
31
32
|
import { getDraftContentsForCard } from "../drafts/store.js";
|
|
32
33
|
import { removeFrame } from "../workspace/commands.js";
|
|
@@ -100,6 +101,7 @@ export function CanvasCard({ card, presentation = "canvas", presentationCard = t
|
|
|
100
101
|
session.registerCleanup(() => releaseDocumentProjection(frameDocument));
|
|
101
102
|
session.registerCleanup(() => disposeInlineTextEdit("frame-disposed", frameDocument));
|
|
102
103
|
session.registerCleanup(startClipboardHandoffController(frameDocument));
|
|
104
|
+
session.registerCleanup(installFrameScrollbarStyles(frameDocument));
|
|
103
105
|
documentSessionRef.current = { document: frameDocument, session };
|
|
104
106
|
}, [disposeDocumentSession, documentOwner]);
|
|
105
107
|
useEffect(() => {
|
|
@@ -383,6 +383,9 @@ export function CanvasElementOverlay() {
|
|
|
383
383
|
borders,
|
|
384
384
|
spacingGuides: affordance?.affectedGuides ?? [],
|
|
385
385
|
spacingAreas: affordance?.affectedAreas ?? [],
|
|
386
|
+
spacingHandleBadge: current.spacingHandleBadge && affordance
|
|
387
|
+
? { ...current.spacingHandleBadge, value: affordance.value }
|
|
388
|
+
: current.spacingHandleBadge,
|
|
386
389
|
};
|
|
387
390
|
});
|
|
388
391
|
};
|
|
@@ -421,6 +424,14 @@ export function CanvasElementOverlay() {
|
|
|
421
424
|
: null;
|
|
422
425
|
const spacingGuides = matchingAffordance?.affectedGuides ?? [];
|
|
423
426
|
const spacingAreas = matchingAffordance?.affectedAreas ?? [];
|
|
427
|
+
const handleAffordance = msg.spacing && hoverElement
|
|
428
|
+
? getSpacingAffordanceForDescriptor(hoverElement, msg.spacing)
|
|
429
|
+
: null;
|
|
430
|
+
const spacingHandleBadge = hoverPoint
|
|
431
|
+
&& handleAffordance
|
|
432
|
+
&& spacingDescriptorMatches(handleAffordance, msg.spacing)
|
|
433
|
+
? { point: hoverPoint, value: handleAffordance.value }
|
|
434
|
+
: null;
|
|
424
435
|
setHover({
|
|
425
436
|
iframe: sourceIframe,
|
|
426
437
|
identity: { elementId: msg.elementId },
|
|
@@ -430,6 +441,7 @@ export function CanvasElementOverlay() {
|
|
|
430
441
|
spacing: spacingGuides.length > 0 ? spacingDescriptor : null,
|
|
431
442
|
spacingGuides,
|
|
432
443
|
spacingAreas,
|
|
444
|
+
spacingHandleBadge,
|
|
433
445
|
});
|
|
434
446
|
}
|
|
435
447
|
else if (data.type === "element-measure-state") {
|
|
@@ -638,8 +650,10 @@ export function CanvasElementOverlay() {
|
|
|
638
650
|
const projectedHoverRect = hover ? projectRect(hover.iframe, hover.rect, projectionZoom) : null;
|
|
639
651
|
const projectedSpacingGuides = hover?.spacingGuides.map((guide) => projectRect(hover.iframe, guide, projectionZoom)) ?? [];
|
|
640
652
|
const projectedSpacingAreas = hover?.spacingAreas.map((area) => projectRect(hover.iframe, area, projectionZoom)) ?? [];
|
|
641
|
-
const
|
|
642
|
-
?
|
|
653
|
+
const spacingBadge = spacingDragBadge
|
|
654
|
+
?? (hover?.spacingHandleBadge ? { iframe: hover.iframe, ...hover.spacingHandleBadge } : null);
|
|
655
|
+
const projectedSpacingBadge = spacingBadge
|
|
656
|
+
? projectPoint(spacingBadge.iframe, spacingBadge.point.x, spacingBadge.point.y, projectionZoom)
|
|
643
657
|
: null;
|
|
644
658
|
const hoverSpacing = hover?.spacing ?? null;
|
|
645
659
|
const measureStateForSelectedFrame = measureState?.iframe === selectedFrame ? measureState : null;
|
|
@@ -667,7 +681,7 @@ export function CanvasElementOverlay() {
|
|
|
667
681
|
: [];
|
|
668
682
|
if (presentationTransitioning || layoutTransitioning || temporaryAppInteraction)
|
|
669
683
|
return _jsx("style", { "data-test": "canvas-element-overlay-styles", children: overlayStyles });
|
|
670
|
-
return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "canvas-element-overlay-styles", children: overlayStyles }), showGuideOverlay && selectedRect && selectedFrameRect ? (_jsx(MeasurementGuideOverlay, { testId: "canvas-measurement-overlay", selectedRect: selectedRect, guideViewport: selectedFrameRect, segments: measurementSegments })) : null, projectedHoverRect ? (_jsxs(_Fragment, { children: [hoverSpacing ? projectedSpacingAreas.map((area, index) => (_jsx("div", { className: "canvas-spacing-fill", "data-test": "canvas-spacing-fill", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-area-index": index, style: overlayStyle(area), "aria-hidden": "true" }, `${hoverSpacing.property}-area-${index}`))) : null, showHoverOutline ? (_jsx("div", { className: "canvas-element-overlay", "data-test": "canvas-hover-outline", style: overlayStyle(projectedHoverRect), "aria-hidden": "true" })) : null, hoverSpacing ? projectedSpacingGuides.map((guide, index) => (_jsx("div", { className: "canvas-spacing-guide", "data-test": "canvas-spacing-guide", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-guide-index": index, style: spacingGuideStyle(guide), "aria-hidden": "true" }, `${hoverSpacing.property}-${index}`))) : null] })) : null, projectedSelectedGeometry.map(({ element, rect }, index) => (_jsx("div", { className: "canvas-selected-outline", "data-test": "canvas-selected-outline", "data-selected-cid": element.cid, "data-selected-index": index, "data-selected-src": element.src, style: overlayStyle(rect), "aria-hidden": "true" }, `${element.cid}-${index}`))), projectedSpacingBadge &&
|
|
684
|
+
return (_jsxs(_Fragment, { children: [_jsx("style", { "data-test": "canvas-element-overlay-styles", children: overlayStyles }), showGuideOverlay && selectedRect && selectedFrameRect ? (_jsx(MeasurementGuideOverlay, { testId: "canvas-measurement-overlay", selectedRect: selectedRect, guideViewport: selectedFrameRect, segments: measurementSegments })) : null, projectedHoverRect ? (_jsxs(_Fragment, { children: [hoverSpacing ? projectedSpacingAreas.map((area, index) => (_jsx("div", { className: "canvas-spacing-fill", "data-test": "canvas-spacing-fill", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-area-index": index, style: overlayStyle(area), "aria-hidden": "true" }, `${hoverSpacing.property}-area-${index}`))) : null, showHoverOutline ? (_jsx("div", { className: "canvas-element-overlay", "data-test": "canvas-hover-outline", style: overlayStyle(projectedHoverRect), "aria-hidden": "true" })) : null, hoverSpacing ? projectedSpacingGuides.map((guide, index) => (_jsx("div", { className: "canvas-spacing-guide", "data-test": "canvas-spacing-guide", "data-kind": hoverSpacing.kind, "data-property": hoverSpacing.property, "data-side": hoverSpacing.side ?? undefined, "data-guide-index": index, style: spacingGuideStyle(guide), "aria-hidden": "true" }, `${hoverSpacing.property}-${index}`))) : null] })) : null, projectedSelectedGeometry.map(({ element, rect }, index) => (_jsx("div", { className: "canvas-selected-outline", "data-test": "canvas-selected-outline", "data-selected-cid": element.cid, "data-selected-index": index, "data-selected-src": element.src, style: overlayStyle(rect), "aria-hidden": "true" }, `${element.cid}-${index}`))), projectedSpacingBadge && spacingBadge ? (_jsx("div", { className: "canvas-spacing-value-badge", "data-test": "canvas-spacing-value-badge", style: { left: projectedSpacingBadge.x, top: projectedSpacingBadge.y }, "aria-hidden": "true", children: spacingValueCss(spacingBadge.value) })) : null, domNavigationEnabled && !inlineTextSession && selectedElements.length === 1 && selected && selectedRect && selectedFrame instanceof HTMLIFrameElement ? (_jsx(DomNavigator, { selected: selected, hierarchy: hierarchy, anchor: selectedRect, project: (element) => element.ownerDocument === selected.domElement.ownerDocument
|
|
671
685
|
? projectRect(selectedFrame, toRect(element.getBoundingClientRect()), projectionZoom)
|
|
672
686
|
: null })) : null, _jsx(DropGuideOverlay, { guide: projectedDropGuide, lineClassName: "canvas-dom-drop-line", lineTestId: "canvas-dom-drop-line", targetClassName: "canvas-dom-drop-target", targetTestId: "canvas-dom-drop-target" })] }));
|
|
673
687
|
}
|
|
@@ -27,7 +27,7 @@ import { disposeInlineTextEdit } from "../inline-text/inlineTextEditor.js";
|
|
|
27
27
|
import { PROTOCOL_VERSION, isProjectionAppliedMessage, isKeyboardShortcutMessage, isRenderedInstanceProjectionReportMessage, isTextProjectionReportMessage, isStructuralProjectionReportMessage, } from "./frameProtocol.js";
|
|
28
28
|
import { wheelPanDelta } from "./wheelPan.js";
|
|
29
29
|
import { iframePointToClientPoint, zoomCameraAtPointer } from "./canvasGestures.js";
|
|
30
|
-
const canvasWorkspaceStyles = ".canvas-workspace {\n position: fixed;\n inset: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface-panel);\n pointer-events: auto;\n}\n\n.canvas-workspace__board {\n flex: 1;\n overflow: hidden;\n position: relative;\n background: var(--surface-panel);\n}\n\n.canvas-workspace__focus-action {\n position: absolute;\n top: max(12px, env(safe-area-inset-top));\n left: max(12px, env(safe-area-inset-left));\n z-index: calc(var(--z-index) - 1);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--space-4);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n pointer-events: auto;\n}\n\n.canvas-workspace__board.is-grabbable {\n cursor: grab;\n}\n\n.canvas-workspace__board.is-grabbing {\n cursor: grabbing;\n}\n\n/* Canvas gestures take precedence over every card interaction while Space is\n held. This also closes the short postMessage gap before an iframe receives\n its mirrored modifier state. */\n.canvas-workspace__board.is-grabbable .canvas-card,\n.canvas-workspace__board.is-grabbing .canvas-card {\n pointer-events: none;\n}\n\n.canvas-workspace__board-content {\n position: absolute;\n width: 100%;\n height: 100%;\n /*\n * Leave `will-change` unset so the browser can re-rasterize this subtree at\n * the current camera scale. A permanent `will-change: transform` promotes\n * it to a fixed bitmap, which makes iframe content look soft when zoomed.\n */\n}\n\n.canvas-workspace__board.is-grabbing .canvas-workspace__board-content {\n /* Panning benefits from a cached composited layer; zoom restores the crisp\n * default path as soon as the gesture ends. */\n will-change: transform;\n}\n\n.canvas-workspace
|
|
30
|
+
const canvasWorkspaceStyles = ".canvas-workspace {\n position: fixed;\n inset: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface-panel);\n pointer-events: auto;\n}\n\n.canvas-workspace__board {\n flex: 1;\n overflow: hidden;\n position: relative;\n background: var(--surface-panel);\n}\n\n.canvas-workspace__focus-action {\n position: absolute;\n top: max(12px, env(safe-area-inset-top));\n left: max(12px, env(safe-area-inset-left));\n z-index: calc(var(--z-index) - 1);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--space-4);\n border-radius: var(--radius-12);\n background: var(--surface-raised-2x);\n box-shadow: var(--shadow-popup);\n pointer-events: auto;\n}\n\n.canvas-workspace__board.is-grabbable {\n cursor: grab;\n}\n\n.canvas-workspace__board.is-grabbing {\n cursor: grabbing;\n}\n\n/* Canvas gestures take precedence over every card interaction while Space is\n held. This also closes the short postMessage gap before an iframe receives\n its mirrored modifier state. */\n.canvas-workspace__board.is-grabbable .canvas-card,\n.canvas-workspace__board.is-grabbing .canvas-card {\n pointer-events: none;\n}\n\n.canvas-workspace__board-content {\n position: absolute;\n width: 100%;\n height: 100%;\n /*\n * Leave `will-change` unset so the browser can re-rasterize this subtree at\n * the current camera scale. A permanent `will-change: transform` promotes\n * it to a fixed bitmap, which makes iframe content look soft when zoomed.\n */\n}\n\n.canvas-workspace__board.is-grabbing .canvas-workspace__board-content {\n /* Panning benefits from a cached composited layer; zoom restores the crisp\n * default path as soon as the gesture ends. */\n will-change: transform;\n}\n\n.canvas-workspace[data-presentation=\"focus\"] .canvas-workspace__board-content {\n inset: 0;\n transform: none !important;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-card:not(.is-presentation-card) {\n visibility: hidden !important;\n pointer-events: none !important;\n}\n\n.canvas-workspace__target-error {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n color: #6b7280;\n font: 500 14px/1.4 system-ui, sans-serif;\n}\n\n/* Moving iframes must not acquire hover state beneath a stationary pointer. */\n.canvas-workspace.is-presentation-transitioning .canvas-card__iframe,\n.canvas-workspace__board.is-layout-transitioning .canvas-card__iframe {\n pointer-events: none;\n}\n";
|
|
31
31
|
const canvasCardStyles = ".canvas-card {\n position: relative;\n}\n\n/* Extend the card's hover region to the toolbar above the iframe. */\n.canvas-card::before {\n content: \"\";\n position: absolute;\n right: 0;\n bottom: 100%;\n left: 0;\n height: calc((var(--control-height-large) + var(--space-16)) * var(--canvas-toolbar-scale, 1));\n pointer-events: auto;\n}\n\n.canvas-card__frame {\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--surface-panel);\n border-radius: 0;\n overflow: hidden;\n}\n\n.canvas-card.is-selected .canvas-card__frame {\n outline: 2px solid var(--frame-accent, var(--accent));\n outline-offset: -2px;\n}\n\n.canvas-card.is-focus-presentation .canvas-card__frame {\n outline: none;\n}\n\n.canvas-card.is-focused.is-selected:not(.is-linked) .canvas-card__frame {\n animation: canvas-card-focus 900ms ease-out;\n}\n\n@keyframes canvas-card-focus {\n 0%, 35% { outline: 3px solid var(--frame-accent, var(--accent)); outline-offset: -3px; }\n 100% { outline: 0 solid transparent; outline-offset: -2px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .canvas-card.is-focused .canvas-card__frame {\n animation: none;\n }\n}\n\n.canvas-card__header {\n display: flex;\n align-items: center;\n position: absolute;\n left: 0;\n right: 0;\n bottom: calc(100% + var(--space-8));\n justify-content: flex-end;\n z-index: 3;\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n}\n\n.canvas-card__drag-surface {\n position: relative;\n display: flex;\n align-items: center;\n gap: var(--space-8);\n flex: 0 1 auto;\n margin-right: auto;\n min-width: 0;\n min-height: var(--control-height-large);\n padding: 0;\n cursor: grab;\n user-select: none;\n touch-action: none;\n}\n\n.canvas-card.is-dragging .canvas-card__drag-surface {\n cursor: grabbing;\n}\n\n.canvas-card__identity {\n display: flex;\n align-items: center;\n gap: var(--space-8);\n min-width: 0;\n}\n\n.canvas-card__dimensions {\n display: flex;\n align-items: center;\n gap: var(--space-8);\n position: relative;\n flex: 0 1 auto;\n min-width: 0;\n max-width: 260px;\n color: var(--text-secondary);\n font-size: var(--font-size-base);\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n pointer-events: none;\n}\n\n.canvas-card__title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.canvas-card__live-badge,\n.canvas-card__iteration-badge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 20px;\n height: 20px;\n background: var(--accent-surface);\n color: var(--accent);\n border-radius: var(--radius-4);\n pointer-events: auto;\n}\n\n.canvas-card__floating-toolbar {\n position: absolute;\n left: 50%;\n flex-shrink: 0;\n width: max-content;\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transform: translate(-50%, calc(-24px + var(--space-4)));\n transition: opacity 140ms ease-out, transform 140ms ease-out, visibility 140ms;\n}\n\n.canvas-card:hover .canvas-card__floating-toolbar,\n.canvas-card:focus-within .canvas-card__floating-toolbar,\n.canvas-card__floating-toolbar[data-busy=\"true\"] {\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n transform: translate(-50%, -24px);\n}\n\n.canvas-card__actions.canvas-toolbar {\n position: static;\n width: max-content;\n transform: none;\n white-space: nowrap;\n}\n\n.canvas-card__actions .canvas-toolbar__divider {\n margin-inline: 0;\n}\n\n@media (hover: none) {\n .canvas-card.is-selected .canvas-card__floating-toolbar {\n opacity: 1;\n visibility: visible;\n pointer-events: auto;\n transform: translate(-50%, -24px);\n }\n}\n\n.canvas-card.is-dragging .canvas-card__floating-toolbar,\n.canvas-workspace__board.is-grabbable .canvas-card__floating-toolbar,\n.canvas-workspace__board.is-grabbing .canvas-card__floating-toolbar,\n.canvas-workspace.is-presentation-transitioning .canvas-card__floating-toolbar,\n.canvas-workspace__board.is-layout-transitioning .canvas-card__floating-toolbar {\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n transition: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .canvas-card__floating-toolbar { transition: none; }\n}\n\n.canvas-card__iteration-error {\n position: absolute;\n right: 0;\n bottom: calc(100% + var(--space-8));\n color: var(--text-danger, #b42318);\n font-size: 12px;\n white-space: nowrap;\n}\n\n.canvas-card__iframe {\n width: 100%;\n height: 100%;\n border: none;\n display: block;\n}\n\n.canvas-card__sketch-overlay {\n position: absolute;\n inset: 0;\n z-index: 2;\n display: block;\n pointer-events: none;\n}\n\n.canvas-card__loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n color: var(--text-muted, #777777);\n font-size: 12px;\n font-weight: 500;\n}\n\n.canvas-card__error {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n height: 100%;\n padding: 20px;\n text-align: center;\n}\n\n.canvas-card__error p {\n margin: 0;\n color: var(--text-muted, #777777);\n font-size: 12px;\n font-weight: 500;\n}\n\n.canvas-card__error-actions {\n display: flex;\n gap: 8px;\n}\n\n.canvas-card__resize-handle {\n position: absolute;\n z-index: 3;\n display: flex;\n align-items: flex-end;\n justify-content: flex-end;\n padding: 4px;\n color: var(--text-muted, #777777);\n opacity: 0;\n transition: opacity 150ms ease;\n touch-action: none;\n user-select: none;\n}\n\n.canvas-card__resize-handle--top,\n.canvas-card__resize-handle--bottom {\n right: 16px;\n left: 16px;\n height: 16px;\n cursor: ns-resize;\n}\n\n.canvas-card__resize-handle--top {\n top: 0;\n}\n\n.canvas-card__resize-handle--bottom {\n bottom: 0;\n}\n\n.canvas-card__resize-handle--left,\n.canvas-card__resize-handle--right {\n top: 16px;\n bottom: 16px;\n width: 16px;\n cursor: ew-resize;\n}\n\n.canvas-card__resize-handle--left {\n left: 0;\n}\n\n.canvas-card__resize-handle--right {\n right: 0;\n}\n\n.canvas-card__resize-handle--top-left,\n.canvas-card__resize-handle--top-right,\n.canvas-card__resize-handle--bottom-right,\n.canvas-card__resize-handle--bottom-left {\n z-index: 4;\n width: 24px;\n height: 24px;\n cursor: nwse-resize;\n}\n\n.canvas-card__resize-handle--top-left {\n top: 0;\n left: 0;\n}\n\n.canvas-card__resize-handle--top-right {\n top: 0;\n right: 0;\n cursor: nesw-resize;\n}\n\n.canvas-card__resize-handle--bottom-right {\n right: 0;\n bottom: 0;\n}\n\n.canvas-card__resize-handle--bottom-left {\n bottom: 0;\n left: 0;\n cursor: nesw-resize;\n}\n\n.canvas-card:hover .canvas-card__resize-handle,\n.canvas-card:focus-within .canvas-card__resize-handle {\n opacity: 1;\n}\n\n.canvas-card__resize-handle:hover {\n color: var(--accent);\n}\n\n.canvas-card__resize-handle:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n opacity: 1;\n}\n\n.canvas-card.is-linked {\n --frame-accent: #a855f7;\n}\n\n.canvas-card.is-linked:not(.is-focus-presentation) .canvas-card__frame {\n outline: 2px solid var(--frame-accent);\n outline-offset: -2px;\n}\n\n.canvas-card__iteration-badge {\n color: var(--text-secondary);\n background: var(--surface-raised-2x);\n}\n";
|
|
32
32
|
const foundationStyles = ":host {\n all: initial;\n --surface-panel: #F5F5F4;\n --surface-control: var(--surface-well);\n --surface-control-hover: var(--surface-well-2x);\n --surface-raised: #FAFAFA;\n --surface-raised-2x: #fff ;\n --surface-raised-hover: #F5F5F4;\n --surface-well: hsla(0, 0%, 0%, 0.05);\n --surface-well-2x: hsla(0, 0%, 0%, 0.10);\n\n --border-subtle: hsla(0, 0%, 0%, 0.1);\n --border-medium: hsla(0, 0%, 0%, 0.2);\n --border-strong: #d8d8d8;\n\n --text-primary: #242424;\n --text-secondary: #6f6f6f;\n --text-muted: #777777;\n --text-muted-subtle: #a1a1a1;\n\n --accent: #0096ff;\n --accent-surface: rgba(0, 176, 255, 0.15);\n --warning: #d97706;\n --warning-text: #9a5b00;\n --warning-surface: #fff6e5;\n --danger: #df4b4b;\n --danger-text: #bd3636;\n --danger-surface: rgba(223, 75, 75, 0.2);\n --success: #2e7d52;\n\n --space-0: 0;\n --space-4: 4px;\n --space-8: 8px;\n --space-12: 12px;\n --space-16: 16px;\n --space-20: 20px;\n --space-24: 24px;\n --space-32: 32px;\n --space-40: 40px;\n --space-48: 48px;\n\n /* Radius */\n --radius-0: 0;\n --radius-4: 4px;\n --radius-8: 8px;\n --radius-12: 12px;\n --radius-16: 16px;\n --radius-20: 20px;\n --radius-24: 24px;\n --radius-pill: 999px;\n\n --radius-control: var(--radius-8);\n --shadow-outline: 0 0 0 1px rgba(0, 0, 0, 0.05);\n --shadow-popup: 0 0 0 1px rgba(0, 0, 0, 0.05), 0 4px 16px rgba(0, 0, 0, 0.10);\n --shadow-control: inset 0 -2px 0 rgba(0, 0, 0, 0.03), 0 0 0 0.5px rgba(0, 0, 0, 0.25);\n --z-index: 2147483647;\n\n --font-family: \"Inter Variable\", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n --font-size-base: 12px;\n --font-size-small: 10px;\n --font-weight-base: 400;\n --font-weight-label: 600;\n\n /* Control semantic sizes */\n --control-height-small: 24px;\n --control-height-large: 32px;\n --control-height-xlarge: 40px;\n --label-column: 104px;\n\n --icon-size-small: 16px;\n --icon-size-large: 20px;\n --icon-stroke-width: 1.5px;\n\n position: fixed;\n inset: 0;\n z-index: var(--z-index);\n pointer-events: none;\n font-family: var(--font-family);\n background: transparent;\n box-sizing: border-box;\n}\n\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nbutton,\ninput,\nselect {\n font-family: inherit;\n}\n";
|
|
33
33
|
import { useInspectorOpen } from "../shell/openStore.js";
|
|
@@ -84,8 +84,7 @@ export function CanvasWorkspace({ primaryUrl }) {
|
|
|
84
84
|
const activatedTargetRef = useRef(null);
|
|
85
85
|
const demoSeededRef = useRef(false);
|
|
86
86
|
const previousPresentationRef = useRef(presentation);
|
|
87
|
-
const
|
|
88
|
-
const canvasEntryAnimationRef = useRef(null);
|
|
87
|
+
const presentationAnimationRef = useRef(null);
|
|
89
88
|
const [boardCursorClass, setBoardCursorClass] = useState("");
|
|
90
89
|
const [selectedTool, setInteractionTool] = useState("design");
|
|
91
90
|
const temporaryAppInteraction = useTemporaryAppInteraction();
|
|
@@ -106,73 +105,54 @@ export function CanvasWorkspace({ primaryUrl }) {
|
|
|
106
105
|
const presentationCardId = selectedCardId ?? focusedCardId ?? cards[0]?.id ?? null;
|
|
107
106
|
const linkedFrameIds = getLinkedFrameIds(cards, selectedCardId);
|
|
108
107
|
useLayoutEffect(() => {
|
|
108
|
+
const previousPresentation = previousPresentationRef.current;
|
|
109
|
+
previousPresentationRef.current = presentation;
|
|
110
|
+
if (previousPresentation === presentation)
|
|
111
|
+
return;
|
|
112
|
+
presentationAnimationRef.current?.cancel();
|
|
113
|
+
presentationAnimationRef.current = null;
|
|
114
|
+
if (!presentationTransitioning)
|
|
115
|
+
return;
|
|
109
116
|
const board = boardRef.current;
|
|
117
|
+
const card = cards.find((candidate) => candidate.id === presentationCardId);
|
|
110
118
|
const cardElement = presentationCardId
|
|
111
119
|
? board?.querySelector(`[data-card-id="${presentationCardId}"]`)
|
|
112
120
|
: null;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
transformOrigin: "center center",
|
|
149
|
-
transform: `translate(${localTranslateX}px, ${localTranslateY}px) scale(${scaleX}, ${scaleY})`,
|
|
150
|
-
},
|
|
151
|
-
{ transformOrigin: "center center", transform: "none" },
|
|
152
|
-
], { duration: PRESENTATION_TRANSITION_MS, easing: "ease", fill: "both" });
|
|
153
|
-
canvasEntryAnimationRef.current = animation;
|
|
154
|
-
void animation.finished.then(() => {
|
|
155
|
-
if (canvasEntryAnimationRef.current !== animation)
|
|
156
|
-
return;
|
|
157
|
-
canvasEntryAnimationRef.current = null;
|
|
158
|
-
animation.cancel();
|
|
159
|
-
boardContent.style.transition = "";
|
|
160
|
-
cardElement.style.transition = "";
|
|
161
|
-
}).catch(() => {
|
|
162
|
-
if (canvasEntryAnimationRef.current !== animation)
|
|
163
|
-
return;
|
|
164
|
-
canvasEntryAnimationRef.current = null;
|
|
165
|
-
boardContent.style.transition = "";
|
|
166
|
-
cardElement.style.transition = "";
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
boardContent.style.transition = "";
|
|
171
|
-
cardElement.style.transition = "";
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
previousPresentationRef.current = presentation;
|
|
175
|
-
}, [camera.zoom, presentation, presentationCardId, presentationTransitioning]);
|
|
121
|
+
if (!board || !card || !cardElement || typeof cardElement.animate !== "function" || camera.zoom <= 0)
|
|
122
|
+
return;
|
|
123
|
+
// The previous layout is already gone, so derive it: focus fills the
|
|
124
|
+
// board, and canvas places the card through the camera.
|
|
125
|
+
const boardRect = snapshotRect(board.getBoundingClientRect());
|
|
126
|
+
const canvasRect = {
|
|
127
|
+
left: boardRect.left + camera.x + card.x * camera.zoom,
|
|
128
|
+
top: boardRect.top + camera.y + card.y * camera.zoom,
|
|
129
|
+
width: card.width * camera.zoom,
|
|
130
|
+
height: card.height * camera.zoom,
|
|
131
|
+
};
|
|
132
|
+
const oldRect = presentation === "focus" ? canvasRect : boardRect;
|
|
133
|
+
const localScale = presentation === "focus" ? 1 : camera.zoom;
|
|
134
|
+
const newRect = snapshotRect(cardElement.getBoundingClientRect());
|
|
135
|
+
if (newRect.width <= 0 || newRect.height <= 0)
|
|
136
|
+
return;
|
|
137
|
+
const localTranslateX = (oldRect.left + oldRect.width / 2 - (newRect.left + newRect.width / 2)) / localScale;
|
|
138
|
+
const localTranslateY = (oldRect.top + oldRect.height / 2 - (newRect.top + newRect.height / 2)) / localScale;
|
|
139
|
+
const scaleX = oldRect.width / newRect.width;
|
|
140
|
+
const scaleY = oldRect.height / newRect.height;
|
|
141
|
+
const animation = cardElement.animate([
|
|
142
|
+
{
|
|
143
|
+
transformOrigin: "center center",
|
|
144
|
+
transform: `translate(${localTranslateX}px, ${localTranslateY}px) scale(${scaleX}, ${scaleY})`,
|
|
145
|
+
},
|
|
146
|
+
{ transformOrigin: "center center", transform: "none" },
|
|
147
|
+
], { duration: PRESENTATION_TRANSITION_MS, easing: "ease", fill: "both" });
|
|
148
|
+
presentationAnimationRef.current = animation;
|
|
149
|
+
void animation.finished.then(() => {
|
|
150
|
+
if (presentationAnimationRef.current !== animation)
|
|
151
|
+
return;
|
|
152
|
+
presentationAnimationRef.current = null;
|
|
153
|
+
animation.cancel();
|
|
154
|
+
}).catch(() => undefined);
|
|
155
|
+
}, [camera, cards, presentation, presentationCardId, presentationTransitioning]);
|
|
176
156
|
useLayoutEffect(() => {
|
|
177
157
|
const board = boardRef.current;
|
|
178
158
|
if (!primaryUrl || !board)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const FRAME_SCROLLBAR_STYLE_ID = "nudge-ui-frame-scrollbar-styles";
|
|
2
|
+
// `:where()` keeps specificity at zero so any application scrollbar rule wins.
|
|
3
|
+
const FRAME_SCROLLBAR_CSS = `
|
|
4
|
+
:where(html) {
|
|
5
|
+
scrollbar-color: rgba(128, 128, 128, 0.5) transparent;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
:where(*) {
|
|
9
|
+
scrollbar-width: thin;
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
12
|
+
/** Gives frame documents thin scrollbars over a transparent track by default. */
|
|
13
|
+
export function installFrameScrollbarStyles(doc) {
|
|
14
|
+
if (!doc.head || doc.getElementById(FRAME_SCROLLBAR_STYLE_ID))
|
|
15
|
+
return () => undefined;
|
|
16
|
+
const style = doc.createElement("style");
|
|
17
|
+
style.id = FRAME_SCROLLBAR_STYLE_ID;
|
|
18
|
+
style.textContent = FRAME_SCROLLBAR_CSS;
|
|
19
|
+
doc.head.append(style);
|
|
20
|
+
return () => style.remove();
|
|
21
|
+
}
|
|
@@ -3,7 +3,7 @@ import type { ChangeRecord } from "./types.js";
|
|
|
3
3
|
import { type CommitResult } from "./draftChanges.js";
|
|
4
4
|
import type { StructuralChange } from "./structuralTypes.js";
|
|
5
5
|
export { isComponentChange, isElementChange, isTextContentChange, isTextContentChangeValue, isPreviewableChange, isTokenChange, } from "./types.js";
|
|
6
|
-
export type { ChangeRecord, ComponentChangeRecord, ElementChangeRecord, PreviewableChangeRecord, TextContentChangeRecord, TokenChangeRecord, RuntimeElementEvidence, } from "./types.js";
|
|
6
|
+
export type { ChangeRecord, ComponentChangeRecord, ElementChangeRecord, PreviewableChangeRecord, TextContentChangeRecord, TokenChangeRecord, RuntimeElementEvidence, SourceElementEvidence, } from "./types.js";
|
|
7
7
|
export interface AppendChangesOptions {
|
|
8
8
|
/** The rendered target captured for each change key at commit time. */
|
|
9
9
|
verificationTargets?: ReadonlyMap<string, HTMLElement | null>;
|
|
@@ -26,6 +26,8 @@ export declare function revertChange(change: ChangeRecord): void;
|
|
|
26
26
|
export declare function reconcileVerifiedChanges(verifiedKeys: ReadonlySet<string>): number;
|
|
27
27
|
/** Atomically reconciles ordinary and structural intent from one agent handoff. */
|
|
28
28
|
export declare function reconcileVerifiedDraftChanges(verifiedKeys: ReadonlySet<string>, verifiedStructuralIds: ReadonlySet<string>): number;
|
|
29
|
+
/** Replaces ordinary and structural intent as one undoable edit. */
|
|
30
|
+
export declare function commitDraftContents(changes: readonly ChangeRecord[], structuralChanges: readonly StructuralChange[]): boolean;
|
|
29
31
|
export declare function discardChangesForSelector(selector: string): void;
|
|
30
32
|
/** Relink removes every CSS declaration owned by one durable rendered target. */
|
|
31
33
|
export declare function discardChangesForInstanceOverride(overrideId: string): void;
|
|
@@ -168,6 +168,14 @@ export function reconcileVerifiedDraftChanges(verifiedKeys, verifiedStructuralId
|
|
|
168
168
|
markForVerification(getChangesSnapshot().map(changeKey));
|
|
169
169
|
return removed;
|
|
170
170
|
}
|
|
171
|
+
/** Replaces ordinary and structural intent as one undoable edit. */
|
|
172
|
+
export function commitDraftContents(changes, structuralChanges) {
|
|
173
|
+
if (!draftChangeStore.commitContents({ changes: [...changes], structuralChanges: [...structuralChanges] }))
|
|
174
|
+
return false;
|
|
175
|
+
reapply(draftChangeStore.getSnapshot());
|
|
176
|
+
markForVerification(getChangesSnapshot().map(changeKey));
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
171
179
|
export function discardChangesForSelector(selector) {
|
|
172
180
|
if (!draftChangeStore.discardChangeRecords({ kind: "selector", selector }))
|
|
173
181
|
return;
|
|
@@ -32,6 +32,7 @@ export interface SerializableElementChange {
|
|
|
32
32
|
component: string;
|
|
33
33
|
};
|
|
34
34
|
runtimeEvidence?: NonNullable<ElementChangeRecord["runtimeEvidence"]>;
|
|
35
|
+
sourceEvidence?: NonNullable<ElementChangeRecord["sourceEvidence"]>;
|
|
35
36
|
scope?: EditScope;
|
|
36
37
|
instanceOverride?: RenderedInstanceOverride;
|
|
37
38
|
state?: NonNullable<ElementChangeRecord["state"]>;
|
|
@@ -16,7 +16,7 @@ export function isSerializableElementChangeValue(value) {
|
|
|
16
16
|
|| !hasOnlyKeys(value, [
|
|
17
17
|
"kind", "cid", "file", "line", "column", "selector", "property",
|
|
18
18
|
"sourceProperty", "sourceAuthoredValue", "oldToken", "newToken", "rawValue",
|
|
19
|
-
"oldRawValue", "source", "runtimeEvidence", "scope", "instanceOverride", "state",
|
|
19
|
+
"oldRawValue", "source", "runtimeEvidence", "sourceEvidence", "scope", "instanceOverride", "state",
|
|
20
20
|
])
|
|
21
21
|
|| (value.kind !== undefined && value.kind !== "element")
|
|
22
22
|
|| typeof value.cid !== "string"
|
|
@@ -33,6 +33,7 @@ export function isSerializableElementChangeValue(value) {
|
|
|
33
33
|
|| (value.rawValue !== undefined && typeof value.rawValue !== "string")
|
|
34
34
|
|| (value.oldRawValue !== undefined && typeof value.oldRawValue !== "string")
|
|
35
35
|
|| (value.runtimeEvidence !== undefined && !isRuntimeElementEvidenceValue(value.runtimeEvidence))
|
|
36
|
+
|| (value.sourceEvidence !== undefined && !isSourceElementEvidenceValue(value.sourceEvidence))
|
|
36
37
|
|| !isEditScopeValue(value.scope)
|
|
37
38
|
|| (value.instanceOverride !== undefined && value.scope !== "rendered-instance")
|
|
38
39
|
|| (value.scope === "rendered-instance" && !isRenderedInstanceOverride(value.instanceOverride))
|
|
@@ -115,6 +116,7 @@ export function serializeElementChange(change) {
|
|
|
115
116
|
oldRawValue: change.oldRawValue,
|
|
116
117
|
source: change.source,
|
|
117
118
|
runtimeEvidence: change.runtimeEvidence ? { ...change.runtimeEvidence } : undefined,
|
|
119
|
+
sourceEvidence: change.sourceEvidence ? { ...change.sourceEvidence } : undefined,
|
|
118
120
|
scope: change.scope === "rendered-instance" ? "rendered-instance" : "source-site",
|
|
119
121
|
instanceOverride: change.scope === "rendered-instance" ? change.instanceOverride : undefined,
|
|
120
122
|
state: change.state,
|
|
@@ -200,6 +202,7 @@ export function deserializeElementChange(serialized) {
|
|
|
200
202
|
oldRawValue: serialized.oldRawValue,
|
|
201
203
|
source: serialized.source,
|
|
202
204
|
runtimeEvidence: serialized.runtimeEvidence ? { ...serialized.runtimeEvidence } : undefined,
|
|
205
|
+
sourceEvidence: serialized.sourceEvidence ? { ...serialized.sourceEvidence } : undefined,
|
|
203
206
|
scope: serialized.scope ?? "source-site",
|
|
204
207
|
instanceOverride: serialized.scope === "rendered-instance" ? serialized.instanceOverride : undefined,
|
|
205
208
|
state: serialized.state,
|
|
@@ -331,6 +334,12 @@ function isRuntimeElementEvidenceValue(value) {
|
|
|
331
334
|
&& (typeof value.props === "string" || value.props === null)
|
|
332
335
|
&& (typeof value.ariaLabel === "string" || value.ariaLabel === null);
|
|
333
336
|
}
|
|
337
|
+
function isSourceElementEvidenceValue(value) {
|
|
338
|
+
return isRecord(value)
|
|
339
|
+
&& hasOnlyKeys(value, ["tagName", "props"])
|
|
340
|
+
&& typeof value.tagName === "string"
|
|
341
|
+
&& (typeof value.props === "string" || value.props === null);
|
|
342
|
+
}
|
|
334
343
|
function isTokenRefValue(value) {
|
|
335
344
|
if (value === null)
|
|
336
345
|
return true;
|
|
@@ -31,6 +31,8 @@ export interface DraftChangeStore {
|
|
|
31
31
|
}): boolean;
|
|
32
32
|
commitStructuralChange(change: StructuralChange): boolean;
|
|
33
33
|
revertStructuralChangeRecord(changeId: string): boolean;
|
|
34
|
+
/** Replaces the active draft as one undoable edit. */
|
|
35
|
+
commitContents(next: DraftContents): boolean;
|
|
34
36
|
reconcileActiveDraftChanges(verifiedKeys: ReadonlySet<string>, verifiedStructuralIds: ReadonlySet<string>): number;
|
|
35
37
|
undoChange(): boolean;
|
|
36
38
|
redoChange(): boolean;
|
|
@@ -175,6 +175,9 @@ function revertStructuralChangeRecordImpl(changeId) {
|
|
|
175
175
|
const structuralChanges = contents.structuralChanges.filter((change) => change.id !== changeId);
|
|
176
176
|
return structuralChanges.length !== contents.structuralChanges.length && commit({ ...contents, structuralChanges });
|
|
177
177
|
}
|
|
178
|
+
function commitContentsImpl(next) {
|
|
179
|
+
return !sameDraftContents(draftState().contents, next) && commit(next);
|
|
180
|
+
}
|
|
178
181
|
export function reconcileDraftChanges(id, verifiedKeys, verifiedStructuralIds) {
|
|
179
182
|
if (!canEditDraft(id))
|
|
180
183
|
return 0;
|
|
@@ -255,6 +258,7 @@ export const draftChangeStore = {
|
|
|
255
258
|
discardChangeRecords: discardChangeRecordsImpl,
|
|
256
259
|
commitStructuralChange: commitStructuralChangeImpl,
|
|
257
260
|
revertStructuralChangeRecord: revertStructuralChangeRecordImpl,
|
|
261
|
+
commitContents: commitContentsImpl,
|
|
258
262
|
reconcileActiveDraftChanges: reconcileActiveDraftChangesImpl,
|
|
259
263
|
undoChange: () => replay("undo"),
|
|
260
264
|
redoChange: () => replay("redo"),
|
|
@@ -18,8 +18,18 @@ export interface RenderedInstanceRef {
|
|
|
18
18
|
text: string | null;
|
|
19
19
|
/** An accessible name can distinguish icon-only repeated controls. */
|
|
20
20
|
ariaLabel?: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The component invocation whose entire output is this element. Present
|
|
23
|
+
* only when evidence alone cannot tell identical outputs apart.
|
|
24
|
+
*/
|
|
25
|
+
invocation?: InvocationRef;
|
|
21
26
|
};
|
|
22
27
|
}
|
|
28
|
+
/** A component callsite, as stamped by the compiler, and its component name. */
|
|
29
|
+
export interface InvocationRef {
|
|
30
|
+
callsiteId: string;
|
|
31
|
+
componentName: string;
|
|
32
|
+
}
|
|
23
33
|
export interface RenderedInstanceOverride {
|
|
24
34
|
id: string;
|
|
25
35
|
target: RenderedInstanceRef;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
function isInvocationRef(value) {
|
|
2
|
+
return isRecord(value)
|
|
3
|
+
&& hasOnlyKeys(value, ["callsiteId", "componentName"])
|
|
4
|
+
&& typeof value.callsiteId === "string" && value.callsiteId.length > 0 && value.callsiteId.length <= 1024
|
|
5
|
+
&& typeof value.componentName === "string" && value.componentName.length > 0 && value.componentName.length <= 256;
|
|
6
|
+
}
|
|
1
7
|
export function isRenderedInstanceRef(value) {
|
|
2
8
|
if (!isRecord(value) || !hasOnlyKeys(value, ["sourceSite", "locator"]))
|
|
3
9
|
return false;
|
|
@@ -8,13 +14,14 @@ export function isRenderedInstanceRef(value) {
|
|
|
8
14
|
|| typeof source.cid !== "string"
|
|
9
15
|
|| typeof source.src !== "string"
|
|
10
16
|
|| !isRecord(locator)
|
|
11
|
-
|| !hasOnlyKeys(locator, ["kind", "occurrence", "props", "text", "ariaLabel"]))
|
|
17
|
+
|| !hasOnlyKeys(locator, ["kind", "occurrence", "props", "text", "ariaLabel", "invocation"]))
|
|
12
18
|
return false;
|
|
13
19
|
return locator.kind === "evidence"
|
|
14
20
|
&& Number.isSafeInteger(locator.occurrence) && locator.occurrence >= 0
|
|
15
21
|
&& (typeof locator.props === "string" || locator.props === null)
|
|
16
22
|
&& (typeof locator.text === "string" || locator.text === null)
|
|
17
|
-
&& (locator.ariaLabel === undefined || typeof locator.ariaLabel === "string" || locator.ariaLabel === null)
|
|
23
|
+
&& (locator.ariaLabel === undefined || typeof locator.ariaLabel === "string" || locator.ariaLabel === null)
|
|
24
|
+
&& (locator.invocation === undefined || isInvocationRef(locator.invocation));
|
|
18
25
|
}
|
|
19
26
|
export function isRenderedInstanceOverride(value) {
|
|
20
27
|
return isRecord(value)
|
|
@@ -13,6 +13,11 @@ export interface RuntimeElementEvidence {
|
|
|
13
13
|
props: string | null;
|
|
14
14
|
ariaLabel: string | null;
|
|
15
15
|
}
|
|
16
|
+
/** Bounded authored facts for one annotated element. */
|
|
17
|
+
export interface SourceElementEvidence {
|
|
18
|
+
tagName: string;
|
|
19
|
+
props: string | null;
|
|
20
|
+
}
|
|
16
21
|
export interface ElementChangeRecord {
|
|
17
22
|
kind?: "element";
|
|
18
23
|
cid: string;
|
|
@@ -37,6 +42,8 @@ export interface ElementChangeRecord {
|
|
|
37
42
|
};
|
|
38
43
|
/** Present when the selected element has a document-local runtime identity. */
|
|
39
44
|
runtimeEvidence?: RuntimeElementEvidence;
|
|
45
|
+
/** Authored facts that relocate the source site after its position shifts. */
|
|
46
|
+
sourceEvidence?: SourceElementEvidence;
|
|
40
47
|
scope?: EditScope;
|
|
41
48
|
/** Durable, controller-owned target for one rendered output. */
|
|
42
49
|
instanceOverride?: RenderedInstanceOverride;
|