waygraph 0.15.25 → 0.15.26

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.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The `#wg-ring` / `#wg-ring-label` CSS (tone/size/weight variants) - the
3
+ * actual visual language `cli.ts`'s own demo stepper paints with. Extracted
4
+ * here (not left as a private `cli.ts` constant) so anything else wanting the
5
+ * SAME "waygraph vision" ring can import pure CSS text, not the rendering
6
+ * engine around it - `cli.ts` runs `main().catch(...)` unconditionally at
7
+ * module load with no `import.meta.url` guard, so importing anything from it
8
+ * is unsafe as a library dependency (see `pilot-overlay.ts`'s own doc
9
+ * comment for the same reasoning re: the badge/panel). This file has always
10
+ * been the pure, side-effect-free home for tone/size/weight - the CSS that
11
+ * renders them belongs alongside it for the same reason. `cli.ts` imports
12
+ * this constant instead of keeping its own duplicate copy.
13
+ */
14
+ export declare const WAYGRAPH_RING_CSS: string;
@@ -0,0 +1,58 @@
1
+ // Split out of the former 2,100-line highlights.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
+ /**
3
+ * The `#wg-ring` / `#wg-ring-label` CSS (tone/size/weight variants) - the
4
+ * actual visual language `cli.ts`'s own demo stepper paints with. Extracted
5
+ * here (not left as a private `cli.ts` constant) so anything else wanting the
6
+ * SAME "waygraph vision" ring can import pure CSS text, not the rendering
7
+ * engine around it - `cli.ts` runs `main().catch(...)` unconditionally at
8
+ * module load with no `import.meta.url` guard, so importing anything from it
9
+ * is unsafe as a library dependency (see `pilot-overlay.ts`'s own doc
10
+ * comment for the same reasoning re: the badge/panel). This file has always
11
+ * been the pure, side-effect-free home for tone/size/weight - the CSS that
12
+ * renders them belongs alongside it for the same reason. `cli.ts` imports
13
+ * this constant instead of keeping its own duplicate copy.
14
+ */
15
+ export const WAYGRAPH_RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
16
+ "border:2.5px solid #7C3AED;border-radius:10px;" +
17
+ // Opacity fade only - NEVER transition border-color/box-shadow. Tone swaps
18
+ // (auto gray -> planned purple -> warning yellow) must snap instantly;
19
+ // color transitions read as a muddy gray/purple/yellow morph on ring 2+.
20
+ "box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease;}" +
21
+ // Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
22
+ // Automation (verify fallback, unmatched fill/click) = gray - operators
23
+ // can ignore engine checks and watch purple + semantic tones.
24
+ "#wg-ring[data-tone=planned]{border-color:#7C3AED;box-shadow:0 0 0 4px rgba(124,58,237,.16);}" +
25
+ "#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
26
+ "#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
27
+ "#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
28
+ "#wg-ring[data-tone=danger]{border-color:#EF4444;box-shadow:0 0 0 4px rgba(239,68,68,.22);}" +
29
+ "#wg-ring[data-tone=success]{border-color:#22C55E;box-shadow:0 0 0 4px rgba(34,197,94,.22);}" +
30
+ // Real, direct user request: a visible color distinction between a real
31
+ // Playwright-driven action (auto gray, unchanged) and a pure DOM
32
+ // inspection (this - orange).
33
+ "#wg-ring[data-tone=orange]{border-color:#F97316;box-shadow:0 0 0 4px rgba(249,115,22,.22);}" +
34
+ // A real element, not a ::after pseudo-element - a pseudo-element's
35
+ // position is CSS-relative to the ring's own box (left:0 always meant
36
+ // "the ring's own left edge"), so it had no way to be clamped back onto
37
+ // screen when that box sat near a viewport edge - the label's text just
38
+ // ran off, invisibly, with no overflow guard at all. A real sibling can
39
+ // be measured (its actual rendered width) and repositioned in JS -
40
+ // pushed back onto screen, same width, never shrunk. See
41
+ // window.__wgPositionRing in cli.ts's own installOverlay.
42
+ "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
43
+ "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
44
+ "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease;}" +
45
+ "#wg-ring-label[data-tone=planned]{background:#7C3AED;color:#fff;}" +
46
+ "#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
47
+ "#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
48
+ "#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
49
+ "#wg-ring-label[data-tone=danger]{background:#DC2626;color:#fff;}" +
50
+ "#wg-ring-label[data-tone=success]{background:#16A34A;color:#fff;}" +
51
+ "#wg-ring-label[data-tone=orange]{background:#EA580C;color:#fff;}" +
52
+ // size: ring pad + label font; weight: label boldness
53
+ "#wg-ring[data-size=sm]{border-width:1.5px;border-radius:8px;}" +
54
+ "#wg-ring[data-size=lg]{border-width:4px;border-radius:12px;}" +
55
+ "#wg-ring-label[data-size=sm]{font-size:10px;line-height:1.25;padding:4px 7px;border-radius:5px;}" +
56
+ "#wg-ring-label[data-size=lg]{font-size:16px;line-height:1.35;padding:8px 14px;border-radius:9px;max-width:min(480px,85vw);}" +
57
+ "#wg-ring-label[data-weight=bold]{font-weight:800;}" +
58
+ "#wg-ring-label[data-weight=normal]{font-weight:600;}";
@@ -0,0 +1,56 @@
1
+ import type { DemoPace, FixtureDurationFields } from "./types.js";
2
+ /** Default fixture dwell when `duration: true`. */
3
+ export declare const FIXTURE_DURATION_MS_DEFAULT = 2000;
4
+ /** Default fixture dwell under `--fast` when duration is enabled. */
5
+ export declare const FIXTURE_DURATION_FAST_MS_DEFAULT = 600;
6
+ /** Default fixture dwell under demo pace `slow` (gaps / wrongs review). */
7
+ export declare const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
8
+ /** @deprecated Prefer FIXTURE_DURATION_MS_DEFAULT */
9
+ export declare const SLIDE_DURATION_MS_DEFAULT = 2000;
10
+ /** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
11
+ export declare const SLIDE_DURATION_FAST_MS_DEFAULT = 600;
12
+ export declare function normalizeDemoPace(raw: unknown): DemoPace;
13
+ /** Named / numeric pace -> scale vs normal (1 = normal). Absolute ms (>20) maps via default dwell. */
14
+ export declare function demoPaceScale(pace: DemoPace): number;
15
+ export declare function demoPaceIsBlitz(pace: DemoPace): boolean;
16
+ /** True when pace should use the short-gate / fast fixture path (not slow authored). */
17
+ export declare function demoPaceIsFast(pace: DemoPace): boolean;
18
+ export declare function demoPaceIsSlow(pace: DemoPace): boolean;
19
+ /** Autoplay / Next-gate ms for this pace (base is WAYGRAPH_AUTOPLAY_MS or 1800). */
20
+ export declare function demoPaceGateMs(pace: DemoPace, baseAutoplayMs: number): number;
21
+ /**
22
+ * Short chip for the demo panel (e.g. `2.5x`, `4500ms`, `slow`).
23
+ * Always printable - numbers speak in the UI, not only in code.
24
+ */
25
+ export declare function formatDemoPaceBadge(pace: DemoPace, baseAutoplayMs?: number): string;
26
+ /**
27
+ * Loud one-line pace description for panel + console
28
+ * (e.g. `pace 2.5x (~4500ms gates)` / `pace 4500ms gates (~2.25x)`).
29
+ */
30
+ export declare function formatDemoPaceLabel(pace: DemoPace, baseAutoplayMs?: number): string;
31
+ /**
32
+ * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
33
+ * or `null` when duration is unset (caller keeps legacy timing).
34
+ *
35
+ * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
36
+ * (`gatesFast`). `--fast` only shortens when pace is unset/normal.
37
+ */
38
+ export declare function resolveFixtureDwellMs(fixture: FixtureDurationFields, opts?: {
39
+ gatesFast?: boolean;
40
+ pace?: DemoPace;
41
+ }): number | null;
42
+ /** @deprecated Prefer {@link resolveFixtureDwellMs} */
43
+ export declare function resolveSlideDwellMs(slide: FixtureDurationFields, opts?: {
44
+ gatesFast?: boolean;
45
+ pace?: DemoPace;
46
+ }): number | null;
47
+ /**
48
+ * Effective demo pace for a step: block override > flow episode > normal.
49
+ * Opaque FF / wasFastForward always win as blitz.
50
+ */
51
+ export declare function resolveStepDemoPace(opts: {
52
+ blockPace?: DemoPace | string | number | null;
53
+ flowPace?: DemoPace | string | number | null;
54
+ fastForward?: boolean;
55
+ wasFastForward?: boolean;
56
+ }): DemoPace;
@@ -0,0 +1,184 @@
1
+ /** Default fixture dwell when `duration: true`. */
2
+ export const FIXTURE_DURATION_MS_DEFAULT = 2000;
3
+ /** Default fixture dwell under `--fast` when duration is enabled. */
4
+ export const FIXTURE_DURATION_FAST_MS_DEFAULT = 600;
5
+ /** Default fixture dwell under demo pace `slow` (gaps / wrongs review). */
6
+ export const FIXTURE_DURATION_SLOW_MS_DEFAULT = 4000;
7
+ /** @deprecated Prefer FIXTURE_DURATION_MS_DEFAULT */
8
+ export const SLIDE_DURATION_MS_DEFAULT = FIXTURE_DURATION_MS_DEFAULT;
9
+ /** @deprecated Prefer FIXTURE_DURATION_FAST_MS_DEFAULT */
10
+ export const SLIDE_DURATION_FAST_MS_DEFAULT = FIXTURE_DURATION_FAST_MS_DEFAULT;
11
+ export function normalizeDemoPace(raw) {
12
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0)
13
+ return raw;
14
+ if (typeof raw === "string") {
15
+ const t = raw.trim().toLowerCase();
16
+ if (t === "blitz" || t === "ff")
17
+ return "blitz";
18
+ if (t === "fast" || t === "quick")
19
+ return "fast";
20
+ if (t === "slow" || t === "careful" || t === "review")
21
+ return "slow";
22
+ if (t === "normal" || t === "default" || t === "1x")
23
+ return "normal";
24
+ // "2x" / "0.5x"
25
+ if (t.endsWith("x")) {
26
+ const n = Number(t.slice(0, -1));
27
+ if (Number.isFinite(n) && n > 0)
28
+ return n;
29
+ }
30
+ const n = Number(t);
31
+ if (Number.isFinite(n) && n > 0)
32
+ return n;
33
+ }
34
+ return "normal";
35
+ }
36
+ /** Named / numeric pace -> scale vs normal (1 = normal). Absolute ms (>20) maps via default dwell. */
37
+ export function demoPaceScale(pace) {
38
+ if (typeof pace === "number") {
39
+ if (pace > 20)
40
+ return Math.min(10, Math.max(0.05, pace / FIXTURE_DURATION_MS_DEFAULT));
41
+ return Math.min(10, Math.max(0.05, pace));
42
+ }
43
+ if (pace === "blitz")
44
+ return 0.05;
45
+ if (pace === "fast")
46
+ return 0.33;
47
+ if (pace === "slow")
48
+ return 2;
49
+ return 1;
50
+ }
51
+ export function demoPaceIsBlitz(pace) {
52
+ return pace === "blitz" || (typeof pace === "number" && pace > 0 && pace <= 0.1);
53
+ }
54
+ /** True when pace should use the short-gate / fast fixture path (not slow authored). */
55
+ export function demoPaceIsFast(pace) {
56
+ if (demoPaceIsBlitz(pace))
57
+ return true;
58
+ if (pace === "fast")
59
+ return true;
60
+ if (typeof pace === "number" && pace <= 20)
61
+ return pace < 0.75;
62
+ return false;
63
+ }
64
+ export function demoPaceIsSlow(pace) {
65
+ if (pace === "slow")
66
+ return true;
67
+ if (typeof pace === "number") {
68
+ if (pace > 20)
69
+ return pace >= 3000;
70
+ return pace > 1.25;
71
+ }
72
+ return false;
73
+ }
74
+ /** Autoplay / Next-gate ms for this pace (base is WAYGRAPH_AUTOPLAY_MS or 1800). */
75
+ export function demoPaceGateMs(pace, baseAutoplayMs) {
76
+ if (demoPaceIsBlitz(pace))
77
+ return Math.min(80, baseAutoplayMs);
78
+ if (typeof pace === "number" && pace > 20) {
79
+ return Math.min(30000, Math.max(80, Math.round(pace)));
80
+ }
81
+ const scale = demoPaceScale(pace);
82
+ return Math.min(30000, Math.max(80, Math.round(baseAutoplayMs * scale)));
83
+ }
84
+ /**
85
+ * Short chip for the demo panel (e.g. `2.5x`, `4500ms`, `slow`).
86
+ * Always printable - numbers speak in the UI, not only in code.
87
+ */
88
+ export function formatDemoPaceBadge(pace, baseAutoplayMs = 1800) {
89
+ const p = normalizeDemoPace(pace);
90
+ if (typeof p === "number" && p > 20)
91
+ return `${Math.round(p)}ms`;
92
+ if (typeof p === "number") {
93
+ const s = Number.isInteger(p) ? String(p) : String(Math.round(p * 100) / 100);
94
+ return `${s}x`;
95
+ }
96
+ if (p === "blitz")
97
+ return "blitz";
98
+ if (p === "fast")
99
+ return "fast";
100
+ if (p === "slow")
101
+ return "slow";
102
+ void baseAutoplayMs;
103
+ return "1x";
104
+ }
105
+ /**
106
+ * Loud one-line pace description for panel + console
107
+ * (e.g. `pace 2.5x (~4500ms gates)` / `pace 4500ms gates (~2.25x)`).
108
+ */
109
+ export function formatDemoPaceLabel(pace, baseAutoplayMs = 1800) {
110
+ const p = normalizeDemoPace(pace);
111
+ const gate = demoPaceGateMs(p, baseAutoplayMs);
112
+ const scale = demoPaceScale(p);
113
+ const scaleTxt = Number.isInteger(scale) ? String(scale) : (Math.round(scale * 100) / 100).toString();
114
+ if (typeof p === "number" && p > 20) {
115
+ return `pace ${Math.round(p)}ms gates (~${scaleTxt}x)`;
116
+ }
117
+ if (typeof p === "number") {
118
+ const s = Number.isInteger(p) ? String(p) : String(Math.round(p * 100) / 100);
119
+ return `pace ${s}x (~${gate}ms gates)`;
120
+ }
121
+ if (p === "blitz")
122
+ return `pace blitz (~${gate}ms, skip theater)`;
123
+ if (p === "fast")
124
+ return `pace fast (~${scaleTxt}x, ~${gate}ms gates)`;
125
+ if (p === "slow")
126
+ return `pace slow (~${scaleTxt}x, ~${gate}ms gates)`;
127
+ return `pace normal (1x, ~${gate}ms gates)`;
128
+ }
129
+ /**
130
+ * Resolve min dwell ms for any fixture (stub / flow fixture / yap slide),
131
+ * or `null` when duration is unset (caller keeps legacy timing).
132
+ *
133
+ * Authoring wins: an explicit slow/numeric pace is not crushed by CLI `--fast`
134
+ * (`gatesFast`). `--fast` only shortens when pace is unset/normal.
135
+ */
136
+ export function resolveFixtureDwellMs(fixture, opts) {
137
+ if (fixture.duration === undefined || fixture.duration === false)
138
+ return null;
139
+ const pace = opts?.pace !== undefined && opts.pace !== null
140
+ ? normalizeDemoPace(opts.pace)
141
+ : opts?.gatesFast
142
+ ? "fast"
143
+ : "normal";
144
+ const fastMs = typeof fixture.fastMode === "number" && Number.isFinite(fixture.fastMode) && fixture.fastMode >= 0
145
+ ? fixture.fastMode
146
+ : FIXTURE_DURATION_FAST_MS_DEFAULT;
147
+ const normalMs = fixture.duration === true
148
+ ? FIXTURE_DURATION_MS_DEFAULT
149
+ : typeof fixture.duration === "number" && Number.isFinite(fixture.duration) && fixture.duration >= 0
150
+ ? fixture.duration
151
+ : FIXTURE_DURATION_MS_DEFAULT;
152
+ if (typeof pace === "number") {
153
+ if (pace > 20)
154
+ return Math.round(pace);
155
+ return Math.max(0, Math.round(normalMs * Math.min(10, Math.max(0.05, pace))));
156
+ }
157
+ if (pace === "blitz" || pace === "fast")
158
+ return fastMs;
159
+ // CLI --fast only when author left pace at normal
160
+ if (opts?.gatesFast && pace === "normal")
161
+ return fastMs;
162
+ if (pace === "slow")
163
+ return Math.max(normalMs, FIXTURE_DURATION_SLOW_MS_DEFAULT);
164
+ return normalMs;
165
+ }
166
+ /** @deprecated Prefer {@link resolveFixtureDwellMs} */
167
+ export function resolveSlideDwellMs(slide, opts) {
168
+ return resolveFixtureDwellMs(slide, opts);
169
+ }
170
+ /**
171
+ * Effective demo pace for a step: block override > flow episode > normal.
172
+ * Opaque FF / wasFastForward always win as blitz.
173
+ */
174
+ export function resolveStepDemoPace(opts) {
175
+ if (opts.fastForward || opts.wasFastForward)
176
+ return "blitz";
177
+ if (opts.blockPace != null && opts.blockPace !== "") {
178
+ return normalizeDemoPace(opts.blockPace);
179
+ }
180
+ if (opts.flowPace != null && opts.flowPace !== "") {
181
+ return normalizeDemoPace(opts.flowPace);
182
+ }
183
+ return "normal";
184
+ }
@@ -0,0 +1,29 @@
1
+ import type { DeviceOrientation, DevicePreset, DeviceState, DeviceViewport } from "./types.js";
2
+ /** Built-in preset sizes (CSS pixels) — stored in portrait for mobile/tablet. */
3
+ export declare const DEVICE_PRESETS: Record<DevicePreset, Omit<DeviceState, "touchMode"> & {
4
+ touchMode?: boolean;
5
+ }>;
6
+ export declare function normalizeDevicePreset(raw: unknown): DevicePreset | undefined;
7
+ export declare function normalizeDeviceOrientation(raw: unknown): DeviceOrientation | undefined;
8
+ /** Infer orientation from box (square counts as landscape). */
9
+ export declare function orientationFromViewport(vp: DeviceViewport): DeviceOrientation;
10
+ /**
11
+ * Swap width/height so the viewport matches the requested orientation.
12
+ * Preserves deviceScaleFactor. No-op when already oriented.
13
+ */
14
+ export declare function applyOrientation(state: DeviceState, orientation: DeviceOrientation): DeviceState;
15
+ /** Resolve a preset name or partial state into a full DeviceState. */
16
+ export declare function resolveDeviceState(input: DevicePreset | DeviceState | DeviceViewport | undefined, touchOverride?: boolean, orientationOverride?: DeviceOrientation): DeviceState | undefined;
17
+ /**
18
+ * Carry-forward for device fixtures (mirror {@link applyTodoPhase}).
19
+ * - clear: hideDevice / clearDevice / device("desktop") with clear intent
20
+ * - set: author set device/viewport/touch this phase
21
+ * - keep: omit - preserve previous (whole episode remain)
22
+ */
23
+ export declare function applyDevicePhase(prev: DeviceState | undefined, phase: {
24
+ deviceSync?: "set" | "clear" | "keep";
25
+ device?: DeviceState;
26
+ }): {
27
+ device: DeviceState | undefined;
28
+ sync: "set" | "clear" | "keep";
29
+ };
@@ -0,0 +1,171 @@
1
+ /** Built-in preset sizes (CSS pixels) — stored in portrait for mobile/tablet. */
2
+ export const DEVICE_PRESETS = {
3
+ mobile: {
4
+ preset: "mobile",
5
+ viewport: { width: 390, height: 844, deviceScaleFactor: 2 },
6
+ isMobile: true,
7
+ hasTouch: true,
8
+ touchMode: true,
9
+ orientation: "portrait",
10
+ },
11
+ tablet: {
12
+ preset: "tablet",
13
+ viewport: { width: 768, height: 1024, deviceScaleFactor: 2 },
14
+ isMobile: true,
15
+ hasTouch: true,
16
+ touchMode: true,
17
+ orientation: "portrait",
18
+ },
19
+ desktop: {
20
+ preset: "desktop",
21
+ viewport: { width: 1280, height: 720, deviceScaleFactor: 1 },
22
+ isMobile: false,
23
+ hasTouch: false,
24
+ touchMode: false,
25
+ orientation: "landscape",
26
+ },
27
+ };
28
+ export function normalizeDevicePreset(raw) {
29
+ const s = String(raw ?? "")
30
+ .trim()
31
+ .toLowerCase();
32
+ if (s === "mobile" || s === "phone" || s === "m")
33
+ return "mobile";
34
+ if (s === "tablet" || s === "pad" || s === "t")
35
+ return "tablet";
36
+ if (s === "desktop" || s === "desk" || s === "d" || s === "main")
37
+ return "desktop";
38
+ return undefined;
39
+ }
40
+ export function normalizeDeviceOrientation(raw) {
41
+ const s = String(raw ?? "")
42
+ .trim()
43
+ .toLowerCase();
44
+ if (s === "portrait" || s === "port" || s === "tall" || s === "p")
45
+ return "portrait";
46
+ if (s === "landscape" || s === "land" || s === "wide" || s === "l")
47
+ return "landscape";
48
+ return undefined;
49
+ }
50
+ /** Infer orientation from box (square counts as landscape). */
51
+ export function orientationFromViewport(vp) {
52
+ return vp.height > vp.width ? "portrait" : "landscape";
53
+ }
54
+ /**
55
+ * Swap width/height so the viewport matches the requested orientation.
56
+ * Preserves deviceScaleFactor. No-op when already oriented.
57
+ */
58
+ export function applyOrientation(state, orientation) {
59
+ const vp = { ...state.viewport };
60
+ const current = orientationFromViewport(vp);
61
+ if (current !== orientation) {
62
+ const w = vp.width;
63
+ vp.width = vp.height;
64
+ vp.height = w;
65
+ }
66
+ return { ...state, viewport: vp, orientation };
67
+ }
68
+ /** Resolve a preset name or partial state into a full DeviceState. */
69
+ export function resolveDeviceState(input, touchOverride, orientationOverride) {
70
+ if (input === undefined)
71
+ return undefined;
72
+ let resolved;
73
+ if (typeof input === "string") {
74
+ const p = normalizeDevicePreset(input);
75
+ if (!p)
76
+ return undefined;
77
+ const base = DEVICE_PRESETS[p];
78
+ const touchMode = touchOverride !== undefined ? !!touchOverride : !!base.touchMode;
79
+ resolved = {
80
+ preset: base.preset,
81
+ viewport: { ...base.viewport },
82
+ isMobile: base.isMobile,
83
+ hasTouch: base.hasTouch || touchMode,
84
+ touchMode,
85
+ orientation: base.orientation || orientationFromViewport(base.viewport),
86
+ };
87
+ }
88
+ else if ("width" in input && "height" in input && !("preset" in input)) {
89
+ const vp = input;
90
+ const touchMode = touchOverride !== undefined ? !!touchOverride : true;
91
+ const box = {
92
+ width: Math.max(200, Math.floor(vp.width)),
93
+ height: Math.max(200, Math.floor(vp.height)),
94
+ ...(vp.deviceScaleFactor !== undefined
95
+ ? { deviceScaleFactor: vp.deviceScaleFactor }
96
+ : { deviceScaleFactor: 2 }),
97
+ };
98
+ resolved = {
99
+ preset: "mobile",
100
+ viewport: box,
101
+ isMobile: true,
102
+ hasTouch: true,
103
+ touchMode,
104
+ orientation: orientationFromViewport(box),
105
+ };
106
+ }
107
+ else {
108
+ const d = input;
109
+ const preset = normalizeDevicePreset(d.preset) || "desktop";
110
+ const base = DEVICE_PRESETS[preset];
111
+ const touchMode = touchOverride !== undefined
112
+ ? !!touchOverride
113
+ : d.touchMode !== undefined
114
+ ? !!d.touchMode
115
+ : !!base.touchMode;
116
+ const viewport = d.viewport
117
+ ? {
118
+ width: Math.max(200, Math.floor(d.viewport.width)),
119
+ height: Math.max(200, Math.floor(d.viewport.height)),
120
+ ...(d.viewport.deviceScaleFactor !== undefined
121
+ ? { deviceScaleFactor: d.viewport.deviceScaleFactor }
122
+ : base.viewport.deviceScaleFactor !== undefined
123
+ ? { deviceScaleFactor: base.viewport.deviceScaleFactor }
124
+ : {}),
125
+ }
126
+ : { ...base.viewport };
127
+ resolved = {
128
+ preset,
129
+ viewport,
130
+ isMobile: d.isMobile !== undefined ? !!d.isMobile : base.isMobile,
131
+ hasTouch: d.hasTouch !== undefined ? !!d.hasTouch : base.hasTouch || touchMode,
132
+ touchMode,
133
+ orientation: normalizeDeviceOrientation(d.orientation) ||
134
+ orientationFromViewport(viewport),
135
+ };
136
+ }
137
+ if (!resolved)
138
+ return undefined;
139
+ const want = orientationOverride ||
140
+ (typeof input === "object" && input && "orientation" in input
141
+ ? normalizeDeviceOrientation(input.orientation)
142
+ : undefined);
143
+ if (want)
144
+ return applyOrientation(resolved, want);
145
+ if (!resolved.orientation) {
146
+ resolved = {
147
+ ...resolved,
148
+ orientation: orientationFromViewport(resolved.viewport),
149
+ };
150
+ }
151
+ return resolved;
152
+ }
153
+ /**
154
+ * Carry-forward for device fixtures (mirror {@link applyTodoPhase}).
155
+ * - clear: hideDevice / clearDevice / device("desktop") with clear intent
156
+ * - set: author set device/viewport/touch this phase
157
+ * - keep: omit - preserve previous (whole episode remain)
158
+ */
159
+ export function applyDevicePhase(prev, phase) {
160
+ const sync = phase.deviceSync ||
161
+ (phase.device ? "set" : "keep");
162
+ if (sync === "clear") {
163
+ return { device: resolveDeviceState("desktop", false), sync: "clear" };
164
+ }
165
+ if (sync === "set") {
166
+ if (phase.device)
167
+ return { device: phase.device, sync: "set" };
168
+ return { device: resolveDeviceState("desktop", false), sync: "clear" };
169
+ }
170
+ return { device: prev, sync: "keep" };
171
+ }
@@ -0,0 +1,8 @@
1
+ import type { HighlightStubPhase, HighlightStubPhaseName, HighlightStubPhaseOrFn, ResolvedHighlight, WaygraphHighlightFixture } from "./types.js";
2
+ import type { Block, Checkpoint } from "../types.js";
3
+ export declare function isStubPhase(v: unknown): v is HighlightStubPhase;
4
+ export declare function applyDefaultZoom(slots: ResolvedHighlight[], defaultZoom?: number): ResolvedHighlight[];
5
+ export declare function applyDefaultZoomOut(slots: ResolvedHighlight[], defaultZoomOut?: boolean): ResolvedHighlight[];
6
+ export declare function mergePhaseMaps(phaseMap: HighlightStubPhase, fixturePhase: Record<string, WaygraphHighlightFixture> | undefined): ResolvedHighlight[];
7
+ export declare function readStubRaw(block: Block<any, any>, phase: HighlightStubPhaseName): HighlightStubPhaseOrFn<any> | undefined;
8
+ export declare function applyLegacyHighlightsShim(block: Block<any, any>, phase: HighlightStubPhaseName, phaseMap: HighlightStubPhase, out?: Checkpoint<string>): HighlightStubPhase;