@pygmalionjs/pygmalion 0.6.28 → 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.
@@ -1,4 +1,4 @@
1
- import { F as s, N as a, e as r, s as i, a as t } from "./FrozenRoutePreview-Xm7o3urC.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
  }>;
@@ -144,7 +144,9 @@ export declare const __flowSessionWarmDebug: {
144
144
  bootsByRunner: Record<string, number>;
145
145
  };
146
146
  /** Drops parked instances globally, or only those owned by one runner. */
147
- export declare function evictFlowSessionWarmInstances(ownerLabel?: string): void;
147
+ export declare function evictFlowSessionWarmInstances(ownerLabel?: string, options?: {
148
+ preservePinned?: boolean;
149
+ }): void;
148
150
  /**
149
151
  * True when a walk of this path would resume on a parked instance. Activation
150
152
  * warming reads it to decide whether the frame already has a resumable
@@ -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;
@@ -6,6 +6,7 @@
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;
@@ -19,8 +20,26 @@ export declare function directInteractiveSessionSurfaceId(pageId: string, stateI
19
20
  export declare function setDirectInteractiveStateRequestHandler(handler: ((pageId: string) => void) | null): void;
20
21
  /** Starts direct-state delivery in the same task as the right-panel click. */
21
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;
22
35
  /** Exposes a runner-owned iframe without transferring its ownership. */
23
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;
24
43
  /** Releases a surface only when the caller still owns the registered iframe. */
25
44
  export declare function releaseInteractiveSessionSurface(screenId: string, iframe: HTMLIFrameElement): void;
26
45
  /** Clears a presentation slot when its page returns to an authored state. */
@@ -1,5 +1,6 @@
1
1
  import type { DesignScreenInteraction, DesignSerializable, StoryboardEnvironment } from './designImport';
2
2
  import type { PageModel } from './store';
3
+ export { reachablePreviewTestIds } from './previewReachability';
3
4
  /**
4
5
  * Screen-state axes — same-frame variations reproduced by a gesture or a boot
5
6
  * condition.
@@ -136,10 +137,10 @@ export declare function validateInteractiveStates(defs: readonly InteractiveStat
136
137
  /** Validates both interaction and condition axes. */
137
138
  export declare const validateScreenStateAxes: typeof validateInteractiveStates;
138
139
  /** Reads only the control-presence evidence needed by the view-only inspector. */
139
- export declare function previewMarkupTestIds(markup: string | null | undefined): readonly string[];
140
+ export declare function previewMarkupTestIds(markup: string | null | undefined, bodyAttributes?: string | null): readonly string[];
140
141
  /**
141
142
  * Axes available on one frame. The test-id requirement is checked against
142
- * either the imported tree or the frozen preview's lightweight test-id index.
143
+ * either the imported tree or the frozen preview's reachable test-id index.
143
144
  * View-only can therefore offer a real screen control without importing the
144
145
  * whole layer tree, while a frame without that control still hides the axis.
145
146
  */
