@pygmalionjs/pygmalion 0.5.34 → 0.6.0

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-hofdx4eP.js";
1
+ import { F as s, N as a, e as r, s as i, a as t } from "./FrozenRoutePreview-DStbZQD4.js";
2
2
  export {
3
3
  s as FrozenRoutePreviewView,
4
4
  a as NodeModel,
@@ -21,7 +21,13 @@ export interface DesignImportAsset {
21
21
  scenarioId?: string;
22
22
  description?: string;
23
23
  }
24
- export type DesignScreenInteractionAction = 'click' | 'focus' | 'fill' | 'check' | 'press' | 'wait' | 'storage';
24
+ export type DesignScreenInteractionAction = 'click' | 'focus'
25
+ /** Holds the pointer over the target through capture. */
26
+ | 'hover'
27
+ /** Holds keyboard focus, including the `:focus-visible` presentation. */
28
+ | 'focus-visible'
29
+ /** Holds the pointer-down `:active` presentation through capture. */
30
+ | 'active' | 'fill' | 'check' | 'press' | 'wait' | 'storage';
25
31
  /**
26
32
  * A critical step to capture the actual app screen after putting it in a specific user flow state.
27
33
  * A CSS selector is used first, and if text is present, the selector candidates are narrowed down to the display string.
@@ -0,0 +1,18 @@
1
+ import type { DesignScreenInteraction } from './designImport';
2
+ export type HeldPseudoState = 'hover' | 'focus' | 'focus-visible' | 'active';
3
+ /** The pseudo gestures still held after the complete recipe has run. */
4
+ export declare function heldPseudoInteractionsFromRecipe(interactions: readonly DesignScreenInteraction[]): DesignScreenInteraction[];
5
+ export declare function heldPseudoStatesFromInteractions(interactions: readonly DesignScreenInteraction[]): HeldPseudoState[];
6
+ /**
7
+ * Converts pseudo-class selectors into stable attribute selectors while
8
+ * leaving comments and quoted CSS values untouched.
9
+ */
10
+ export declare function materializeHeldPseudoCss(cssText: string, states: readonly HeldPseudoState[]): string;
11
+ export interface HeldPseudoMaterialization {
12
+ cleanup(): void;
13
+ issues: string[];
14
+ }
15
+ /** Applies the stable selector attributes that correspond to held gestures. */
16
+ export declare function applyHeldPseudoAttributes(root: ParentNode, interactions: readonly DesignScreenInteraction[]): HeldPseudoMaterialization;
17
+ /** Materializes held pseudo classes in a live same-origin preview document. */
18
+ export declare function materializeHeldPseudoDocument(document: Document, interactions: readonly DesignScreenInteraction[]): HeldPseudoMaterialization;
@@ -1,7 +1,8 @@
1
1
  import type { DesignScreenInteraction, StoryboardEnvironment } from './designImport';
2
2
  import type { PageModel } from './store';
3
3
  /**
4
- * Interactive statesaxes a screen can be put into by acting on it.
4
+ * Screen-state axessame-frame variations reproduced by a gesture or a boot
5
+ * condition.
5
6
  *
6
7
  * A collapsed sidebar or a hovered row is not a separate screen: it is the
7
8
  * same screen after one gesture, reachable on every frame that has the
@@ -61,9 +62,20 @@ export interface InteractiveStateOption {
61
62
  };
62
63
  };
63
64
  }
65
+ export type ScreenStateAxisKind = 'interaction' | 'condition';
64
66
  export interface InteractiveStateDef {
65
67
  id: string;
66
68
  label: string;
69
+ /**
70
+ * Why this variation belongs on the frame instead of becoming another frame.
71
+ *
72
+ * `interaction` is a deterministic local state reached by a gesture on the
73
+ * already booted screen. `condition` is an external state that must exist
74
+ * before the screen boots, such as an empty response, denied permission, or
75
+ * stalled request. The validator refuses mixed axes so a sample value cannot
76
+ * quietly masquerade as a review state.
77
+ */
78
+ kind: ScreenStateAxisKind;
67
79
  /**
68
80
  * Frames the axis applies to. A frame qualifies when its route matches
69
81
  * (prefix) and its imported tree carries the test id, so an axis declared
@@ -98,6 +110,12 @@ export interface InteractiveStateDiagnostic {
98
110
  interactionId: string;
99
111
  message: string;
100
112
  }
113
+ /** Preferred public name: the collection contains interaction and condition axes. */
114
+ export type ScreenStateAxisDef = InteractiveStateDef;
115
+ /** Preferred public name: an option on either kind of screen-state axis. */
116
+ export type ScreenStateOption = InteractiveStateOption;
117
+ /** Preferred public name for validation output. */
118
+ export type ScreenStateAxisDiagnostic = InteractiveStateDiagnostic;
101
119
  /**
102
120
  * Whether selecting this option asks supply for a different screen.
103
121
  *
@@ -108,12 +126,16 @@ export interface InteractiveStateDiagnostic {
108
126
  */
109
127
  export declare function optionChangesSupply(option: InteractiveStateOption): boolean;
110
128
  export declare function validateInteractiveStates(defs: readonly InteractiveStateDef[]): InteractiveStateDiagnostic[];
129
+ /** Validates both interaction and condition axes. */
130
+ export declare const validateScreenStateAxes: typeof validateInteractiveStates;
111
131
  /**
112
132
  * Axes available on one frame. The test-id requirement is checked against
113
133
  * the imported tree, so an axis stays hidden until the frame actually shows
114
134
  * the control it drives — a frame without a sidebar never offers to fold it.
115
135
  */
116
136
  export declare function interactiveStatesForPage(page: PageModel, defs: readonly InteractiveStateDef[]): InteractiveStateDef[];
137
+ /** Preferred public name for axes applicable to one frame. */
138
+ export declare const screenStateAxesForPage: typeof interactiveStatesForPage;
117
139
  /**
118
140
  * The option this frame's capture already shows, or undefined when the axis
119
141
  * declares a single base instead of witnessing each option.
@@ -126,5 +148,8 @@ export declare function capturedInteractiveOption(def: InteractiveStateDef, page
126
148
  * one option declared without steps.
127
149
  */
128
150
  export declare function baseInteractiveOption(def: InteractiveStateDef, page?: PageModel): InteractiveStateOption | undefined;
151
+ export declare function setScreenStateAxes(defs: readonly InteractiveStateDef[] | undefined): void;
152
+ /** @deprecated Use setScreenStateAxes. */
129
153
  export declare function setInteractiveStates(defs: readonly InteractiveStateDef[] | undefined): void;
130
154
  export declare function getInteractiveStates(): readonly InteractiveStateDef[];
155
+ export declare const getScreenStateAxes: typeof getInteractiveStates;
@@ -1,3 +1,4 @@
1
+ import type { DesignScreenInteraction } from './designImport';
1
2
  export interface PreparedShadowPreview {
2
3
  bodyHtml: string;
3
4
  sharedCssText: string;
@@ -28,12 +29,17 @@ export interface ShadowPreviewMountInput {
28
29
  overrideCssText?: string;
29
30
  /** Resolves relative element and CSS resource URLs. */
30
31
  baseUrl?: string;
32
+ /** Recipe whose terminal pseudo gestures must remain visible in frozen DOM. */
33
+ interactions?: readonly DesignScreenInteraction[];
31
34
  }
32
35
  export interface ShadowPreviewHandle {
33
36
  readonly root: ShadowRoot;
34
37
  readonly contentElement: HTMLElement;
35
38
  readonly sharedCssHash: string;
36
39
  readonly overrideKey: string;
40
+ readonly heldPseudoIssues: readonly string[];
41
+ /** Rebuilds held pseudo rules from external stylesheets after their links load. */
42
+ refreshLinkedHeldPseudoCss(): void;
37
43
  updateBodyHtml(bodyHtml: string, baseUrl?: string): void;
38
44
  updateOverrideCss(cssText: string): void;
39
45
  updateSharedCss(cssText: string): void;
@@ -5,7 +5,7 @@ import { type ApplyPayload, type DesignChangePayload, type DesignChangeResult, t
5
5
  import { type InspectApplyPayload } from './editor/inspect';
6
6
  import { type ScreenFlowPath } from './editor/screenFlows';
7
7
  import { type ScenarioCoverageClaim } from './editor/scenarioCoverage';
8
- import { type InteractiveStateDef } from './editor/interactiveStates';
8
+ import { type InteractiveStateDef, type ScreenStateAxisDef } from './editor/interactiveStates';
9
9
  import { type SurfaceClassification } from './editor/surfaceDemands';
10
10
  import { type ScreenDimensionDef } from './editor/screenDimensions';
11
11
  import { type FrameBranchKindDef, type FrameLabelLabels } from './editor/frameLabels';
@@ -46,8 +46,8 @@ export { applyScenarioCoverageClaim, validateScenarioCoverage, } from './editor/
46
46
  export type { ApplyCoverageResult, ScenarioCoverageClaim, ScenarioCoverageContext, ScenarioCoverageDiagnostic, } from './editor/scenarioCoverage';
47
47
  export { enumerateControlCandidates, enumerateRequestCandidates, enumerateRouteCandidates, frameSurfaceDemands, getObservedRequests, getSurfaceClassifications, MIN_SURFACE_REASON_LENGTH, requestEndpointShape, setObservedRequests, setSurfaceClassifications, unclassifiedSurfaces, validateSurfaceDemands, } from './editor/surfaceDemands';
48
48
  export type { ControlNodeLike, FrameSurfaceDemands, ObservedRequest, RouteEnumerationInput, SurfaceCandidate, SurfaceClassification, SurfaceDemandDiagnostic, SurfaceDemandKind, SurfaceVerdict, } from './editor/surfaceDemands';
49
- export { validateInteractiveStates } from './editor/interactiveStates';
50
- export type { InteractiveStateDef, InteractiveStateDiagnostic, InteractiveStateOption, } from './editor/interactiveStates';
49
+ export { validateInteractiveStates, validateScreenStateAxes, } from './editor/interactiveStates';
50
+ export type { InteractiveStateDef, InteractiveStateDiagnostic, InteractiveStateOption, ScreenStateAxisDef, ScreenStateAxisDiagnostic, ScreenStateAxisKind, ScreenStateOption, } from './editor/interactiveStates';
51
51
  export { validateScreenCards } from './editor/screenCards';
52
52
  export type { CardBadgeOption, CardSlotDef, ScreenCardDef, ScreenCardDiagnostic, } from './editor/screenCards';
53
53
  export { validateFrameBranchKinds, framesMissingScenario } from './editor/frameLabels';
@@ -210,7 +210,7 @@ export declare function pygmalionOpenPreview(): void;
210
210
  export declare function pygmalionResetCurrentEdits(): boolean;
211
211
  export interface InitialPageDef extends DesignImportInitialPage {
212
212
  }
213
- export declare function PygmalionEditor({ registry, tokens, initialPages, initialCanvas, initialEditMode, componentConnections, viewportPresets, sessionPresets, previewEnvironmentControls, designImport, onTokensChange, appOrigin, previewRevision, previewCacheNamespace, previewArtifacts, previewArtifactEndpoint, previewConcurrency, previewOpen, onPreviewOpenChange, flowCanvas, frameSurface, captureSupply, screenFlows, screenFlowConcurrency, scenarioCoverage, screenDimensions, screenLists, sectionHeaders, sectionHeaderLabels, screenLanes, frameBranchKinds, frameLabelLabels, screenCards, liveScreenBudget, interactiveStates, surfaceClassifications, onApply, onDesignChange, onInspectApply, onInspectPreview, onInspectImpact, storyboard, storyboardDiscovery, storyboardDiscoveryEndpoint, }: {
213
+ export declare function PygmalionEditor({ registry, tokens, initialPages, initialCanvas, initialEditMode, componentConnections, viewportPresets, sessionPresets, previewEnvironmentControls, designImport, onTokensChange, appOrigin, previewRevision, previewCacheNamespace, previewArtifacts, previewArtifactEndpoint, previewConcurrency, previewOpen, onPreviewOpenChange, flowCanvas, frameSurface, captureSupply, screenFlows, screenFlowConcurrency, scenarioCoverage, screenDimensions, screenLists, sectionHeaders, sectionHeaderLabels, screenLanes, frameBranchKinds, frameLabelLabels, screenCards, liveScreenBudget, screenStateAxes, interactiveStates, surfaceClassifications, onApply, onDesignChange, onInspectApply, onInspectPreview, onInspectImpact, storyboard, storyboardDiscovery, storyboardDiscoveryEndpoint, }: {
214
214
  registry?: ComponentRegistry;
215
215
  tokens?: TokenDef[];
216
216
  initialPages?: InitialPageDef[];
@@ -362,9 +362,14 @@ export declare function PygmalionEditor({ registry, tokens, initialPages, initia
362
362
  */
363
363
  liveScreenBudget?: number;
364
364
  /**
365
- * Host-declared interactive states: axes a screen reaches by acting on it
366
- * (folding the sidebar, hovering a row) rather than by being a separate
367
- * captured frame. Validate with validateInteractiveStates.
365
+ * Host-declared screen-state axes. Interaction axes replay local gestures;
366
+ * condition axes recreate external boot conditions. Both stay attached to
367
+ * the same frame instead of multiplying the canvas. Validate with
368
+ * validateScreenStateAxes.
369
+ */
370
+ screenStateAxes?: readonly ScreenStateAxisDef[];
371
+ /**
372
+ * @deprecated Use screenStateAxes. Kept as a migration bridge.
368
373
  */
369
374
  interactiveStates?: readonly InteractiveStateDef[];
370
375
  /**
@@ -81,11 +81,7 @@ export declare const FrameScreenLists: import("react").FunctionComponent<{
81
81
  export declare const FrameCardBadges: import("react").FunctionComponent<{
82
82
  page: PageModel;
83
83
  }>;
84
- /**
85
- * Interactive states — axes reachable by acting on the screen (folding the
86
- * sidebar, hovering a row). They are not sibling frames: the same frame is
87
- * re-supplied with the option's steps appended to its recipe.
88
- */
84
+ /** Same-frame states, separated by the mechanism that reproduces them. */
89
85
  export declare const FrameInteractiveStates: import("react").FunctionComponent<{
90
86
  page: PageModel;
91
87
  }>;
@@ -0,0 +1,127 @@
1
+ # Screen state contract
2
+
3
+ Pygmalion uses five review placements. A host must choose the placement from
4
+ how the variation is reproduced, not from how different its pixels look.
5
+
6
+ | Placement | Use when | Pygmalion declaration |
7
+ | --- | --- | --- |
8
+ | Frame | The variation is an independent task, journey checkpoint, structural composition, overlay context, or supported viewport that reviewers must navigate to directly. | `DesignScreenCase` |
9
+ | Interaction state | The same booted screen reaches a deterministic, reversible visual endpoint through a local gesture. | `ScreenStateAxisDef` with `kind: 'interaction'` |
10
+ | Condition state | The same screen needs external data, network, storage, permission, media, or timing conditions before it boots. | `ScreenStateAxisDef` with `kind: 'condition'` |
11
+ | Editable parameter | The variation only samples text length, row count, content, or another freely editable value and has no distinct product endpoint. | Edit mode, component props, list controls, viewport controls, or QA data |
12
+ | Behavior | The action has no stable visual endpoint to preserve. | `DesignBehaviorScenario` or an automated interaction test |
13
+
14
+ The practical test is: after reproducing the variation, is there a stable state
15
+ that another reviewer should be able to select again? If yes, use a frame or a
16
+ screen-state axis. If the answer is only “this value is longer” or “the request
17
+ was sent,” keep it in editing or QA instead.
18
+
19
+ ## Frame versus same-frame state
20
+
21
+ Preserve a separate frame when at least one of these is true:
22
+
23
+ - it is a named step in a user journey;
24
+ - it changes the screen's primary task or structural composition;
25
+ - it provides an independent review anchor that must remain visible beside
26
+ another screen;
27
+ - it is a supported viewport whose responsive composition must be compared.
28
+
29
+ Use a same-frame axis when all of these are true:
30
+
31
+ - the route, task, and main structure stay the same;
32
+ - the variation is deterministic and can be recreated from the captured base;
33
+ - choosing it does not depend on an undocumented sequence;
34
+ - the final visual state remains visible long enough to capture.
35
+
36
+ Pseudo states such as hover, keyboard focus, and pointer active are always
37
+ interaction states. They must not become duplicate frames.
38
+
39
+ ## Required axis classification
40
+
41
+ `kind` is mandatory and is enforced when the editor installs the axes.
42
+ `validateScreenStateAxes` is also exported for host validation gates.
43
+
44
+ ```ts
45
+ import type { ScreenStateAxisDef } from '@pygmalionjs/pygmalion';
46
+
47
+ export const screenStateAxes: readonly ScreenStateAxisDef[] = [
48
+ {
49
+ id: 'editable-heading',
50
+ label: 'Heading',
51
+ kind: 'interaction',
52
+ requires: { testId: 'editable-heading' },
53
+ options: [
54
+ { id: 'rest', label: 'Rest' },
55
+ {
56
+ id: 'hover',
57
+ label: 'Hover',
58
+ steps: [
59
+ {
60
+ action: 'hover',
61
+ label: 'Hover heading',
62
+ selector: '[data-testid="editable-heading"]',
63
+ },
64
+ ],
65
+ },
66
+ {
67
+ id: 'editing',
68
+ label: 'Editing',
69
+ steps: [
70
+ {
71
+ action: 'click',
72
+ label: 'Open heading editor',
73
+ selector: '[data-testid="editable-heading"]',
74
+ },
75
+ {
76
+ action: 'wait',
77
+ label: 'Heading input is visible',
78
+ selector: '[data-testid="heading-input"]',
79
+ },
80
+ ],
81
+ },
82
+ ],
83
+ },
84
+ {
85
+ id: 'results',
86
+ label: 'Results',
87
+ kind: 'condition',
88
+ options: [
89
+ { id: 'loaded', label: 'Loaded' },
90
+ {
91
+ id: 'failed',
92
+ label: 'Failed',
93
+ environment: {
94
+ network: {
95
+ conditions: [
96
+ { match: '/api/results', outcome: { kind: 'fail', status: 500 } },
97
+ ],
98
+ },
99
+ },
100
+ },
101
+ ],
102
+ },
103
+ ];
104
+
105
+ <PygmalionEditor screenStateAxes={screenStateAxes} />;
106
+ ```
107
+
108
+ An interaction option cannot declare `environment`. Every non-base condition
109
+ option must declare one. An interaction option ending in `fill` is rejected
110
+ unless a later targeted `wait` proves that the input reached a distinct visual
111
+ result. This keeps empty strings and maximum-length samples out of the state
112
+ panel while still allowing a search input to reproduce a real empty-result
113
+ state.
114
+
115
+ ## Held pseudo states
116
+
117
+ Recipes support `hover`, `focus-visible`, and `active` in addition to `focus`.
118
+ These actions remain held through capture. The browser runner dispatches the
119
+ corresponding events, the Playwright worker performs the real pointer or focus
120
+ gesture, and frozen DOM previews materialize `:hover`, `:focus`,
121
+ `:focus-visible`, `:focus-within`, and `:active` selectors as scoped data
122
+ attributes. A DOM preview therefore preserves the same pseudo-state styling as
123
+ the captured application rather than relying on the editor's current pointer.
124
+
125
+ A held pseudo action must be terminal; only `wait` steps may follow it. This
126
+ prevents a declaration from claiming a transient hover that a later click has
127
+ already replaced.
@@ -381,6 +381,13 @@ export async function runStoryboardInteraction(page, interaction) {
381
381
  await target.click({ timeout: timeoutMs });
382
382
  } else if (interaction.action === 'focus') {
383
383
  await target.focus({ timeout: timeoutMs });
384
+ } else if (interaction.action === 'hover') {
385
+ await target.hover({ timeout: timeoutMs });
386
+ } else if (interaction.action === 'focus-visible') {
387
+ await target.focus({ timeout: timeoutMs });
388
+ } else if (interaction.action === 'active') {
389
+ await target.hover({ timeout: timeoutMs });
390
+ await page.mouse.down();
384
391
  } else if (interaction.action === 'fill') {
385
392
  await target.fill(interaction.value ?? '', { timeout: timeoutMs });
386
393
  } else if (interaction.action === 'check') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pygmalionjs/pygmalion",
3
- "version": "0.5.34",
3
+ "version": "0.6.0",
4
4
  "description": "Code-backed DOM design sandbox and visual QA editor",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "files": [
34
34
  "dist-lib",
35
+ "docs/screen-state-contract.md",
35
36
  "node/component-branches.mjs",
36
37
  "node/design-session.mjs",
37
38
  "node/dev-mirror.mjs",