@pygmalionjs/pygmalion 0.6.27 → 0.6.29

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 (29) hide show
  1. package/dist-lib/{FrozenRoutePreview-ziDz92ma.js → FrozenRoutePreview-D9J35QsM.js} +3568 -2799
  2. package/dist-lib/pygmalion.js +11044 -10373
  3. package/dist-lib/testing.js +1 -1
  4. package/dist-lib/types/canvas/ShadowRoutePreview.d.ts +3 -0
  5. package/dist-lib/types/canvas/useFlowSession.d.ts +3 -1
  6. package/dist-lib/types/editor/designImport.d.ts +42 -3
  7. package/dist-lib/types/editor/flowSessionScheduler.d.ts +54 -5
  8. package/dist-lib/types/editor/flowSessions.d.ts +51 -9
  9. package/dist-lib/types/editor/frameInteraction.d.ts +7 -1
  10. package/dist-lib/types/editor/geometryStability.d.ts +122 -0
  11. package/dist-lib/types/editor/heldPseudoStates.d.ts +22 -0
  12. package/dist-lib/types/editor/interactiveSessionSurface.d.ts +32 -0
  13. package/dist-lib/types/editor/interactiveStates.d.ts +27 -14
  14. package/dist-lib/types/editor/previewBootstrap.d.ts +7 -1
  15. package/dist-lib/types/editor/previewReachability.d.ts +7 -0
  16. package/dist-lib/types/editor/projectRuntime.d.ts +5 -0
  17. package/dist-lib/types/editor/routePreview.d.ts +5 -0
  18. package/dist-lib/types/editor/stateSwitchMetrics.d.ts +150 -0
  19. package/dist-lib/types/editor/store.d.ts +70 -3
  20. package/dist-lib/types/editor/storyboardDiscovery.d.ts +1 -0
  21. package/dist-lib/types/editor/variantPrefetch.d.ts +69 -0
  22. package/dist-lib/types/lib.d.ts +67 -1
  23. package/docs/screen-state-contract.md +61 -9
  24. package/node/dev-mirror.mjs +80 -8
  25. package/node/preview-artifact-plugin.mjs +14 -1
  26. package/node/preview-artifact-store.mjs +39 -3
  27. package/node/storyboard-capture-runtime.mjs +90 -0
  28. package/node/vite.mjs +1 -0
  29. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { F as s, N as a, e as r, s as i, a as t } from "./FrozenRoutePreview-ziDz92ma.js";
1
+ import { F as s, N as a, e as r, s as i, a as t } from "./FrozenRoutePreview-D9J35QsM.js";
2
2
  export {
3
3
  s as FrozenRoutePreviewView,
4
4
  a as NodeModel,
@@ -17,4 +17,7 @@ export declare const ShadowRoutePreview: import("react").FunctionComponent<{
17
17
  mountPriority?: number;
18
18
  onDocumentHeightChange?: (height: number) => void;
19
19
  onReplayIssuesChange?: (issues: string[]) => void;
20
+ onReachableTestIdsChange?: (testIds: readonly string[]) => void;
21
+ /** Enables immediate scrolling while the live React document warms. */
22
+ interactionEnabled?: boolean;
20
23
  }>;
@@ -29,6 +29,7 @@ export declare function useInteractiveSessionPending(page: {
29
29
  interactiveStateId?: string;
30
30
  interactiveOptionId?: string;
31
31
  interactiveOptionIds?: readonly string[];
32
+ interactiveStateUsesDesiredState?: boolean;
32
33
  }, targetCacheKey?: string): boolean;
33
34
  /** The runner-owned live document currently reproducing this state. */
34
35
  export declare function useInteractiveSessionSurface(page: {
@@ -36,7 +37,8 @@ export declare function useInteractiveSessionSurface(page: {
36
37
  interactiveStateId?: string;
37
38
  interactiveOptionId?: string;
38
39
  interactiveOptionIds?: readonly string[];
39
- }): InteractiveSessionSurface | null;
40
+ interactiveStateUsesDesiredState?: boolean;
41
+ }, preparedSurfaceId?: string | null): InteractiveSessionSurface | null;
40
42
  /**
41
43
  * True while this frame holds one of the host's live screen slots, meaning it
42
44
  * renders the running app rather than its frozen capture.
@@ -64,11 +64,27 @@ export interface DesignScreenInteraction {
64
64
  export interface DesignScreenInteractionRunOptions {
65
65
  /**
66
66
  * Fallback pause after click, focus, hover, fill, check, and press steps.
67
- * The capture default is 600ms. A step's explicit `settleMs` always wins.
67
+ * The capture default is 600ms. A step's explicit `settleMs` wins unless
68
+ * target-driven replay can synchronize on the following observed wait.
68
69
  */
69
70
  defaultActionSettleMs?: number;
71
+ /**
72
+ * Uses observable wait steps as the synchronization point for the action
73
+ * immediately before them. This removes the action's fixed settle and the
74
+ * matched wait's implicit 500ms pause; an explicit settle on the wait and a
75
+ * timer-only wait remain authoritative. Capture replay keeps the default
76
+ * `authored` policy, while an already-visible interactive transition can use
77
+ * `target-driven` and let the final stability pass protect serialization.
78
+ */
79
+ settlePolicy?: 'authored' | 'target-driven';
70
80
  /** Called after boot presets succeed and immediately before actions begin. */
71
81
  onBeforeInteractions?: () => void;
82
+ /**
83
+ * Called as soon as every interaction succeeds, before capture assertions.
84
+ * Presentation can therefore hand over the live document while validation
85
+ * and snapshot persistence continue in the background.
86
+ */
87
+ onAfterInteractions?: () => void | Promise<void>;
72
88
  }
