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.
Files changed (43) hide show
  1. package/dist/client.mjs +17 -9
  2. package/dist/inspector/agent/verification.d.ts +4 -6
  3. package/dist/inspector/agent/verification.js +60 -16
  4. package/dist/inspector/canvas/CanvasCard.js +2 -0
  5. package/dist/inspector/canvas/CanvasElementOverlay.js +17 -3
  6. package/dist/inspector/canvas/CanvasWorkspace.js +46 -66
  7. package/dist/inspector/canvas/frameScrollbarStyles.d.ts +3 -0
  8. package/dist/inspector/canvas/frameScrollbarStyles.js +21 -0
  9. package/dist/inspector/changes/changesLog.d.ts +3 -1
  10. package/dist/inspector/changes/changesLog.js +8 -0
  11. package/dist/inspector/changes/codecs.d.ts +1 -0
  12. package/dist/inspector/changes/codecs.js +10 -1
  13. package/dist/inspector/changes/draftChanges.d.ts +2 -0
  14. package/dist/inspector/changes/draftChanges.js +4 -0
  15. package/dist/inspector/changes/editModel.d.ts +10 -0
  16. package/dist/inspector/changes/editModel.js +9 -2
  17. package/dist/inspector/changes/types.d.ts +7 -0
  18. package/dist/inspector/comments/element.js +10 -3
  19. package/dist/inspector/componentSemantics/adapterRegistry.d.ts +6 -1
  20. package/dist/inspector/componentSemantics/adapterRegistry.js +23 -1
  21. package/dist/inspector/componentSemantics/reactRuntime.d.ts +2 -1
  22. package/dist/inspector/componentSemantics/reactRuntime.js +38 -0
  23. package/dist/inspector/componentSemantics/runtimeBridge.d.ts +4 -2
  24. package/dist/inspector/componentSemantics/runtimeBridge.js +17 -0
  25. package/dist/inspector/componentSemantics/types.d.ts +11 -0
  26. package/dist/inspector/iterations/capture.js +2 -1
  27. package/dist/inspector/projection/renderedInstance.d.ts +15 -2
  28. package/dist/inspector/projection/renderedInstance.js +53 -6
  29. package/dist/inspector/projection/structuralProjection.js +2 -2
  30. package/dist/inspector/projection/textProjection.js +56 -11
  31. package/dist/inspector/prompt/clipboardHandoff.d.ts +5 -0
  32. package/dist/inspector/prompt/clipboardHandoff.js +5 -0
  33. package/dist/inspector/prompt/generatePrompt.js +5 -1
  34. package/dist/inspector/selection/sourceSite.d.ts +12 -0
  35. package/dist/inspector/selection/sourceSite.js +28 -0
  36. package/dist/inspector/shell/CopyPromptButton.js +49 -7
  37. package/dist/inspector/styleEditors/BackgroundEditor.js +1 -1
  38. package/dist/inspector/tokens/TokenField.js +6 -4
  39. package/dist/inspector/tokens/editActions.js +6 -0
  40. package/dist/inspector/ui/styles.js +1 -1
  41. package/dist/inspector/workspace/sentItems.d.ts +19 -0
  42. package/dist/inspector/workspace/sentItems.js +51 -0
  43. 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 { resolveRenderedInstance } from "../projection/renderedInstance.js";
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 verifyStyle(change, doc) {
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 false;
57
- let targets;
55
+ return [];
58
56
  try {
59
- targets = Array.from(doc.querySelectorAll(selector));
57
+ return Array.from(doc.querySelectorAll(selector));
60
58
  }
61
59
  catch {
62
- return false;
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
- const resolved = resolveTextProjectionTarget(doc, change.target, change.after);
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, change.target, change.after);
103
+ const node = resolveTextProjectionTextNode(resolved.element, target, change.after);
74
104
  if (node)
75
105
  return node.nodeValue === change.after;
76
- return change.target.textNodePath === undefined
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
- && resolveRenderedInstance(doc, change.target).status === "missing";
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 = resolveRenderedInstance(doc, change.target);
139
+ const target = resolveMovedRenderedInstance(doc, change.target);
110
140
  if (target.status !== "resolved")
111
141
  return false;
112
- const parent = resolveRenderedInstance(doc, change.destination.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 = resolveRenderedInstance(doc, change.destination.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 projectedSpacingBadge = spacingDragBadge
642
- ? projectPoint(spacingDragBadge.iframe, spacingDragBadge.point.x, spacingDragBadge.point.y, projectionZoom)
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 && spacingDragBadge ? (_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(spacingDragBadge.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
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.is-presentation-transitioning .canvas-workspace__board-content {\n transition: transform 200ms ease;\n}\n\n.canvas-workspace[data-presentation=\"focus\"] .canvas-workspace__board-content {\n inset: 0;\n transform: none !important;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-card.is-presentation-card {\n transition: left 200ms ease, top 200ms ease;\n}\n\n.canvas-workspace.is-presentation-transitioning .canvas-card:not(.is-presentation-card) {\n visibility: hidden !important;\n pointer-events: none !important;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .canvas-workspace.is-presentation-transitioning .canvas-workspace__board-content,\n .canvas-workspace.is-presentation-transitioning .canvas-card.is-presentation-card {\n transition: none;\n }\n}\n\n.canvas-workspace__target-error {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n color: #6b7280;\n font: 500 14px/1.4 system-ui, sans-serif;\n}\n\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";
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 focusCardRectRef = useRef(null);
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
- const boardContent = board?.querySelector('[data-test="canvas-board-content"]');
114
- if (presentation === "focus") {
115
- canvasEntryAnimationRef.current?.cancel();
116
- canvasEntryAnimationRef.current = null;
117
- if (boardContent)
118
- boardContent.style.transition = "";
119
- if (cardElement)
120
- cardElement.style.transition = "";
121
- focusCardRectRef.current = cardElement ? snapshotRect(cardElement.getBoundingClientRect()) : null;
122
- }
123
- else if (presentation === "canvas"
124
- && previousPresentationRef.current === "focus"
125
- && presentationTransitioning
126
- && cardElement
127
- && boardContent
128
- && focusCardRectRef.current
129
- && typeof cardElement.animate === "function") {
130
- // Commit the final layout before measuring it. The old implementation
131
- // transitioned both the camera and the card's position, which made the
132
- // nested scale and translation produce a sideways drift.
133
- boardContent.style.transition = "none";
134
- cardElement.style.transition = "none";
135
- const oldRect = focusCardRectRef.current;
136
- const newRect = snapshotRect(cardElement.getBoundingClientRect());
137
- if (newRect.width > 0 && newRect.height > 0 && camera.zoom > 0) {
138
- const oldCenterX = oldRect.left + oldRect.width / 2;
139
- const oldCenterY = oldRect.top + oldRect.height / 2;
140
- const newCenterX = newRect.left + newRect.width / 2;
141
- const newCenterY = newRect.top + newRect.height / 2;
142
- const localTranslateX = (oldCenterX - newCenterX) / camera.zoom;
143
- const localTranslateY = (oldCenterY - newCenterY) / camera.zoom;
144
- const scaleX = oldRect.width / newRect.width;
145
- const scaleY = oldRect.height / newRect.height;
146
- const animation = cardElement.animate([
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,3 @@
1
+ export declare const FRAME_SCROLLBAR_STYLE_ID = "nudge-ui-frame-scrollbar-styles";
2
+ /** Gives frame documents thin scrollbars over a transparent track by default. */
3
+ export declare function installFrameScrollbarStyles(doc: Document): () => void;
@@ -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;