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,173 @@
1
+ import { normalizeHighlightSize, normalizeHighlightTone, normalizeHighlightWeight } from "./style.js";
2
+ export function isStubPhase(v) {
3
+ return !!v && typeof v === "object" && !Array.isArray(v);
4
+ }
5
+ function pickDurationFields(base, patch) {
6
+ const out = {};
7
+ const duration = patch?.duration !== undefined ? patch.duration : base?.duration;
8
+ const fastMode = patch?.fastMode !== undefined ? patch.fastMode : base?.fastMode;
9
+ if (duration !== undefined)
10
+ out.duration = duration;
11
+ if (fastMode !== undefined)
12
+ out.fastMode = fastMode;
13
+ return out;
14
+ }
15
+ function pickStyleFields(base, patch) {
16
+ const out = {};
17
+ const tone = patch?.tone !== undefined ? patch.tone : base?.tone;
18
+ const size = patch?.size !== undefined ? patch.size : base?.size;
19
+ const weight = patch?.weight !== undefined ? patch.weight : base?.weight;
20
+ if (tone !== undefined)
21
+ out.tone = normalizeHighlightTone(tone);
22
+ if (size !== undefined)
23
+ out.size = normalizeHighlightSize(size);
24
+ if (weight !== undefined)
25
+ out.weight = normalizeHighlightWeight(weight);
26
+ return out;
27
+ }
28
+ function pickZoom(base, patch) {
29
+ const zoom = patch?.zoom !== undefined ? patch.zoom : base?.zoom;
30
+ if (zoom !== undefined && Number.isFinite(zoom) && zoom > 0)
31
+ return { zoom };
32
+ return {};
33
+ }
34
+ function pickFxFields(base, patch) {
35
+ const out = {};
36
+ const focus = patch?.focus !== undefined ? patch.focus : base?.focus;
37
+ const color = patch?.color !== undefined ? patch.color : base?.color;
38
+ const cursor = patch?.cursor !== undefined ? patch.cursor : base?.cursor;
39
+ const todo = patch?.todo !== undefined ? patch.todo : base?.todo;
40
+ const gesture = patch?.gesture !== undefined ? patch.gesture : base?.gesture;
41
+ if (focus !== undefined)
42
+ out.focus = !!focus;
43
+ if (color !== undefined && String(color).trim())
44
+ out.color = String(color).trim();
45
+ if (cursor !== undefined)
46
+ out.cursor = !!cursor;
47
+ if (todo !== undefined && String(todo).trim())
48
+ out.todo = String(todo).trim();
49
+ if (gesture === "tap" || gesture === "hold" || gesture === "click" || gesture === "swipe") {
50
+ out.gesture = gesture;
51
+ }
52
+ return out;
53
+ }
54
+ function mergeSlot(base, patch) {
55
+ if (!base && !patch)
56
+ return null;
57
+ const dwell = pickDurationFields(base, patch);
58
+ const style = pickStyleFields(base, patch);
59
+ const zoom = pickZoom(base, patch);
60
+ const fx = pickFxFields(base, patch);
61
+ if (!base && patch) {
62
+ if (!patch.selector)
63
+ return null;
64
+ return {
65
+ selector: patch.selector,
66
+ label: patch.label,
67
+ ...(patch.detail ? { detail: patch.detail } : {}),
68
+ ...(patch.tag ? { tag: patch.tag } : {}),
69
+ ...style,
70
+ ...dwell,
71
+ ...zoom,
72
+ ...fx,
73
+ };
74
+ }
75
+ if (base && !patch) {
76
+ return {
77
+ ...base,
78
+ ...(base.tone ? { tone: normalizeHighlightTone(base.tone) } : {}),
79
+ ...(base.size ? { size: normalizeHighlightSize(base.size) } : {}),
80
+ ...(base.weight ? { weight: normalizeHighlightWeight(base.weight) } : {}),
81
+ ...pickZoom(base, undefined),
82
+ ...pickFxFields(base, undefined),
83
+ };
84
+ }
85
+ return {
86
+ selector: patch.selector ?? base.selector,
87
+ label: patch.label,
88
+ ...(patch.detail !== undefined
89
+ ? { detail: patch.detail }
90
+ : base.detail
91
+ ? { detail: base.detail }
92
+ : {}),
93
+ ...(patch.tag !== undefined ? { tag: patch.tag } : base.tag ? { tag: base.tag } : {}),
94
+ ...style,
95
+ ...dwell,
96
+ ...zoom,
97
+ ...fx,
98
+ };
99
+ }
100
+ export function applyDefaultZoom(slots, defaultZoom) {
101
+ if (defaultZoom === undefined || !(defaultZoom > 0))
102
+ return slots;
103
+ return slots.map((h) => (h.zoom !== undefined && h.zoom > 0 ? h : { ...h, zoom: defaultZoom }));
104
+ }
105
+ export function applyDefaultZoomOut(slots, defaultZoomOut) {
106
+ if (defaultZoomOut === undefined)
107
+ return slots;
108
+ return slots.map((h) => h.zoomOut !== undefined ? h : { ...h, zoomOut: defaultZoomOut });
109
+ }
110
+ export function mergePhaseMaps(phaseMap, fixturePhase) {
111
+ const phaseKeys = Object.keys(phaseMap);
112
+ const fixtureKeys = Object.keys(fixturePhase || {});
113
+ const allKeys = [...new Set([...phaseKeys, ...fixtureKeys])];
114
+ const allNumeric = allKeys.length > 0 && allKeys.every((k) => Number.isFinite(Number(k)) && String(Number(k)) === k);
115
+ // Numeric stub ids (legacy "0","1") sort by number. Named stubs keep author
116
+ // insertion order - localeCompare used to reorder password/submit/username
117
+ // and advance the wrong todo index on fill.
118
+ const ordered = allNumeric
119
+ ? [...allKeys].sort((a, b) => Number(a) - Number(b))
120
+ : [...phaseKeys, ...fixtureKeys.filter((k) => !phaseKeys.includes(k))];
121
+ const resolved = [];
122
+ for (const id of ordered) {
123
+ const merged = mergeSlot(phaseMap[id], fixturePhase?.[id]);
124
+ if (merged) {
125
+ resolved.push({
126
+ ...merged,
127
+ slotId: id,
128
+ ...(merged.todo ? { todo: merged.todo } : {}),
129
+ });
130
+ }
131
+ }
132
+ return resolved;
133
+ }
134
+ function phaseFromLegacyHighlights(highlights) {
135
+ const out = {};
136
+ if (!highlights)
137
+ return out;
138
+ highlights.forEach((h, i) => {
139
+ if (h && typeof h.selector === "string" && typeof h.label === "string") {
140
+ out[String(i)] = {
141
+ selector: h.selector,
142
+ label: h.label,
143
+ ...(h.tone ? { tone: normalizeHighlightTone(h.tone) } : {}),
144
+ };
145
+ }
146
+ });
147
+ return out;
148
+ }
149
+ export function readStubRaw(block, phase) {
150
+ const instr = block.instruction;
151
+ if (phase === "stubBefore")
152
+ return instr.stubBefore;
153
+ if (phase === "stubAfter")
154
+ return instr.stubAfter;
155
+ return instr.stubOnError;
156
+ }
157
+ export function applyLegacyHighlightsShim(block, phase, phaseMap, out) {
158
+ if (phase !== "stubAfter" || Object.keys(phaseMap).length > 0)
159
+ return phaseMap;
160
+ const instr = block.instruction;
161
+ if (!instr.highlights)
162
+ return phaseMap;
163
+ let legacy = instr.highlights;
164
+ if (typeof legacy === "function") {
165
+ try {
166
+ legacy = legacy(out ?? { __state: "" });
167
+ }
168
+ catch {
169
+ legacy = [];
170
+ }
171
+ }
172
+ return phaseFromLegacyHighlights(Array.isArray(legacy) ? legacy : []);
173
+ }
@@ -0,0 +1,43 @@
1
+ import type { HighlightFixtureMap, HighlightStubPhaseName, ResolvedHighlight, StubPhaseResult, WaygraphSlide } from "./types.js";
2
+ import type { Block, Checkpoint } from "../types.js";
3
+ import type { MemPage } from "../mem-page.js";
4
+ /**
5
+ * Run an open stub lifecycle (preferred) or resolve a static slot map.
6
+ * Returns rings + episode fixtures (todos / todoIndex / zoom).
7
+ *
8
+ * @example
9
+ * const before = await runStubPhase(block, "stubBefore", { fixtures });
10
+ * // before.todos, before.highlights
11
+ */
12
+ export declare function runStubPhase(block: Block<any, any>, phase: HighlightStubPhaseName, opts?: {
13
+ out?: Checkpoint<string>;
14
+ error?: unknown;
15
+ mem?: MemPage;
16
+ fixtures?: HighlightFixtureMap;
17
+ }): Promise<StubPhaseResult>;
18
+ /**
19
+ * Sync resolve of highlight slots. Prefer {@link runStubPhase} for open
20
+ * async lifecycles + episode fixtures (todos/zoom).
21
+ */
22
+ export declare function resolveHighlightSlots(block: Block<any, any>, phase: HighlightStubPhaseName, opts?: {
23
+ out?: Checkpoint<string>;
24
+ fixtures?: HighlightFixtureMap;
25
+ }): ResolvedHighlight[];
26
+ /**
27
+ * Resolve multi-step yap slides: flow fixture `slides` wins when present,
28
+ * else `instruction.slides` (function form allowed).
29
+ */
30
+ export declare function resolveSlides(block: Block<any, any>, opts?: {
31
+ out?: Checkpoint<string>;
32
+ fixtures?: HighlightFixtureMap;
33
+ }): WaygraphSlide[];
34
+ /**
35
+ * True when the block authored stubAfter (lifecycle fn, slots, fixtures, or legacy highlights).
36
+ * When true, demo should not use verify-trait fallback.
37
+ */
38
+ export declare function hasAuthoredStubAfter(block: Block<any, any>, out?: Checkpoint<string>, fixtures?: HighlightFixtureMap): boolean;
39
+ /**
40
+ * True when stubOnError is authored (lifecycle fn and/or rings).
41
+ * Demo fail path only - success never consults this.
42
+ */
43
+ export declare function hasAuthoredStubOnError(block: Block<any, any>, fixtures?: HighlightFixtureMap): boolean;
@@ -0,0 +1,211 @@
1
+ import { createStubCtx, liftLegacySlotTodos } from "./stub-ctx.js";
2
+ import { applyDefaultZoom, applyDefaultZoomOut, applyLegacyHighlightsShim, isStubPhase, mergePhaseMaps, readStubRaw } from "./merge.js";
3
+ import { buildTodoDock, normalizeTodoStyle, normalizeTodos } from "./todo-dock.js";
4
+ import { resolveDeviceState } from "./device.js";
5
+ /**
6
+ * Run an open stub lifecycle (preferred) or resolve a static slot map.
7
+ * Returns rings + episode fixtures (todos / todoIndex / zoom).
8
+ *
9
+ * @example
10
+ * const before = await runStubPhase(block, "stubBefore", { fixtures });
11
+ * // before.todos, before.highlights
12
+ */
13
+ export async function runStubPhase(block, phase, opts) {
14
+ const bag = { highlights: {} };
15
+ const ctx = createStubCtx(bag, {
16
+ ...(opts?.out !== undefined ? { out: opts.out } : {}),
17
+ ...(opts?.error !== undefined ? { error: opts.error } : {}),
18
+ ...(opts?.mem !== undefined ? { mem: opts.mem } : {}),
19
+ });
20
+ const raw = readStubRaw(block, phase);
21
+ if (typeof raw === "function") {
22
+ try {
23
+ const ret = await raw(ctx);
24
+ if (isStubPhase(ret)) {
25
+ bag.highlights = { ...bag.highlights, ...ret };
26
+ }
27
+ }
28
+ catch {
29
+ /* best-effort */
30
+ }
31
+ // Compat: legacy `(out) => map` that expected Checkpoint (__state on arg)
32
+ if (Object.keys(bag.highlights).length === 0 && !bag.todos) {
33
+ try {
34
+ const legacyOut = (opts?.out ?? { __state: "" });
35
+ const ret2 = raw(legacyOut);
36
+ if (isStubPhase(ret2))
37
+ bag.highlights = { ...ret2 };
38
+ }
39
+ catch {
40
+ /* ignore */
41
+ }
42
+ }
43
+ }
44
+ else if (isStubPhase(raw)) {
45
+ bag.highlights = { ...raw };
46
+ }
47
+ bag.highlights = applyLegacyHighlightsShim(block, phase, bag.highlights, opts?.out);
48
+ liftLegacySlotTodos(bag);
49
+ const fixturePhase = opts?.fixtures?.[block.name]?.[phase];
50
+ let highlights = mergePhaseMaps(bag.highlights, fixturePhase);
51
+ highlights = applyDefaultZoom(highlights, bag.zoom);
52
+ highlights = applyDefaultZoomOut(highlights, bag.zoomOut);
53
+ const todoDock = bag.hideTodos
54
+ ? undefined
55
+ : buildTodoDock({
56
+ todos: bag.todos,
57
+ todoIndex: bag.todoIndex,
58
+ todoTitle: bag.todoTitle,
59
+ todoId: bag.todoId,
60
+ todoStyle: bag.todoStyle,
61
+ todoGroups: bag.todoGroups,
62
+ todoPos: bag.todoPos,
63
+ });
64
+ const todos = todoDock?.groups[0]?.items ||
65
+ (bag.hideTodos
66
+ ? []
67
+ : normalizeTodos(bag.todos, bag.todoIndex, normalizeTodoStyle(bag.todoStyle) || "sequential"));
68
+ // Omit todos entirely => keep previous dock. Explicit empty / hideTodos => clear.
69
+ const todoSync = bag.hideTodos
70
+ ? "clear"
71
+ : bag.todoTouched && todoDock
72
+ ? "set"
73
+ : bag.todoTouched && !todoDock
74
+ ? "clear"
75
+ : "keep";
76
+ let deviceState;
77
+ if (bag.hideDevice) {
78
+ deviceState = resolveDeviceState("desktop", false);
79
+ }
80
+ else if (bag.device) {
81
+ deviceState = bag.touchOverride !== undefined
82
+ ? {
83
+ ...bag.device,
84
+ touchMode: bag.touchOverride,
85
+ hasTouch: bag.device.hasTouch || bag.touchOverride,
86
+ }
87
+ : bag.device;
88
+ }
89
+ else if (bag.touchOverride !== undefined) {
90
+ deviceState = resolveDeviceState("desktop", bag.touchOverride);
91
+ }
92
+ const deviceSync = bag.hideDevice
93
+ ? "clear"
94
+ : bag.deviceTouched && deviceState
95
+ ? "set"
96
+ : bag.deviceTouched && !deviceState
97
+ ? "clear"
98
+ : "keep";
99
+ return {
100
+ highlights,
101
+ todos,
102
+ todoSync,
103
+ deviceSync,
104
+ ...(bag.todoIndex !== undefined ? { todoIndex: bag.todoIndex } : {}),
105
+ ...(todoDock ? { todoDock } : {}),
106
+ ...(deviceState ? { device: deviceState } : {}),
107
+ ...(bag.zoom !== undefined ? { zoom: bag.zoom } : {}),
108
+ ...(bag.zoomOut !== undefined ? { zoomOut: bag.zoomOut } : {}),
109
+ ...(bag.title !== undefined && bag.title !== "" ? { title: bag.title } : {}),
110
+ ...(bag.todoPos !== undefined ? { todoPos: bag.todoPos } : {}),
111
+ ...(bag.todoDockUi !== undefined ? { todoDockUi: bag.todoDockUi } : {}),
112
+ ...(bag.todoParallel === true ? { todoParallel: true } : {}),
113
+ };
114
+ }
115
+ /**
116
+ * Sync resolve of highlight slots. Prefer {@link runStubPhase} for open
117
+ * async lifecycles + episode fixtures (todos/zoom).
118
+ */
119
+ export function resolveHighlightSlots(block, phase, opts) {
120
+ const bag = { highlights: {} };
121
+ const raw = readStubRaw(block, phase);
122
+ if (typeof raw === "function") {
123
+ try {
124
+ const ctx = createStubCtx(bag, {
125
+ ...(opts?.out !== undefined ? { out: opts.out } : {}),
126
+ });
127
+ const ret = raw(ctx);
128
+ if (isStubPhase(ret)) {
129
+ bag.highlights = { ...bag.highlights, ...ret };
130
+ }
131
+ }
132
+ catch {
133
+ try {
134
+ const legacyOut = (opts?.out ?? { __state: "" });
135
+ const ret2 = raw(legacyOut);
136
+ if (isStubPhase(ret2))
137
+ bag.highlights = { ...ret2 };
138
+ }
139
+ catch {
140
+ bag.highlights = {};
141
+ }
142
+ }
143
+ }
144
+ else if (isStubPhase(raw)) {
145
+ bag.highlights = { ...raw };
146
+ }
147
+ bag.highlights = applyLegacyHighlightsShim(block, phase, bag.highlights, opts?.out);
148
+ liftLegacySlotTodos(bag);
149
+ const fixturePhase = opts?.fixtures?.[block.name]?.[phase];
150
+ return applyDefaultZoom(mergePhaseMaps(bag.highlights, fixturePhase), bag.zoom);
151
+ }
152
+ /**
153
+ * Resolve multi-step yap slides: flow fixture `slides` wins when present,
154
+ * else `instruction.slides` (function form allowed).
155
+ */
156
+ export function resolveSlides(block, opts) {
157
+ const fromFixture = opts?.fixtures?.[block.name]?.slides;
158
+ if (fromFixture && fromFixture.length > 0) {
159
+ return [...fromFixture];
160
+ }
161
+ const instr = block.instruction;
162
+ let raw = instr.slides;
163
+ if (typeof raw === "function") {
164
+ try {
165
+ raw = raw(opts?.out ?? { __state: "" });
166
+ }
167
+ catch {
168
+ raw = [];
169
+ }
170
+ }
171
+ if (!Array.isArray(raw))
172
+ return [];
173
+ return raw.filter((s) => s && typeof s.caption === "string" && s.caption.length > 0);
174
+ }
175
+ function stubPhaseLooksAuthored(block, phase, fixtures) {
176
+ const raw = readStubRaw(block, phase);
177
+ if (typeof raw === "function")
178
+ return true;
179
+ if (isStubPhase(raw) && Object.keys(raw).length > 0)
180
+ return true;
181
+ const fx = fixtures?.[block.name]?.[phase];
182
+ if (fx && Object.keys(fx).length > 0)
183
+ return true;
184
+ return false;
185
+ }
186
+ /**
187
+ * True when the block authored stubAfter (lifecycle fn, slots, fixtures, or legacy highlights).
188
+ * When true, demo should not use verify-trait fallback.
189
+ */
190
+ export function hasAuthoredStubAfter(block, out, fixtures) {
191
+ if (stubPhaseLooksAuthored(block, "stubAfter", fixtures))
192
+ return true;
193
+ const instr = block.instruction;
194
+ if (instr.highlights)
195
+ return true;
196
+ return resolveHighlightSlots(block, "stubAfter", {
197
+ ...(out !== undefined ? { out } : {}),
198
+ ...(fixtures !== undefined ? { fixtures } : {}),
199
+ }).length > 0;
200
+ }
201
+ /**
202
+ * True when stubOnError is authored (lifecycle fn and/or rings).
203
+ * Demo fail path only - success never consults this.
204
+ */
205
+ export function hasAuthoredStubOnError(block, fixtures) {
206
+ if (stubPhaseLooksAuthored(block, "stubOnError", fixtures))
207
+ return true;
208
+ return (resolveHighlightSlots(block, "stubOnError", {
209
+ ...(fixtures !== undefined ? { fixtures } : {}),
210
+ }).length > 0);
211
+ }
@@ -0,0 +1,10 @@
1
+ import type { StubBagState, StubCtx } from "./types.js";
2
+ import type { Checkpoint } from "../types.js";
3
+ import type { MemPage } from "../mem-page.js";
4
+ export declare function createStubCtx<Out extends Checkpoint<string>>(bag: StubBagState, opts: {
5
+ out?: Out;
6
+ error?: unknown;
7
+ mem?: MemPage;
8
+ }): StubCtx<Out>;
9
+ /** Lift legacy per-slot todos (0.12.23) onto the phase bag once. */
10
+ export declare function liftLegacySlotTodos(bag: StubBagState): void;