73
89
  /** A JSON-compatible value that conveys host-specific meaning without Pygmalion interpreting it. */
74
90
  export type DesignSerializable = string | number | boolean | null | readonly DesignSerializable[] | {
@@ -80,6 +96,15 @@ export type DesignSerializable = string | number | boolean | null | readonly Des
80
96
  * The meaning of keys and values ​​is entirely owned by the host adapter.
81
97
  */
82
98
  export type DesignScreenPreset = Readonly<Record<string, DesignSerializable>>;
99
+ /**
100
+ * Complete host-owned UI state for a rendered preview document.
101
+ *
102
+ * Unlike an interaction recipe, this is applied after the application mounts
103
+ * and asks React to render the requested state directly. Every application is
104
+ * a full replacement — including an empty object — so a reused preview cannot
105
+ * retain an override from the frame that occupied it previously.
106
+ */
107
+ export type DesignScreenDesiredState = Readonly<Record<string, DesignSerializable>>;
83
108
  export type StoryboardPermissionState = 'granted' | 'denied' | 'prompt';
84
109
  export type StoryboardMediaFailure = 'not-allowed' | 'not-found' | 'not-readable' | 'overconstrained';
85
110
  export interface StoryboardMediaDevice {
@@ -231,6 +256,8 @@ export interface DesignImportPage {
231
256
  */
232
257
  scenarioIds?: readonly string[];
233
258
  interactions?: readonly DesignScreenInteraction[];
259
+ /** Host-owned post-mount UI state rendered directly instead of replaying gestures. */
260
+ desiredState?: DesignScreenDesiredState;
234
261
  environment?: StoryboardEnvironment;
235
262
  preset?: DesignScreenPreset;
236
263
  assertions?: readonly DesignScreenAssertion[];
@@ -352,6 +379,8 @@ export interface DesignImportInitialPage {
352
379
  /** Every authored scenario this initial screen demonstrates. */
353
380
  scenarioIds?: readonly string[];
354
381
  interactions?: DesignScreenInteraction[];
382
+ /** Host-owned post-mount UI state rendered directly instead of replaying gestures. */
383
+ desiredState?: DesignScreenDesiredState;
355
384
  environment?: StoryboardEnvironment;
356
385
  preset?: DesignScreenPreset;
357
386
  assertions?: DesignScreenAssertion[];
@@ -375,8 +404,8 @@ export interface DesignScreenInteractionReport {
375
404
  failedLabel?: string;
376
405
  error?: string;
377
406
  }
378
- export type DesignScreenCaptureFailureStage = 'preset' | 'interaction' | 'assertion';
379
- export type DesignScreenCaptureFailureCode = 'preset_executor_missing' | 'preset_rejected' | 'preset_error' | 'interaction_failed' | 'invalid_selector' | 'existence_mismatch' | 'text_mismatch' | 'attribute_mismatch' | 'visibility_mismatch';
407
+ export type DesignScreenCaptureFailureStage = 'preset' | 'state' | 'interaction' | 'assertion';
408
+ export type DesignScreenCaptureFailureCode = 'preset_executor_missing' | 'preset_rejected' | 'preset_error' | 'state_executor_missing' | 'state_rejected' | 'state_error' | 'interaction_failed' | 'invalid_selector' | 'existence_mismatch' | 'text_mismatch' | 'attribute_mismatch' | 'visibility_mismatch';
380
409
  export interface DesignScreenCaptureFailure {
381
410
  stage: DesignScreenCaptureFailureStage;
382
411
  code: DesignScreenCaptureFailureCode;
@@ -434,10 +463,18 @@ export interface DesignScreenPresetExecutionResult {
434
463
  details?: DesignSerializable;
435
464
  }
436
465
  export type DesignScreenPresetExecutor = (request: DesignScreenPresetRequest, context: DesignScreenPresetContext) => Promise<DesignScreenPresetExecutionResult | void> | DesignScreenPresetExecutionResult | void;
466
+ /** Serializable desired-state transport. The host alone owns the key semantics. */
467
+ export interface DesignScreenDesiredStateRequest {
468
+ pageId: string;
469
+ route?: string;
470
+ state: DesignScreenDesiredState;
471
+ }
472
+ export type DesignScreenDesiredStateExecutor = (request: DesignScreenDesiredStateRequest, context: DesignScreenPresetContext) => Promise<DesignScreenPresetExecutionResult | void> | DesignScreenPresetExecutionResult | void;
437
473
  export interface DesignScreenCaptureSpec {
438
474
  pageId: string;
439
475
  route?: string;
440
476
  preset?: DesignScreenPreset;
477
+ desiredState?: DesignScreenDesiredState;
441
478
  interactions?: readonly DesignScreenInteraction[];
442
479
  assertions?: readonly DesignScreenAssertion[];
443
480
  }
@@ -495,6 +532,8 @@ export interface CreateDesignImportOptions {
495
532
  layerOptions?: DomImportOptions;
496
533
  /** The only boundary that enforces the host-specific semantics of the preset. The package does not interpret the payload. */
497
534
  executeScreenPreset?: DesignScreenPresetExecutor;
535
+ /** Applies complete host-owned UI state after mount and before gesture replay. */
536
+ executeDesiredState?: DesignScreenDesiredStateExecutor;
498
537
  /**
499
538
  * How screen pages spread across canvases. `single` (default) keeps every
500
539
  * screen on one canvas. `section` gives each section its own canvas
@@ -1,5 +1,5 @@
1
1
  import { type DesignImportController, type DesignScreenCaptureSpec } from './designImport';
2
- import type { ResolvedFlowPath } from './flowSessions';
2
+ import type { FlowSessionRunner, ResolvedFlowPath } from './flowSessions';
3
3
  import type { ScreenFlowPath } from './screenFlows';
4
4
  import { type PageModel } from './store';
5
5
  /** Applies the active frame's own boot condition to a path-specific session. */
@@ -70,18 +70,31 @@ export declare function subscribeViewportSessions(listener: () => void): () => v
70
70
  type InteractivePreparationController = Pick<DesignImportController, 'prepareScreenCapture' | 'prepareScreenState'>;
71
71
  /**
72
72
  * Reproduces an editor-controlled state without capture-only quiescence.
73
- * The runner still waits for a trailing paint and stable geometry before it
74
- * serializes, but presentation begins only after the state recipe has
75
- * finished. Showing a runner before its clicks and fills finish makes the
76
- * reviewer watch the script operate the product instead of reviewing a state.
73
+ * Direct desired state is presented as soon as the host commits React; replay
74
+ * states remain hidden until their gestures finish. Frozen persistence may
75
+ * continue afterward without delaying either live presentation path.
77
76
  */
78
77
  export declare function prepareInteractiveSessionState(iframe: HTMLIFrameElement, spec: DesignScreenCaptureSpec, controller?: InteractivePreparationController | null, onStateReady?: () => void): Promise<import("./designImport").DesignScreenCaptureReport>;
78
+ /**
79
+ * Test seam: replaces the interactive runner with one whose boot/prepare/
80
+ * publish are injected fakes, so the variant-and-repark queueing can run
81
+ * headless. Passing null restores the default runner on next use.
82
+ */
83
+ export declare function __setInteractiveSessionRunnerForTests(runner: FlowSessionRunner | null): void;
79
84
  export declare function interactiveSessionScreenId(page: {
80
85
  id: string;
81
86
  interactiveStateId?: string;
82
87
  interactiveOptionId?: string;
83
88
  interactiveOptionIds?: readonly string[];
84
89
  }): string;
90
+ /** Direct sibling options render through one persistent React document. */
91
+ export declare function interactiveSessionSurfaceId(page: {
92
+ id: string;
93
+ interactiveStateId?: string;
94
+ interactiveOptionId?: string;
95
+ interactiveOptionIds?: readonly string[];
96
+ interactiveStateUsesDesiredState?: boolean;
97
+ }): string;
85
98
  /**
86
99
  * Materializes one screen in its declared interactive state through the
87
100
  * screen's flow path. The prefix walks as pass-through; the target publishes
@@ -97,15 +110,51 @@ export declare function interactiveSessionWillDeliver(page: {
97
110
  interactiveStateId?: string;
98
111
  interactiveOptionId?: string;
99
112
  interactiveOptionIds?: readonly string[];
113
+ interactiveStateUsesDesiredState?: boolean;
100
114
  }, targetCacheKey?: string): boolean;
101
115
  /** Subscribe to interactive-state delivery changes. */
102
116
  export declare function subscribeInteractiveSessions(listener: () => void): () => void;
117
+ /** How one speculative enqueue attempt ended. */
118
+ export type InteractiveVariantPrefetchOutcome = 'queued' | 'busy' | 'tracked' | 'unavailable';
119
+ /** True when the interactive runner has nothing queued or walking. */
120
+ export declare function interactiveSessionRunnerIsIdle(): boolean;
121
+ /**
122
+ * True when the interactive runner already tracks this variant's delivery in
123
+ * any status. Unlike interactiveSessionWillDeliver, an untracked variant
124
+ * reads as false here: the prefetch policy asks "is anyone already on it",
125
+ * not "should the frame wait".
126
+ */
127
+ export declare function interactiveSessionTracksVariant(variantPage: {
128
+ importPageId?: string;
129
+ id: string;
130
+ interactiveStateId?: string;
131
+ interactiveOptionId?: string;
132
+ }): boolean;
133
+ /**
134
+ * Queues one speculative walk for a hypothetical variant identity — a shallow
135
+ * page clone carrying the interactions/environment/state fields the store's
136
+ * applyInteractiveState would produce for the option (variantPrefetch.ts
137
+ * derives it). Sibling of materializePageInteractiveState, kept separate so
138
+ * speculation never changes the real request path.
139
+ *
140
+ * The user must never wait behind speculation: the runner is concurrency-1
141
+ * FIFO, so this enqueues only into an EMPTY runner ('busy' otherwise). At
142
+ * most one speculative walk is therefore ever queued or running, and a real
143
+ * selection made meanwhile queues behind that single walk at worst.
144
+ */
145
+ export declare function prefetchPageInteractiveVariant(variantPage: PageModel, previewRevision: string): InteractiveVariantPrefetchOutcome;
103
146
  /** Diagnostic counters for the interactive-state path. */
104
147
  export declare const __interactiveSessionDebug: {
105
148
  requested: number;
106
149
  noPath: number;
107
150
  resolvedNull: number;
108
151
  queued: number;
152
+ /** Base-prefix re-park warming walks queued behind variant deliveries. */
153
+ reparkQueued: number;
154
+ /** Re-parks skipped because a parked base instance already serves the prefix. */
155
+ reparkAlreadyParked: number;
156
+ /** Re-parks skipped because the option adds no steps over the base screen. */
157
+ reparkNoExtraSteps: number;
109
158
  };
110
159
  /** Interactive-runner delivery states for the headless debug surface. */
111
160
  export declare function __debugInteractiveSessionStates(): Record<string, unknown>;
@@ -1,4 +1,4 @@
1
- import type { DesignScreenAssertion, DesignScreenCaptureReport, DesignScreenInteraction, DesignScreenPreset } from './designImport';
1
+ import type { DesignScreenAssertion, DesignScreenCaptureReport, DesignScreenDesiredState, DesignScreenInteraction, DesignScreenPreset } from './designImport';
2
2
  export interface ResolvedFlowWaypoint {
3
3
  screenId: string;
4
4
  /**
@@ -12,6 +12,19 @@ export interface ResolvedFlowWaypoint {
12
12
  width: number;
13
13
  height: number;
14
14
  steps: readonly DesignScreenInteraction[];
15
+ /** Complete host-owned UI state applied before this waypoint's steps. */
16
+ desiredState?: DesignScreenDesiredState;
17
+ /**
18
+ * This waypoint is a synchronous host render over an already reproduced
19
+ * screen. Interactive supply may publish it after paint without repeating
20
+ * capture-only assertions and geometry waits. Authoritative capture still
21
+ * performs the full contract.
22
+ */
23
+ fastDesiredState?: boolean;
24
+ /** Stable live-presentation slot; sibling direct states share one document. */
25
+ interactiveSurfaceId?: string;
26
+ /** Activation warm-up may prepare a surface before an option is selected. */
27
+ prewarmSurfacePageId?: string;
15
28
  /** The waypoint's own capture assertions, checked after its steps settle. */
16
29
  assertions?: readonly DesignScreenAssertion[];
17
30
  /**
@@ -77,14 +90,16 @@ export interface FlowSessionRunnerOptions {
77
90
  /** Navigates the instance and resolves when the document loaded. */
78
91
  boot?: (iframe: HTMLIFrameElement, url: string) => Promise<void>;
79
92
  /**
80
- * Prepares one waypoint on the instance: preset (host executor) ->
81
- * interactions -> assertions, exactly the capture pipeline's contract.
93
+ * Prepares one waypoint on the instance: preset (host executor) -> desired
94
+ * state (host executor) -> interactions -> assertions, exactly the capture
95
+ * pipeline's contract.
82
96
  * Defaults to the design-import controller's prepareScreenCapture.
83
97
  */
84
98
  prepare?: (iframe: HTMLIFrameElement, spec: {
85
99
  pageId: string;
86
100
  route?: string;
87
101
  preset?: DesignScreenPreset;
102
+ desiredState?: DesignScreenDesiredState;
88
103
  interactions?: readonly DesignScreenInteraction[];
89
104
  assertions?: readonly DesignScreenAssertion[];
90
105
  }, waypoint: ResolvedFlowWaypoint) => Promise<DesignScreenCaptureReport>;
@@ -97,15 +112,21 @@ export interface FlowSessionRunnerOptions {
97
112
  publish?: (cacheKey: string, snapshot: string) => boolean;
98
113
  /** Releases a live document after its delivery finishes or is abandoned. */
99
114
  onWaypointFinish?: (iframe: HTMLIFrameElement, waypoint: ResolvedFlowWaypoint, outcome: 'completed' | 'failed' | 'cancelled') => void;
115
+ /** Keeps a parked document alive while another surface is presenting it. */
116
+ keepWarmInstanceAlive?: (iframe: HTMLIFrameElement) => boolean;
100
117
  onError?: (pathId: string, error: unknown) => void;
101
118
  }
102
119
  /**
103
120
  * Resolves when the document's layout stops moving: two consecutive
104
- * animation frames with an identical element-geometry hash. JS-driven
105
- * entrance animations (rAF springs writing inline transforms) are invisible
106
- * to the frozen stylesheet's animation:none — serializing mid-flight pins
121
+ * animation frames whose element geometry matches. JS-driven entrance
122
+ * animations (rAF springs writing inline transforms) are invisible to the
123
+ * frozen stylesheet's animation:none — serializing mid-flight pins
107
124
  * intermediate rects as overlapping boxes, so the walk waits them out.
108
- * Perpetual animations hit the timeout and serialize best-effort.
125
+ * Movement owned by effectively-infinite animations (spinners, pulses,
126
+ * loops) is exempt from the convergence check instead of blocking it, so
127
+ * screens with perpetual motion settle as fast as still ones; the timeout
128
+ * stays as the backstop for documents that never converge either way.
129
+ * The policy lives in ./geometryStability, where it is tested as pure data.
109
130
  */
110
131
  export declare function waitForGeometryStability(iframe: HTMLIFrameElement, timeoutMs?: number): Promise<void>;
111
132
  export declare function createFlowSessionIframe(): HTMLIFrameElement;
@@ -117,23 +138,37 @@ export declare const __flowSessionWarmDebug: {
117
138
  reuses: number;
118
139
  parks: number;
119
140
  evictions: number;
141
+ /** Warming walks answered by renewing an already-parked instance's lease. */
142
+ refreshes: number;
120
143
  /** Which supply booted, so a stray second boot is attributable. */
121
144
  bootsByRunner: Record<string, number>;
122
145
  };
123
- /** Drops every parked instance identity change, canvas change, teardown. */
124
- export declare function evictFlowSessionWarmInstances(): void;
146
+ /** Drops parked instances globally, or only those owned by one runner. */
147
+ export declare function evictFlowSessionWarmInstances(ownerLabel?: string, options?: {
148
+ preservePinned?: boolean;
149
+ }): void;
125
150
  /**
126
151
  * True when a walk of this path would resume on a parked instance. Activation
127
152
  * warming reads it to decide whether the frame already has a resumable
128
153
  * instance or one has to be walked in the background.
129
154
  */
130
155
  export declare function warmInstanceServesPath(path: ResolvedFlowPath): boolean;
156
+ /** Returns an idle document already parked at this path's exact endpoint. */
157
+ export declare function warmInstanceAtPathEnd(path: ResolvedFlowPath): HTMLIFrameElement | null;
131
158
  export declare function createFlowSessionRunner(options?: FlowSessionRunnerOptions): {
132
159
  /**
133
160
  * Queues paths for materialization. Screens already delivered, failed, or
134
161
  * claimed by a queued path are skipped — callers re-request freely.
135
162
  */
136
163
  materialize(paths: readonly ResolvedFlowPath[]): number;
164
+ /**
165
+ * Applies a path's terminal state on a known live document. The caller
166
+ * guarantees that this iframe already reached the path's route and base
167
+ * prefix; all gesture steps are therefore skipped and only the target's
168
+ * complete desired-state map is prepared. FIFO ordering keeps a document
169
+ * exposed by the current walk from being mutated until that walk finishes.
170
+ */
171
+ materializeOnInstance(path: ResolvedFlowPath, iframe: HTMLIFrameElement): number;
137
172
  /**
138
173
  * Drops queued paths the predicate rejects, releasing their pending
139
174
  * screens so a later materialize can queue them again. Running walks are
@@ -153,6 +188,13 @@ export declare function createFlowSessionRunner(options?: FlowSessionRunnerOptio
153
188
  forgetScreen(screenId: string): boolean;
154
189
  /** True while the session still owes this screen its snapshot. */
155
190
  willDeliver(screenId: string): boolean;
191
+ /**
192
+ * True when nothing is queued or walking. Speculative callers read this
193
+ * before enqueueing: the runner is FIFO, so speculative work queued ahead
194
+ * of a designer's real request would delay it — speculation may only
195
+ * enter an empty runner.
196
+ */
197
+ isIdle(): boolean;
156
198
  subscribe(listener: () => void): () => void;
157
199
  /**
158
200
  * Drops all session state. Running paths notice the round change at their
@@ -1,4 +1,4 @@
1
- export type FrameInteractionPhase = 'idle' | 'starting' | 'interacting';
1
+ export type FrameInteractionPhase = 'idle' | 'starting' | 'preview' | 'interacting';
2
2
  export interface FrameInteractionScrollState {
3
3
  x: number;
4
4
  y: number;
@@ -51,6 +51,12 @@ export declare function subscribeFrameInteraction(listener: () => void): () => v
51
51
  * without learning whether the running surface is an iframe or another host.
52
52
  */
53
53
  export declare function connectFrameInteractionViewport(frameId: string, adapter: FrameInteractionViewportAdapter): () => void;
54
+ /**
55
+ * Makes the frozen DOM useful immediately while its live React document warms.
56
+ * The preview is a scroll-only bridge: navigation and form submission stay
57
+ * contained, and the live iframe later replaces it without ending the session.
58
+ */
59
+ export declare function connectFrameInteractionPreview(frameId: string, root: ShadowRoot, contentElement: HTMLElement): () => void;
54
60
  /** Scrolls the running frame viewport and immediately republishes its reading. */
55
61
  export declare function setFrameInteractionScroll(position: {
56
62
  x?: number;
@@ -0,0 +1,122 @@
1
+ /** Final backstop: a document that never settles is serialized best-effort. */
2
+ export declare const GEOMETRY_STABILITY_TIMEOUT_MS = 3000;
3
+ /**
4
+ * Secondary cap once perpetual motion is being ignored: when every rect that
5
+ * still moves belongs to an ignored element but structural churn (element
6
+ * count, sampled identity) keeps the strict check from passing, the document
7
+ * will not settle by waiting longer.
8
+ */
9
+ export declare const PERPETUAL_RESIDUE_TIMEOUT_MS = 1000;
10
+ /**
11
+ * An element that moved on every consecutive sample pair for this long is
12
+ * treated as perpetually driven even when no animation claims it. One-shot
13
+ * springs settle well inside this window, so they are never reclassified —
14
+ * and a mover that pauses for even one pair either settles the wait outright
15
+ * or is disqualified from reclassification for good.
16
+ */
17
+ export declare const PERSISTENT_MOVER_MS = 1500;
18
+ /**
19
+ * Floor on observed pairs before the persistent-mover rule may fire, so a
20
+ * slow sampling cadence cannot classify from a handful of observations.
21
+ */
22
+ export declare const PERSISTENT_MOVER_MIN_PAIRS = 10;
23
+ /** How many elements a geometry sample inspects, from the top of the body. */
24
+ export declare const GEOMETRY_SAMPLE_LIMIT = 160;
25
+ /** One sampled element: an opaque identity plus its rounded geometry. */
26
+ export interface GeometryElementSample {
27
+ /** Stable identity across samples — in the DOM, the element itself. */
28
+ key: unknown;
29
+ /** Rounded rect signature, e.g. "12,40,300,24". */
30
+ rect: string;
31
+ /**
32
+ * True when an effectively-infinite animation drives this element or an
33
+ * ancestor, so its movement must not block convergence.
34
+ */
35
+ perpetual: boolean;
36
+ }
37
+ /** A point-in-time view of the document's geometry. */
38
+ export interface GeometrySample {
39
+ /** Total rendered element count — a structure signal beyond the sampled window. */
40
+ elementCount: number;
41
+ elements: readonly GeometryElementSample[];
42
+ /** Sample timestamp from the caller's clock, in milliseconds. */
43
+ at: number;
44
+ }
45
+ export type GeometrySettleReason =
46
+ /** Everything held still for one full sample pair — the strict criterion. */
47
+ 'stable'
48
+ /** Everything held still except elements owned by perpetual motion. */
49
+ | 'stable-ignoring-perpetual'
50
+ /** Ignored motion plus structural churn: settled at the lower cap. */
51
+ | 'perpetual-residue'
52
+ /** The final backstop: serialized best-effort. */
53
+ | 'timeout';
54
+ export interface GeometryVerdict {
55
+ settled: boolean;
56
+ reason?: GeometrySettleReason;
57
+ }
58
+ export interface GeometryStabilityJudgeOptions {
59
+ timeoutMs?: number;
60
+ perpetualResidueTimeoutMs?: number;
61
+ persistentMoverMs?: number;
62
+ persistentMoverMinPairs?: number;
63
+ }
64
+ export interface GeometryStabilityJudge {
65
+ /** Consumes the next sample and reports whether the wait may end. */
66
+ next(sample: GeometrySample): GeometryVerdict;
67
+ }
68
+ /**
69
+ * Creates the convergence policy for one stability wait. The strict
70
+ * criterion is unchanged from the original hash comparison: one sample pair
71
+ * with identical element count, identical sampled identities in order, and
72
+ * identical rects. Movement owned by perpetual animations is exempt from
73
+ * the rect comparison; structure changes never are (they fall through to
74
+ * the residue cap or the timeout).
75
+ */
76
+ export declare function createGeometryStabilityJudge(options?: GeometryStabilityJudgeOptions): GeometryStabilityJudge;
77
+ export interface AnimationEffectLike {
78
+ target?: unknown;
79
+ getTiming?: () => {
80
+ iterations?: number;
81
+ };
82
+ getComputedTiming?: () => {
83
+ endTime?: unknown;
84
+ };
85
+ }
86
+ export interface AnimationLike {
87
+ effect?: AnimationEffectLike | null;
88
+ }
89
+ interface RectLike {
90
+ left: number;
91
+ top: number;
92
+ width: number;
93
+ height: number;
94
+ }
95
+ export interface GeometryElementLike {
96
+ nodeType: number;
97
+ getBoundingClientRect(): RectLike;
98
+ contains?(other: GeometryElementLike): boolean;
99
+ }
100
+ export interface GeometryDocumentLike {
101
+ body?: {
102
+ querySelectorAll(selectors: string): ArrayLike<GeometryElementLike>;
103
+ } | null;
104
+ getAnimations?(options?: {
105
+ subtree?: boolean;
106
+ }): AnimationLike[];
107
+ }
108
+ /** True for an animation that will never finish on its own. */
109
+ export declare function isEffectivelyPerpetualAnimation(animation: AnimationLike): boolean;
110
+ /**
111
+ * Elements owned by animations that will never finish on their own. CSS
112
+ * animations with an infinite iteration count and Web Animations API loops
113
+ * both surface here; one-shot animations never do.
114
+ */
115
+ export declare function collectPerpetualMotionRoots(doc: GeometryDocumentLike): GeometryElementLike[];
116
+ /**
117
+ * Reads one geometry sample from a document: rounded rects of the first
118
+ * `limit` elements, each flagged when a perpetual animation drives it or an
119
+ * ancestor (a transform loop on a container moves every descendant rect).
120
+ */
121
+ export declare function sampleGeometry(doc: GeometryDocumentLike, at: number, limit?: number): GeometrySample;
122
+ export {};
@@ -2,6 +2,28 @@ import type { DesignScreenInteraction } from './designImport';
2
2
  export type HeldPseudoState = 'hover' | 'focus' | 'focus-visible' | 'active';
3
3
  /** The pseudo gestures still held after the complete recipe has run. */
4
4
  export declare function heldPseudoInteractionsFromRecipe(interactions: readonly DesignScreenInteraction[]): DesignScreenInteraction[];
5
+ /** True when the interaction is a gesture the frozen preview can hold via CSS. */
6
+ export declare function isHeldPseudoInteraction(interaction: DesignScreenInteraction): boolean;
7
+ /**
8
+ * Why these steps cannot be held on the given frozen document, or an empty
9
+ * list when client-side holding reproduces them faithfully.
10
+ *
11
+ * A target stamped with script-driven pseudo events (PSEUDO_EVENT_ATTRIBUTE)
12
+ * answers the gesture in JavaScript, which a frozen document cannot replay —
13
+ * such steps need the ordinary walk. A missing non-optional target also
14
+ * blocks: the live app may still render it, so the walk keeps that chance.
15
+ */
16
+ export declare function heldPseudoHoldBlockers(root: ParentNode, steps: readonly DesignScreenInteraction[]): string[];
17
+ /**
18
+ * The recipe a preview must hold: the page's own interactions plus any
19
+ * presentation-layer held gestures (the pseudo fast path). The held steps
20
+ * come last, so they override the recipe's terminal gestures exactly as a
21
+ * rewritten recipe would.
22
+ */
23
+ export declare function presentationInteractions(page: {
24
+ interactions?: readonly DesignScreenInteraction[];
25
+ heldPseudoInteractions?: readonly DesignScreenInteraction[];
26
+ }): readonly DesignScreenInteraction[] | undefined;
5
27
  export declare function heldPseudoStatesFromInteractions(interactions: readonly DesignScreenInteraction[]): HeldPseudoState[];
6
28
  /**
7
29
  * Converts pseudo-class selectors into stable attribute selectors while
@@ -6,13 +6,45 @@
6
6
  * published, while the active frame may mirror the running document
7
7
  * immediately so capture stability never becomes interaction latency.
8
8
  */
9
+ import type { DesignScreenDesiredState } from './designImport';
9
10
  export interface InteractiveSessionSurface {
10
11
  iframe: HTMLIFrameElement;
11
12
  screenId: string;
12
13
  }
14
+ /** Stable presentation slot shared by sibling direct-state options. */
15
+ export declare function directInteractiveSessionSurfaceId(pageId: string, stateId: string): string;
16
+ /**
17
+ * Connects the store's synchronous option selection to the session scheduler
18
+ * without making the store import the scheduler that already depends on it.
19
+ */
20
+ export declare function setDirectInteractiveStateRequestHandler(handler: ((pageId: string) => void) | null): void;
21
+ /** Starts direct-state delivery in the same task as the right-panel click. */
22
+ export declare function requestDirectInteractiveState(pageId: string): boolean;
23
+ /** Installs the paint-first path used before the observable page recipe changes. */
24
+ export declare function setDirectInteractiveStatePreviewHandler(handler: ((request: {
25
+ pageId: string;
26
+ stateId: string;
27
+ desiredState: DesignScreenDesiredState;
28
+ }) => boolean) | null): void;
29
+ /** Applies a direct host state without invalidating the editor model yet. */
30
+ export declare function requestDirectInteractiveStatePreview(request: {
31
+ pageId: string;
32
+ stateId: string;
33
+ desiredState: DesignScreenDesiredState;
34
+ }): boolean;
13
35
  /** Exposes a runner-owned iframe without transferring its ownership. */
14
36
  export declare function exposeInteractiveSessionSurface(screenId: string, iframe: HTMLIFrameElement): void;
37
+ /**
38
+ * Hands an in-flight prewarm slot to the frame that has just exposed its axis.
39
+ * If preparation has not presented the iframe yet, its first exposure follows
40
+ * the redirect instead of landing in the now-private staging slot.
41
+ */
42
+ export declare function redirectInteractiveSessionSurface(fromScreenId: string, toScreenId: string): boolean;
15
43
  /** Releases a surface only when the caller still owns the registered iframe. */
16
44
  export declare function releaseInteractiveSessionSurface(screenId: string, iframe: HTMLIFrameElement): void;
45
+ /** Clears a presentation slot when its page returns to an authored state. */
46
+ export declare function clearInteractiveSessionSurface(screenId: string): void;
47
+ /** Active direct-state surfaces pin their warm React document against eviction. */
48
+ export declare function hasInteractiveSessionSurfaceIframe(iframe: HTMLIFrameElement): boolean;
17
49
  export declare function getInteractiveSessionSurface(screenId: string): InteractiveSessionSurface | null;
18
50
  export declare function subscribeInteractiveSessionSurface(screenId: string, listener: () => void): () => void;