@@ -0,0 +1,7 @@
1
+ export declare const REACHABLE_TEST_IDS_ATTRIBUTE = "data-pygmalion-reachable-testids";
2
+ /** Test ids whose rendered controls can actually be reached from this frame. */
3
+ export declare function reachablePreviewTestIds(root: ParentNode): readonly string[];
4
+ /** Stamps capture-time reachability without mutating the running document. */
5
+ export declare function stampReachablePreviewTestIds(source: Document, clone: HTMLElement): void;
6
+ /** Reads a capture-time reachability index, or null for a legacy snapshot. */
7
+ export declare function indexedReachablePreviewTestIds(markup: string): readonly string[] | null;
@@ -221,6 +221,12 @@ export interface PageModel {
221
221
  interactiveStateId?: string;
222
222
  /** The active option is rendered through host desired state, without replay. */
223
223
  interactiveStateUsesDesiredState?: boolean;
224
+ /**
225
+ * Imported layers still showing behind a direct live surface. The surface
226
+ * paints first; FrameView consumes this key and invalidates the old layers
227
+ * on the following frame so a large tree cannot block the visible switch.
228
+ */
229
+ interactiveLayerRefreshKey?: string;
224
230
  interactiveOptionId?: string;
225
231
  /**
226
232
  * Options held together on a combining axis (InteractiveStateDef.multiple).
@@ -260,7 +266,7 @@ export interface PageModel {
260
266
  assertions?: DesignScreenAssertion[];
261
267
  /** Host-declared warm instance group. Set when the screen state is declared, not replayed. */
262
268
  session?: string;
263
- /** Test ids observed in the frozen preview before editable layers are imported. */
269
+ /** Reachable test ids observed in the frozen preview before editable layers are imported. */
264
270
  previewTestIds?: readonly string[];
265
271
  layerImportCount?: number;
266
272
  layerImportTruncated?: boolean;
@@ -467,6 +473,8 @@ export declare class EditorStore {
467
473
  private pageEditRevisions;
468
474
  /** pageId → edit revision at the moment a frozen preview was promoted without a boot (D2). */
469
475
  private provisionalLayerImports;
476
+ /** Latest paint-first direct-state commit scheduled for each page. */
477
+ private directStateCommitRevisions;
470
478
  /** pageId → source geometry awaiting its post-mount fidelity measurement. */
471
479
  private pendingLayerImportGeometry;
472
480
  /** View-only motion playback never becomes part of a frame recipe or undo history. */
@@ -737,7 +745,11 @@ export declare class EditorStore {
737
745
  * Pure-CSS pseudo options short-circuit through the held-pseudo fast path
738
746
  * (applyHeldPseudoInteractiveState) and never touch the recipe.
739
747
  */
740
- applyInteractiveState(pageId: string, def: InteractiveStateDef, optionId: string): boolean;
748
+ applyInteractiveState(pageId: string, def: InteractiveStateDef, optionId: string, options?: {
749
+ skipPaintFirstPreview?: boolean;
750
+ }): boolean;
751
+ /** Invalidates retained layers only after their direct live replacement painted. */
752
+ commitInteractiveLayerRefresh(pageId: string, refreshKey: string): boolean;
741
753
  /** The root viewport endpoint currently preserved in this frame recipe. */
742
754
  frameScrollRecipe(pageId: string): Readonly<{
743
755
  x: number;
@@ -75,10 +75,13 @@ automatic duplicate. Click-driven toggles and other application states remain
75
75
  declared axes unless they already have their own review frame.
76
76
 
77
77
  In view-only mode, `requires.testId` is matched against the frozen preview's
78
- lightweight test-id index as well as an imported layer tree. This keeps right
79
- panel state controls available without turning selection into a layer import or
80
- live boot, and still hides an axis from screens whose captured DOM lacks its
81
- target.
78
+ lightweight **reachable** test-id index as well as an imported layer tree. A
79
+ test id under `display: none`, hidden visibility, zero opacity, or an inert
80
+ ancestor does not qualify merely because React kept that branch mounted. This
81
+ keeps right-panel state controls available without turning selection into a
82
+ layer import or live boot, while hiding axes that the captured frame cannot
83
+ actually reach. Legacy snapshots without a capture-time index are measured
84
+ after their inert DOM mounts.
82
85
 
83
86
  ## Automatic motion coverage
84
87
 
@@ -1056,6 +1056,56 @@ export function serializeStoryboardPreviewDocument(
1056
1056
  const clone = document.documentElement.cloneNode(true);
1057
1057
  if (!(clone instanceof HTMLElement)) return null;
1058
1058
 
1059
+ // A frozen snapshot preserves branches that are mounted but visually absent
1060
+ // (for example, two workspace panels cross-faded with opacity). Presence
1061
+ // alone therefore cannot tell the inspector which controls this captured
1062
+ // frame can actually reach. Record a compact, capture-time visibility index
1063
+ // on the body; older snapshots without it are measured again after mount.
1064
+ try {
1065
+ const reachableTestIds = new Set();
1066
+ const hasRenderedBox = (element) => {
1067
+ const rect = element.getBoundingClientRect();
1068
+ if (rect.width > 0 && rect.height > 0) return true;
1069
+ return [...element.querySelectorAll('*')].some((descendant) => {
1070
+ const descendantRect = descendant.getBoundingClientRect();
1071
+ return descendantRect.width > 0 && descendantRect.height > 0;
1072
+ });
1073
+ };
1074
+ const isReachable = (element) => {
1075
+ for (
1076
+ let current = element;
1077
+ current;
1078
+ current = current.parentElement
1079
+ ) {
1080
+ const style = window.getComputedStyle(current);
1081
+ const opacity = Number.parseFloat(style.opacity || '1');
1082
+ if (
1083
+ current.hasAttribute('hidden') ||
1084
+ current.hasAttribute('inert') ||
1085
+ style.display === 'none' ||
1086
+ style.visibility === 'hidden' ||
1087
+ style.visibility === 'collapse' ||
1088
+ (Number.isFinite(opacity) && opacity <= 0)
1089
+ ) {
1090
+ return false;
1091
+ }
1092
+ }
1093
+ return hasRenderedBox(element);
1094
+ };
1095
+ for (const element of document.documentElement.querySelectorAll('*')) {
1096
+ const testId = element.getAttribute('data-testid');
1097
+ if (testId && isReachable(element)) reachableTestIds.add(testId);
1098
+ }
1099
+ const cloneBody = clone.querySelector('body');
1100
+ cloneBody?.setAttribute(
1101
+ 'data-pygmalion-reachable-testids',
1102
+ encodeURIComponent(JSON.stringify([...reachableTestIds].sort())),
1103
+ );
1104
+ } catch {
1105
+ // Reachability metadata is additive. A constrained capture realm may not
1106
+ // implement layout; the mounted-preview fallback handles that snapshot.
1107
+ }
1108
+
1059
1109
  const sourceInputs = [...document.querySelectorAll('input')];
1060
1110
  const cloneInputs = [...clone.querySelectorAll('input')];
1061
1111
  sourceInputs.forEach((source, index) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pygmalionjs/pygmalion",
3
- "version": "0.6.28",
3
+ "version": "0.6.29",
4
4
  "description": "Code-backed DOM design sandbox and visual QA editor",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {