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,281 @@
1
+ import { normalizeTodoPos, normalizeTodoStyle } from "./todo-dock.js";
2
+ import { applyOrientation, normalizeDeviceOrientation, resolveDeviceState } from "./device.js";
3
+ export function createStubCtx(bag, opts) {
4
+ return {
5
+ mem: opts.mem,
6
+ out: opts.out,
7
+ error: opts.error,
8
+ highlights(slots) {
9
+ bag.highlights = { ...(slots || {}) };
10
+ },
11
+ ring(id, stub) {
12
+ bag.highlights = { ...bag.highlights, [id]: stub };
13
+ },
14
+ todos(a, b, c) {
15
+ bag.todoTouched = true;
16
+ bag.hideTodos = false;
17
+ delete bag.todoGroups;
18
+ if (typeof a === "string") {
19
+ const id = String(a).trim();
20
+ if (id)
21
+ bag.todoId = id;
22
+ else
23
+ delete bag.todoId;
24
+ const items = (Array.isArray(b) ? b : []);
25
+ bag.todos = items;
26
+ const opts = (c && typeof c === "object" ? c : {});
27
+ if (opts.title != null)
28
+ bag.todoTitle = String(opts.title);
29
+ if (opts.index != null)
30
+ bag.todoIndex = Number(opts.index);
31
+ const st = normalizeTodoStyle(opts.style);
32
+ if (st)
33
+ bag.todoStyle = st;
34
+ const pos = normalizeTodoPos(opts.pos);
35
+ if (pos)
36
+ bag.todoPos = pos;
37
+ bag.todoParallel = opts.parallel === true;
38
+ if (!items.length)
39
+ bag.hideTodos = true;
40
+ return;
41
+ }
42
+ bag.todos = a;
43
+ // Legacy list-only form: leave parallel unset (CLI treats as replace).
44
+ delete bag.todoParallel;
45
+ if (!a || !a.length)
46
+ bag.hideTodos = true;
47
+ },
48
+ todoIndex(n) {
49
+ bag.todoIndex = n;
50
+ },
51
+ todoTitle(text) {
52
+ bag.todoTitle = String(text ?? "");
53
+ },
54
+ todoId(id) {
55
+ bag.todoTouched = true;
56
+ const cleaned = String(id ?? "").trim();
57
+ if (cleaned)
58
+ bag.todoId = cleaned;
59
+ else
60
+ delete bag.todoId;
61
+ },
62
+ todoStyle(style) {
63
+ const s = normalizeTodoStyle(style);
64
+ if (s)
65
+ bag.todoStyle = s;
66
+ },
67
+ todoGroups(groups) {
68
+ bag.todoTouched = true;
69
+ bag.hideTodos = false;
70
+ bag.todoGroups = groups;
71
+ if (!groups || !groups.length)
72
+ bag.hideTodos = true;
73
+ },
74
+ hideTodos() {
75
+ bag.todoTouched = true;
76
+ bag.hideTodos = true;
77
+ bag.todos = [];
78
+ delete bag.todoGroups;
79
+ },
80
+ clearTodos() {
81
+ bag.todoTouched = true;
82
+ bag.hideTodos = true;
83
+ bag.todos = [];
84
+ delete bag.todoGroups;
85
+ },
86
+ device(preset) {
87
+ bag.deviceTouched = true;
88
+ bag.hideDevice = false;
89
+ const resolved = resolveDeviceState(preset, bag.touchOverride, bag.orientationOverride);
90
+ if (resolved) {
91
+ bag.device = resolved;
92
+ if (resolved.preset === "desktop" && !resolved.touchMode) {
93
+ bag.hideDevice = true;
94
+ }
95
+ }
96
+ },
97
+ viewport(box) {
98
+ bag.deviceTouched = true;
99
+ bag.hideDevice = false;
100
+ const resolved = resolveDeviceState(box, bag.touchOverride, bag.orientationOverride);
101
+ if (resolved)
102
+ bag.device = resolved;
103
+ },
104
+ touch(on) {
105
+ bag.deviceTouched = true;
106
+ const enabled = on === undefined ? true : !!on;
107
+ bag.touchOverride = enabled;
108
+ if (enabled)
109
+ bag.hideDevice = false;
110
+ if (bag.device) {
111
+ bag.device = {
112
+ ...bag.device,
113
+ touchMode: enabled,
114
+ hasTouch: bag.device.hasTouch || enabled,
115
+ };
116
+ if (enabled)
117
+ bag.hideDevice = false;
118
+ }
119
+ else if (enabled) {
120
+ const base = resolveDeviceState("desktop", true, bag.orientationOverride);
121
+ if (base)
122
+ bag.device = { ...base, touchMode: true, hasTouch: true };
123
+ }
124
+ },
125
+ orientation(o) {
126
+ const want = normalizeDeviceOrientation(o);
127
+ if (!want)
128
+ return;
129
+ bag.deviceTouched = true;
130
+ bag.hideDevice = false;
131
+ bag.orientationOverride = want;
132
+ const base = bag.device ||
133
+ resolveDeviceState("mobile", bag.touchOverride) ||
134
+ resolveDeviceState("mobile");
135
+ if (base)
136
+ bag.device = applyOrientation(base, want);
137
+ },
138
+ landscape() {
139
+ this.orientation("landscape");
140
+ },
141
+ portrait() {
142
+ this.orientation("portrait");
143
+ },
144
+ hideDevice() {
145
+ bag.deviceTouched = true;
146
+ bag.hideDevice = true;
147
+ bag.device = resolveDeviceState("desktop", false);
148
+ bag.touchOverride = false;
149
+ bag.orientationOverride = undefined;
150
+ },
151
+ clearDevice() {
152
+ bag.deviceTouched = true;
153
+ bag.hideDevice = true;
154
+ bag.device = resolveDeviceState("desktop", false);
155
+ bag.touchOverride = false;
156
+ bag.orientationOverride = undefined;
157
+ },
158
+ zoom(n) {
159
+ if (Number.isFinite(n) && n > 0)
160
+ bag.zoom = n;
161
+ },
162
+ zoomOut(keep) {
163
+ bag.zoomOut = !!keep;
164
+ },
165
+ title(text) {
166
+ bag.title = String(text ?? "");
167
+ },
168
+ banner(text) {
169
+ bag.title = String(text ?? "");
170
+ },
171
+ todoPos(side) {
172
+ const p = normalizeTodoPos(side);
173
+ if (p)
174
+ bag.todoPos = p;
175
+ },
176
+ todoDockUi(opts) {
177
+ if (!opts || typeof opts !== "object")
178
+ return;
179
+ bag.todoDockUi = { ...(bag.todoDockUi || {}), ...opts };
180
+ },
181
+ todoDockFull() {
182
+ bag.todoDockUi = {
183
+ compact: false,
184
+ collision: false,
185
+ behindRing: false,
186
+ };
187
+ },
188
+ set(partial) {
189
+ if (partial.highlights)
190
+ bag.highlights = { ...partial.highlights };
191
+ if (partial.todos !== undefined) {
192
+ bag.todoTouched = true;
193
+ bag.hideTodos = !partial.todos || !partial.todos.length;
194
+ bag.todos = partial.todos;
195
+ if (partial.todoGroups === undefined)
196
+ delete bag.todoGroups;
197
+ }
198
+ if (partial.todoIndex !== undefined)
199
+ bag.todoIndex = partial.todoIndex;
200
+ if (partial.todoTitle !== undefined)
201
+ bag.todoTitle = String(partial.todoTitle ?? "");
202
+ if (partial.todoId !== undefined) {
203
+ bag.todoTouched = true;
204
+ const cleaned = String(partial.todoId ?? "").trim();
205
+ if (cleaned)
206
+ bag.todoId = cleaned;
207
+ else
208
+ delete bag.todoId;
209
+ }
210
+ const ts = normalizeTodoStyle(partial.todoStyle);
211
+ if (ts)
212
+ bag.todoStyle = ts;
213
+ if (partial.todoGroups !== undefined) {
214
+ bag.todoTouched = true;
215
+ bag.hideTodos = !partial.todoGroups || !partial.todoGroups.length;
216
+ bag.todoGroups = partial.todoGroups;
217
+ }
218
+ if (partial.touch !== undefined) {
219
+ bag.deviceTouched = true;
220
+ bag.touchOverride = !!partial.touch;
221
+ }
222
+ if (partial.orientation !== undefined) {
223
+ const want = normalizeDeviceOrientation(partial.orientation);
224
+ if (want) {
225
+ bag.deviceTouched = true;
226
+ bag.hideDevice = false;
227
+ bag.orientationOverride = want;
228
+ const base = bag.device ||
229
+ resolveDeviceState("mobile", bag.touchOverride) ||
230
+ resolveDeviceState("mobile");
231
+ if (base)
232
+ bag.device = applyOrientation(base, want);
233
+ }
234
+ }
235
+ if (partial.device !== undefined) {
236
+ bag.deviceTouched = true;
237
+ bag.hideDevice = false;
238
+ const resolved = resolveDeviceState(partial.device, bag.touchOverride, bag.orientationOverride);
239
+ if (resolved) {
240
+ bag.device = resolved;
241
+ if (resolved.preset === "desktop" && !resolved.touchMode)
242
+ bag.hideDevice = true;
243
+ }
244
+ }
245
+ if (partial.viewport !== undefined) {
246
+ bag.deviceTouched = true;
247
+ bag.hideDevice = false;
248
+ const resolved = resolveDeviceState(partial.viewport, bag.touchOverride, bag.orientationOverride);
249
+ if (resolved)
250
+ bag.device = resolved;
251
+ }
252
+ if (partial.zoom !== undefined && Number.isFinite(partial.zoom) && partial.zoom > 0) {
253
+ bag.zoom = partial.zoom;
254
+ }
255
+ if (partial.zoomOut !== undefined)
256
+ bag.zoomOut = !!partial.zoomOut;
257
+ if (partial.title !== undefined)
258
+ bag.title = String(partial.title ?? "");
259
+ const tp = normalizeTodoPos(partial.todoPos);
260
+ if (tp)
261
+ bag.todoPos = tp;
262
+ if (partial.todoDockUi !== undefined && partial.todoDockUi && typeof partial.todoDockUi === "object") {
263
+ bag.todoDockUi = { ...(bag.todoDockUi || {}), ...partial.todoDockUi };
264
+ }
265
+ },
266
+ };
267
+ }
268
+ /** Lift legacy per-slot todos (0.12.23) onto the phase bag once. */
269
+ export function liftLegacySlotTodos(bag) {
270
+ if (bag.todos && bag.todos.length)
271
+ return;
272
+ for (const stub of Object.values(bag.highlights)) {
273
+ const legacy = stub;
274
+ if (legacy.todos && legacy.todos.length) {
275
+ bag.todos = legacy.todos;
276
+ if (legacy.todoIndex !== undefined)
277
+ bag.todoIndex = legacy.todoIndex;
278
+ return;
279
+ }
280
+ }
281
+ }
@@ -0,0 +1,27 @@
1
+ import type { HighlightSize, HighlightStyleDefaults, HighlightTone, HighlightWeight } from "./types.js";
2
+ /** Apply Flow defaults under per-slot authored values (slot wins when set). */
3
+ export declare function applyHighlightStyleDefaults<T extends {
4
+ tone?: HighlightTone | string;
5
+ size?: HighlightSize | string;
6
+ weight?: HighlightWeight | string;
7
+ }>(slot: T, defaults?: HighlightStyleDefaults | null): T & {
8
+ tone: HighlightTone;
9
+ size: HighlightSize;
10
+ weight: HighlightWeight;
11
+ };
12
+ /** Map author aliases (`error` -> danger) to a canonical tone. */
13
+ export declare function normalizeHighlightTone(raw: unknown): HighlightTone;
14
+ /** Map size aliases to sm/md/lg. */
15
+ export declare function normalizeHighlightSize(raw: unknown): HighlightSize;
16
+ /** Map weight aliases to normal/bold. */
17
+ export declare function normalizeHighlightWeight(raw: unknown): HighlightWeight;
18
+ /** Prefix label with a short ASCII icon for semantic tones. */
19
+ export declare function toneIconPrefix(tone: HighlightTone): string;
20
+ /** Caption for ring / slide UI (label|caption + optional detail/tag + tone icon). */
21
+ export declare function formatHighlightCaption(h: {
22
+ label?: string;
23
+ caption?: string;
24
+ detail?: string;
25
+ tag?: string;
26
+ tone?: HighlightTone | string;
27
+ }): string;
@@ -0,0 +1,84 @@
1
+ /** Apply Flow defaults under per-slot authored values (slot wins when set). */
2
+ export function applyHighlightStyleDefaults(slot, defaults) {
3
+ const tone = slot.tone !== undefined && slot.tone !== ""
4
+ ? normalizeHighlightTone(slot.tone)
5
+ : defaults?.tone !== undefined
6
+ ? normalizeHighlightTone(defaults.tone)
7
+ : "planned";
8
+ const size = slot.size !== undefined && slot.size !== ""
9
+ ? normalizeHighlightSize(slot.size)
10
+ : defaults?.size !== undefined
11
+ ? normalizeHighlightSize(defaults.size)
12
+ : "md";
13
+ const weight = slot.weight !== undefined && slot.weight !== ""
14
+ ? normalizeHighlightWeight(slot.weight)
15
+ : defaults?.weight !== undefined
16
+ ? normalizeHighlightWeight(defaults.weight)
17
+ : "normal";
18
+ return { ...slot, tone, size, weight };
19
+ }
20
+ const TONE_ICONS = {
21
+ planned: "",
22
+ auto: "",
23
+ info: "i",
24
+ warning: "!",
25
+ danger: "x",
26
+ success: "+",
27
+ orange: "",
28
+ };
29
+ /** Map author aliases (`error` -> danger) to a canonical tone. */
30
+ export function normalizeHighlightTone(raw) {
31
+ if (typeof raw !== "string")
32
+ return "planned";
33
+ const t = raw.trim().toLowerCase();
34
+ if (t === "auto" || t === "automation" || t === "engine")
35
+ return "auto";
36
+ if (t === "info" || t === "blue")
37
+ return "info";
38
+ if (t === "warning" || t === "warn" || t === "yellow")
39
+ return "warning";
40
+ if (t === "danger" || t === "error" || t === "red")
41
+ return "danger";
42
+ if (t === "success" || t === "ok" || t === "green")
43
+ return "success";
44
+ if (t === "orange" || t === "amber" || t === "dom" || t === "inspect")
45
+ return "orange";
46
+ if (t === "planned" || t === "purple" || t === "authored")
47
+ return "planned";
48
+ return "planned";
49
+ }
50
+ /** Map size aliases to sm/md/lg. */
51
+ export function normalizeHighlightSize(raw) {
52
+ if (typeof raw !== "string")
53
+ return "md";
54
+ const t = raw.trim().toLowerCase();
55
+ if (t === "sm" || t === "s" || t === "small" || t === "tiny")
56
+ return "sm";
57
+ if (t === "lg" || t === "l" || t === "large" || t === "big" || t === "xl")
58
+ return "lg";
59
+ return "md";
60
+ }
61
+ /** Map weight aliases to normal/bold. */
62
+ export function normalizeHighlightWeight(raw) {
63
+ if (typeof raw !== "string")
64
+ return "normal";
65
+ const t = raw.trim().toLowerCase();
66
+ if (t === "bold" || t === "strong" || t === "heavy" || t === "b")
67
+ return "bold";
68
+ return "normal";
69
+ }
70
+ /** Prefix label with a short ASCII icon for semantic tones. */
71
+ export function toneIconPrefix(tone) {
72
+ const icon = TONE_ICONS[tone];
73
+ return icon ? `[${icon}] ` : "";
74
+ }
75
+ /** Caption for ring / slide UI (label|caption + optional detail/tag + tone icon). */
76
+ export function formatHighlightCaption(h) {
77
+ const tone = normalizeHighlightTone(h.tone);
78
+ const icon = toneIconPrefix(tone);
79
+ const primary = (h.label ?? h.caption ?? "").trim();
80
+ const tag = h.tag ? `[${h.tag}] ` : "";
81
+ if (h.detail)
82
+ return `${icon}${tag}${primary} - ${h.detail}`;
83
+ return `${icon}${tag}${primary}`;
84
+ }
@@ -0,0 +1,52 @@
1
+ import type { TodoDockState, TodoDockUiOpts, TodoDockUiResolved, TodoListStyle, WaygraphTodoGroupInput, WaygraphTodoInput, WaygraphTodoItem } from "./types.js";
2
+ /**
3
+ * Resolve todo-dock UX from env + optional author patch.
4
+ * `WAYGRAPH_TODO_UI=full|off|legacy` forces compact/collision/behindRing off.
5
+ */
6
+ export declare function resolveTodoDockUi(patch?: TodoDockUiOpts | null, env?: NodeJS.ProcessEnv): TodoDockUiResolved;
7
+ /** Normalize checklist dock side. */
8
+ export declare function normalizeTodoPos(raw: unknown): "left" | "right" | undefined;
9
+ /** Normalize dock list style. */
10
+ export declare function normalizeTodoStyle(raw: unknown): TodoListStyle | undefined;
11
+ /**
12
+ * Normalize checklist rows; apply todoIndex for sequential done/current.
13
+ * In checklist / bullets mode, todoIndex is ignored unless items omit explicit done
14
+ * (bullets never auto-mark current).
15
+ */
16
+ export declare function normalizeTodos(todos: readonly WaygraphTodoInput[] | undefined, todoIndex?: number, style?: TodoListStyle): WaygraphTodoItem[];
17
+ /** Build the floating dock from bag / phase fields. */
18
+ export declare function buildTodoDock(opts: {
19
+ todos?: readonly WaygraphTodoInput[] | undefined;
20
+ todoIndex?: number | undefined;
21
+ todoTitle?: string | undefined;
22
+ todoId?: string | undefined;
23
+ todoStyle?: TodoListStyle | undefined;
24
+ todoGroups?: readonly WaygraphTodoGroupInput[] | undefined;
25
+ todoPos?: "left" | "right" | undefined;
26
+ }): TodoDockState | undefined;
27
+ /**
28
+ * Carry-forward rules for the floating todo dock across demo steps.
29
+ * - clear: author called hideTodos / empty todos()
30
+ * - set: author authored a new dock this phase
31
+ * - keep: author omitted todos — preserve previous dock (do not wipe)
32
+ */
33
+ export declare function applyTodoPhase(prev: TodoDockState | undefined, phase: {
34
+ todoSync?: "set" | "clear" | "keep";
35
+ todoDock?: TodoDockState;
36
+ todos?: WaygraphTodoItem[];
37
+ todoPos?: "left" | "right";
38
+ todoId?: string;
39
+ }): {
40
+ dock: TodoDockState | undefined;
41
+ sync: "set" | "clear" | "keep";
42
+ };
43
+ /**
44
+ * Advance every sequential group to `index` (capped). Checklist / bullets unchanged.
45
+ * Used while cycling rings / fill/click stubs so the walkthrough moves.
46
+ */
47
+ export declare function advanceTodoDock(dock: TodoDockState | undefined, index: number): TodoDockState | undefined;
48
+ /**
49
+ * Mark sequential groups complete (all done, none current) - e.g. end of step.
50
+ * Checklist / bullets groups keep explicit done flags.
51
+ */
52
+ export declare function completeSequentialTodoDock(dock: TodoDockState | undefined): TodoDockState | undefined;
@@ -0,0 +1,248 @@
1
+ function envFlagBool(env, key, fallback) {
2
+ const v = env[key];
3
+ if (v === undefined || v === "")
4
+ return fallback;
5
+ const s = String(v).trim().toLowerCase();
6
+ if (s === "0" || s === "false" || s === "off" || s === "no")
7
+ return false;
8
+ if (s === "1" || s === "true" || s === "on" || s === "yes")
9
+ return true;
10
+ return fallback;
11
+ }
12
+ function envFlagInt(env, key, fallback) {
13
+ const n = Number(env[key]);
14
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
15
+ }
16
+ /**
17
+ * Resolve todo-dock UX from env + optional author patch.
18
+ * `WAYGRAPH_TODO_UI=full|off|legacy` forces compact/collision/behindRing off.
19
+ */
20
+ export function resolveTodoDockUi(patch, env = process.env) {
21
+ const mode = String(env.WAYGRAPH_TODO_UI || "smart")
22
+ .trim()
23
+ .toLowerCase();
24
+ const full = mode === "full" || mode === "off" || mode === "0" || mode === "legacy" || mode === "classic";
25
+ const base = {
26
+ compact: full ? false : envFlagBool(env, "WAYGRAPH_TODO_COMPACT", true),
27
+ collision: full ? false : envFlagBool(env, "WAYGRAPH_TODO_COLLISION", true),
28
+ behindRing: full ? false : envFlagBool(env, "WAYGRAPH_TODO_BEHIND", true),
29
+ cap: envFlagInt(env, "WAYGRAPH_TODO_CAP", 5),
30
+ expandCap: envFlagInt(env, "WAYGRAPH_TODO_EXPAND_CAP", 14),
31
+ };
32
+ if (base.expandCap < base.cap)
33
+ base.expandCap = base.cap;
34
+ if (!patch)
35
+ return base;
36
+ const cap = patch.cap !== undefined && Number.isFinite(patch.cap)
37
+ ? Math.max(1, Math.floor(Number(patch.cap)))
38
+ : base.cap;
39
+ let expandCap = patch.expandCap !== undefined && Number.isFinite(patch.expandCap)
40
+ ? Math.max(1, Math.floor(Number(patch.expandCap)))
41
+ : base.expandCap;
42
+ if (expandCap < cap)
43
+ expandCap = cap;
44
+ return {
45
+ compact: patch.compact !== undefined ? !!patch.compact : base.compact,
46
+ collision: patch.collision !== undefined ? !!patch.collision : base.collision,
47
+ behindRing: patch.behindRing !== undefined ? !!patch.behindRing : base.behindRing,
48
+ cap,
49
+ expandCap,
50
+ };
51
+ }
52
+ /** Normalize checklist dock side. */
53
+ export function normalizeTodoPos(raw) {
54
+ const s = String(raw ?? "")
55
+ .trim()
56
+ .toLowerCase();
57
+ if (s === "left" || s === "l")
58
+ return "left";
59
+ if (s === "right" || s === "r")
60
+ return "right";
61
+ return undefined;
62
+ }
63
+ /** Normalize dock list style. */
64
+ export function normalizeTodoStyle(raw) {
65
+ const s = String(raw ?? "")
66
+ .trim()
67
+ .toLowerCase();
68
+ if (s === "sequential" || s === "seq" || s === "steps" || s === "step")
69
+ return "sequential";
70
+ if (s === "checklist" || s === "check" || s === "checks" || s === "box")
71
+ return "checklist";
72
+ if (s === "bullets" ||
73
+ s === "bullet" ||
74
+ s === "list" ||
75
+ s === "ul" ||
76
+ s === "points" ||
77
+ s === "plain") {
78
+ return "bullets";
79
+ }
80
+ return undefined;
81
+ }
82
+ /**
83
+ * Normalize checklist rows; apply todoIndex for sequential done/current.
84
+ * In checklist / bullets mode, todoIndex is ignored unless items omit explicit done
85
+ * (bullets never auto-mark current).
86
+ */
87
+ export function normalizeTodos(todos, todoIndex, style = "sequential") {
88
+ if (!todos || todos.length === 0)
89
+ return [];
90
+ const idx = todoIndex !== undefined && Number.isFinite(todoIndex) ? Math.floor(todoIndex) : undefined;
91
+ return todos.map((t, i) => {
92
+ const text = typeof t === "string" ? t.trim() : String(t?.text ?? "").trim();
93
+ const item = { text: text || `Item ${i + 1}` };
94
+ if (typeof t === "object" && t) {
95
+ if (t.done !== undefined)
96
+ item.done = !!t.done;
97
+ if (t.current !== undefined)
98
+ item.current = !!t.current;
99
+ const idRaw = t.id ?? t.name;
100
+ if (idRaw != null && String(idRaw).trim())
101
+ item.id = String(idRaw).trim();
102
+ }
103
+ if (style === "checklist") {
104
+ if (item.done === undefined)
105
+ item.done = false;
106
+ item.current = false;
107
+ return item;
108
+ }
109
+ if (style === "bullets") {
110
+ // Flat list: no walkthrough current; keep explicit done if authored.
111
+ item.current = false;
112
+ return item;
113
+ }
114
+ if (idx !== undefined) {
115
+ if (item.done === undefined)
116
+ item.done = i < idx;
117
+ if (item.current === undefined)
118
+ item.current = i === idx;
119
+ }
120
+ return item;
121
+ });
122
+ }
123
+ /** Build the floating dock from bag / phase fields. */
124
+ export function buildTodoDock(opts) {
125
+ const style = normalizeTodoStyle(opts.todoStyle) || "sequential";
126
+ const pos = normalizeTodoPos(opts.todoPos);
127
+ const dockId = opts.todoId != null && String(opts.todoId).trim() ? String(opts.todoId).trim() : undefined;
128
+ if (opts.todoGroups && opts.todoGroups.length) {
129
+ const groups = opts.todoGroups
130
+ .map((g) => {
131
+ const gs = normalizeTodoStyle(g.style) || style;
132
+ const items = normalizeTodos(g.items, g.todoIndex, gs);
133
+ if (!items.length)
134
+ return null;
135
+ const group = { style: gs, items };
136
+ if (g.title && String(g.title).trim())
137
+ group.title = String(g.title).trim();
138
+ const gid = g.id ?? g.name;
139
+ if (gid != null && String(gid).trim())
140
+ group.id = String(gid).trim();
141
+ return group;
142
+ })
143
+ .filter((g) => !!g);
144
+ if (!groups.length)
145
+ return undefined;
146
+ const dock = { style, groups };
147
+ if (pos)
148
+ dock.pos = pos;
149
+ if (dockId)
150
+ dock.id = dockId;
151
+ return dock;
152
+ }
153
+ const items = normalizeTodos(opts.todos, opts.todoIndex, style);
154
+ if (!items.length)
155
+ return undefined;
156
+ const group = { style, items };
157
+ // Title once on the dock - not also on the lone group (avoids duplicate headings).
158
+ const dock = { style, groups: [group] };
159
+ if (opts.todoTitle && String(opts.todoTitle).trim()) {
160
+ dock.title = String(opts.todoTitle).trim();
161
+ }
162
+ if (pos)
163
+ dock.pos = pos;
164
+ if (dockId)
165
+ dock.id = dockId;
166
+ return dock;
167
+ }
168
+ /**
169
+ * Carry-forward rules for the floating todo dock across demo steps.
170
+ * - clear: author called hideTodos / empty todos()
171
+ * - set: author authored a new dock this phase
172
+ * - keep: author omitted todos — preserve previous dock (do not wipe)
173
+ */
174
+ export function applyTodoPhase(prev, phase) {
175
+ const sync = phase.todoSync ||
176
+ (phase.todoDock ? "set" : phase.todos && phase.todos.length ? "set" : "keep");
177
+ if (sync === "clear")
178
+ return { dock: undefined, sync: "clear" };
179
+ if (sync === "set") {
180
+ if (phase.todoDock)
181
+ return { dock: phase.todoDock, sync: "set" };
182
+ if (phase.todos && phase.todos.length) {
183
+ const dock = buildTodoDock({
184
+ todos: phase.todos,
185
+ ...(phase.todoPos ? { todoPos: phase.todoPos } : {}),
186
+ ...(phase.todoId ? { todoId: phase.todoId } : {}),
187
+ });
188
+ return { dock, sync: "set" };
189
+ }
190
+ return { dock: undefined, sync: "clear" };
191
+ }
192
+ return { dock: prev, sync: "keep" };
193
+ }
194
+ /**
195
+ * Advance every sequential group to `index` (capped). Checklist / bullets unchanged.
196
+ * Used while cycling rings / fill/click stubs so the walkthrough moves.
197
+ */
198
+ export function advanceTodoDock(dock, index) {
199
+ if (!dock || !dock.groups.length)
200
+ return dock;
201
+ const i = Number.isFinite(index) ? Math.max(0, Math.floor(index)) : 0;
202
+ return {
203
+ ...dock,
204
+ groups: dock.groups.map((g) => {
205
+ if (g.style !== "sequential")
206
+ return g;
207
+ const capped = Math.min(i, Math.max(0, g.items.length - 1));
208
+ const raw = g.items.map((it) => {
209
+ const row = { text: it.text };
210
+ if (it.id)
211
+ row.id = it.id;
212
+ else if (it.name)
213
+ row.id = it.name;
214
+ return row;
215
+ });
216
+ return {
217
+ ...g,
218
+ items: normalizeTodos(raw, capped, "sequential"),
219
+ };
220
+ }),
221
+ };
222
+ }
223
+ /**
224
+ * Mark sequential groups complete (all done, none current) - e.g. end of step.
225
+ * Checklist / bullets groups keep explicit done flags.
226
+ */
227
+ export function completeSequentialTodoDock(dock) {
228
+ if (!dock)
229
+ return dock;
230
+ return {
231
+ ...dock,
232
+ groups: dock.groups.map((g) => {
233
+ if (g.style !== "sequential")
234
+ return g;
235
+ return {
236
+ ...g,
237
+ items: g.items.map((it) => {
238
+ const row = { text: it.text, done: true, current: false };
239
+ if (it.id)
240
+ row.id = it.id;
241
+ else if (it.name)
242
+ row.id = it.name;
243
+ return row;
244
+ }),
245
+ };
246
+ }),
247
+ };
248
+ }