waygraph 0.12.44 → 0.12.46

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.
package/dist/cli.js CHANGED
@@ -226,6 +226,10 @@ import {
226
226
  formatDemoPaceBadge,
227
227
  formatDemoPaceLabel,
228
228
  normalizeTodos,
229
+ buildTodoDock,
230
+ applyTodoPhase,
231
+ advanceTodoDock,
232
+ completeSequentialTodoDock,
229
233
  } from "waygraph";
230
234
 
231
235
  function walkDir(dir, pattern) {
@@ -1082,20 +1086,53 @@ async function installOverlay(page, title) {
1082
1086
  // Floating todo dock - lives outside #wg-panel so --mini / Hide never
1083
1087
  // hide the checklist. Click cycles left <-> right with CSS transform.
1084
1088
  // Authors set side via ctx.todoPos("left"|"right"); env / --todo-* fallback.
1085
- window.__wgSyncTodos = (list, pos) => {
1086
- list = Array.isArray(list) ? list : [];
1087
- const POS = ["left", "right"];
1089
+ // Payload:
1090
+ // { sync: "clear" } -> remove dock
1091
+ // { sync: "keep" } -> leave dock alone (no wipe)
1092
+ // { sync: "set", dock } / { sync:"set", list } -> render / recreate
1093
+ // Compat: bare array = set list (empty array = clear - legacy only).
1094
+ window.__wgSyncTodos = (payload, pos) => {
1095
+ let sync = "set";
1096
+ let list = [];
1097
+ let dockState = null;
1098
+ let wantPos =
1099
+ pos === "left" || pos === "right" ? pos : null;
1100
+ if (Array.isArray(payload)) {
1101
+ list = payload;
1102
+ sync = list.length ? "set" : "clear";
1103
+ } else if (payload && typeof payload === "object") {
1104
+ sync = payload.sync === "clear" || payload.sync === "keep" ? payload.sync : "set";
1105
+ dockState = payload.dock || null;
1106
+ if (payload.pos === "left" || payload.pos === "right") wantPos = payload.pos;
1107
+ if (Array.isArray(payload.list)) list = payload.list;
1108
+ else if (dockState && Array.isArray(dockState.groups)) {
1109
+ // Flatten for legacy callers; full dock rendered below when present.
1110
+ list = [];
1111
+ for (const g of dockState.groups) {
1112
+ if (g && Array.isArray(g.items)) list = list.concat(g.items);
1113
+ }
1114
+ }
1115
+ } else {
1116
+ list = [];
1117
+ sync = "clear";
1118
+ }
1119
+
1088
1120
  let dock = document.getElementById("wg-todo-dock");
1089
1121
  // Drop any in-panel leftover (old builds put #wg-todos in .wg-body).
1090
1122
  document.querySelectorAll("#wg-panel #wg-todos").forEach((el) => el.remove());
1091
- if (!list.length) {
1123
+
1124
+ // keep + no new payload: do not remove an existing dock (PIA #10).
1125
+ if (sync === "keep") {
1126
+ if (!dockState && !list.length) return;
1127
+ // After navigation the DOM may be gone - recreate from carried dock.
1128
+ sync = "set";
1129
+ }
1130
+ if (sync === "clear" || (sync === "set" && !dockState && !list.length)) {
1092
1131
  if (dock) dock.remove();
1093
1132
  return;
1094
1133
  }
1095
- const wantPos =
1096
- pos === "left" || pos === "right"
1097
- ? pos
1098
- : null;
1134
+
1135
+ const POS = ["left", "right"];
1099
1136
  if (!dock) {
1100
1137
  dock = document.createElement("div");
1101
1138
  dock.id = "wg-todo-dock";
@@ -1133,33 +1170,84 @@ async function installOverlay(page, title) {
1133
1170
  /* ignore */
1134
1171
  }
1135
1172
  }
1173
+
1136
1174
  const esc = (s) =>
1137
1175
  String(s)
1138
1176
  .replace(/&/g, "&amp;")
1139
1177
  .replace(/</g, "&lt;")
1140
- .replace(/>/g, "&gt;");
1141
- dock.innerHTML =
1142
- '<ul id="wg-todos">' +
1143
- list
1144
- .map((t) => {
1145
- const cls = t.current
1146
- ? "wg-todo-current"
1147
- : t.done
1148
- ? "wg-todo-done"
1149
- : "wg-todo-pending";
1150
- const mark = t.done ? "\u2713" : t.current ? "\u2192" : "\u25CB";
1151
- return (
1152
- '<li class="' +
1153
- cls +
1154
- '"><span class="wg-todo-mark">' +
1155
- mark +
1156
- "</span><span>" +
1157
- esc(t.text || "") +
1158
- "</span></li>"
1159
- );
1160
- })
1161
- .join("") +
1162
- "</ul>";
1178
+ .replace(/>/g, "&gt;")
1179
+ .replace(/"/g, "&quot;");
1180
+ const attrId = (raw) => {
1181
+ const id = raw != null && String(raw).trim() ? String(raw).trim() : "";
1182
+ return id ? ' data-wg-todo-item="' + esc(id) + '"' : "";
1183
+ };
1184
+ const renderItems = (items, style) => {
1185
+ const st = style === "checklist" ? "checklist" : "sequential";
1186
+ return (
1187
+ '<ul id="wg-todos" data-wg-todo-style="' +
1188
+ st +
1189
+ '">' +
1190
+ (items || [])
1191
+ .map((t) => {
1192
+ const cls = t.current
1193
+ ? "wg-todo-current"
1194
+ : t.done
1195
+ ? "wg-todo-done"
1196
+ : "wg-todo-pending";
1197
+ let mark;
1198
+ if (st === "checklist") {
1199
+ mark = t.done ? "\u2611" : "\u2610";
1200
+ } else {
1201
+ mark = t.done ? "\u2713" : t.current ? "\u2192" : "\u25CB";
1202
+ }
1203
+ const rowId = t.id || t.name || "";
1204
+ return (
1205
+ '<li class="' +
1206
+ cls +
1207
+ '"' +
1208
+ attrId(rowId) +
1209
+ '><span class="wg-todo-mark">' +
1210
+ mark +
1211
+ "</span><span>" +
1212
+ esc(t.text || "") +
1213
+ "</span></li>"
1214
+ );
1215
+ })
1216
+ .join("") +
1217
+ "</ul>"
1218
+ );
1219
+ };
1220
+
1221
+ const dockId =
1222
+ (dockState && dockState.id) ||
1223
+ (payload && !Array.isArray(payload) && payload.todoId) ||
1224
+ "";
1225
+ if (dockId) dock.setAttribute("data-wg-todo-id", String(dockId));
1226
+ else dock.removeAttribute("data-wg-todo-id");
1227
+
1228
+ let html = "";
1229
+ if (dockState && Array.isArray(dockState.groups) && dockState.groups.length) {
1230
+ const dockTitle = dockState.title && String(dockState.title).trim();
1231
+ if (dockTitle) {
1232
+ html += '<div class="wg-todo-dock-title">' + esc(dockTitle) + "</div>";
1233
+ }
1234
+ for (const g of dockState.groups) {
1235
+ const gTitle = g.title && String(g.title).trim();
1236
+ const gid = g.id || g.name || "";
1237
+ html +=
1238
+ '<div class="wg-todo-group"' +
1239
+ (gid ? ' data-wg-todo-group="' + esc(gid) + '"' : "") +
1240
+ ">";
1241
+ if (gTitle) {
1242
+ html += '<div class="wg-todo-group-title">' + esc(gTitle) + "</div>";
1243
+ }
1244
+ html += renderItems(g.items, g.style || dockState.style);
1245
+ html += "</div>";
1246
+ }
1247
+ } else {
1248
+ html = renderItems(list, "sequential");
1249
+ }
1250
+ dock.innerHTML = html;
1163
1251
  if (window.__wgStampModal) {
1164
1252
  window.__wgStampModal(dock, "todos", { ready: true });
1165
1253
  }
@@ -1302,7 +1390,16 @@ async function renderBeforeStep(page, info) {
1302
1390
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
1303
1391
  });
1304
1392
  }
1305
- if (window.__wgSyncTodos) window.__wgSyncTodos(info.todos || [], info.todoPos || null);
1393
+ if (window.__wgSyncTodos) {
1394
+ const sync = info.todoSync || (info.todos && info.todos.length ? "set" : "keep");
1395
+ window.__wgSyncTodos({
1396
+ sync: sync,
1397
+ dock: info.todoDock || null,
1398
+ list: info.todos || [],
1399
+ pos: info.todoPos || null,
1400
+ todoId: info.todoId || (info.todoDock && info.todoDock.id) || null,
1401
+ });
1402
+ }
1306
1403
  if (window.__wgStampModal) {
1307
1404
  window.__wgStampModal(panel, "panel", {
1308
1405
  phase: "before",
@@ -1840,7 +1937,16 @@ async function renderAfterStep(page, info) {
1840
1937
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
1841
1938
  });
1842
1939
  }
1843
- if (window.__wgSyncTodos) window.__wgSyncTodos(info.todos || [], info.todoPos || null);
1940
+ if (window.__wgSyncTodos) {
1941
+ const sync = info.todoSync || (info.todos && info.todos.length ? "set" : "keep");
1942
+ window.__wgSyncTodos({
1943
+ sync: sync,
1944
+ dock: info.todoDock || null,
1945
+ list: info.todos || [],
1946
+ pos: info.todoPos || null,
1947
+ todoId: info.todoId || (info.todoDock && info.todoDock.id) || null,
1948
+ });
1949
+ }
1844
1950
  if (window.__wgStampModal) {
1845
1951
  window.__wgStampModal(panel, "panel", {
1846
1952
  phase: "after",
@@ -2908,6 +3014,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2908
3014
  }
2909
3015
 
2910
3016
  let result;
3017
+ /** Carry floating todo dock across steps (empty stubBefore must not wipe). */
3018
+ let lastTodoDock = undefined;
2911
3019
  for (let i = 0; i < resolved.length; i++) {
2912
3020
  const r = resolved[i];
2913
3021
  // The block breadcrumb is scoped to THIS step's own episode, not the
@@ -2993,13 +3101,24 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2993
3101
  stubBeforeRef.current = stubBeforePhase.highlights.map((h) =>
2994
3102
  applyHighlightStyleDefaults(h, flowStyle),
2995
3103
  );
2996
- const stubBeforeTodos = stubBeforePhase.todos || [];
3104
+ const appliedBefore = applyTodoPhase(lastTodoDock, {
3105
+ todoSync: stubBeforePhase.todoSync,
3106
+ todoDock: stubBeforePhase.todoDock,
3107
+ todos: stubBeforePhase.todos,
3108
+ todoPos: stubBeforePhase.todoPos,
3109
+ });
3110
+ lastTodoDock = appliedBefore.dock;
3111
+ const stubBeforeTodos =
3112
+ (lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
3113
+ stubBeforePhase.todos ||
3114
+ [];
2997
3115
  const overlayTitle =
2998
3116
  (stubBeforePhase.title && String(stubBeforePhase.title).trim()) ||
2999
3117
  (r.episodeTitle && String(r.episodeTitle).trim()) ||
3000
3118
  title ||
3001
3119
  "waygraph demo";
3002
- const overlayTodoPos = stubBeforePhase.todoPos || undefined;
3120
+ const overlayTodoPos =
3121
+ (lastTodoDock && lastTodoDock.pos) || stubBeforePhase.todoPos || undefined;
3003
3122
  const isNavBlock = r.block.__waygraphKind === "nav";
3004
3123
  const autoNow = await currentAutoplay();
3005
3124
  // Video / --mini: compact pill. Never pass false - Hide/localStorage wins.
@@ -3051,6 +3170,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3051
3170
  stepperMode,
3052
3171
  forceCollapsed,
3053
3172
  todos: stubBeforeTodos,
3173
+ todoDock: lastTodoDock,
3174
+ todoSync: appliedBefore.sync,
3175
+ todoId: lastTodoDock && lastTodoDock.id,
3054
3176
  ...paceSpeak,
3055
3177
  });
3056
3178
  const edits = await gate();
@@ -3206,16 +3328,28 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3206
3328
  let stubAfterTodos = [];
3207
3329
  let afterOverlayTitle = overlayTitle;
3208
3330
  let afterTodoPos = overlayTodoPos;
3331
+ let appliedAfter = { dock: lastTodoDock, sync: "keep" };
3209
3332
  if (hasAuthoredStubAfter(r.block, result, fixturesAfter)) {
3210
3333
  const afterPhase = await runStubPhase(r.block, "stubAfter", {
3211
3334
  out: result,
3212
3335
  fixtures: fixturesAfter,
3213
3336
  });
3214
- stubAfterTodos = afterPhase.todos || [];
3337
+ appliedAfter = applyTodoPhase(lastTodoDock, {
3338
+ todoSync: afterPhase.todoSync,
3339
+ todoDock: afterPhase.todoDock,
3340
+ todos: afterPhase.todos,
3341
+ todoPos: afterPhase.todoPos,
3342
+ });
3343
+ lastTodoDock = appliedAfter.dock;
3344
+ stubAfterTodos =
3345
+ (lastTodoDock && lastTodoDock.groups[0] && lastTodoDock.groups[0].items) ||
3346
+ afterPhase.todos ||
3347
+ [];
3215
3348
  if (afterPhase.title && String(afterPhase.title).trim()) {
3216
3349
  afterOverlayTitle = String(afterPhase.title).trim();
3217
3350
  }
3218
3351
  if (afterPhase.todoPos) afterTodoPos = afterPhase.todoPos;
3352
+ if (lastTodoDock && lastTodoDock.pos) afterTodoPos = lastTodoDock.pos;
3219
3353
  highlights = afterPhase.highlights.map((h) => {
3220
3354
  const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
3221
3355
  return {
@@ -3255,6 +3389,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3255
3389
  episodeTitle: r.episodeTitle,
3256
3390
  stepperMode,
3257
3391
  todos: stubAfterTodos,
3392
+ todoDock: lastTodoDock,
3393
+ todoSync: appliedAfter.sync,
3394
+ todoId: lastTodoDock && lastTodoDock.id,
3258
3395
  forceCollapsed:
3259
3396
  !!process.env.WAYGRAPH_VIDEO ||
3260
3397
  process.env.WAYGRAPH_MINI === "1" ||
@@ -34,17 +34,75 @@ export type FixtureDurationFields = {
34
34
  */
35
35
  export type WaygraphTodoItem = {
36
36
  text: string;
37
+ /**
38
+ * Stable id for external asserts / redirects (Mailhog, deep links).
39
+ * Stamped as `data-wg-todo-item` on the row.
40
+ */
41
+ id?: string;
42
+ /** Alias for {@link WaygraphTodoItem.id}. */
43
+ name?: string;
37
44
  /** Explicit done. If omitted, derived from todoIndex (index < todoIndex). */
38
45
  done?: boolean;
39
46
  /** Explicit current. If omitted, derived from todoIndex (index === todoIndex). */
40
47
  current?: boolean;
41
48
  };
42
49
  export type WaygraphTodoInput = string | WaygraphTodoItem;
50
+ /**
51
+ * Dock list presentation:
52
+ * - `sequential` - arrow on current, strike done (demo walkthrough)
53
+ * - `checklist` - checkbox done/pending only (no current arrow; independent checks)
54
+ */
55
+ export type TodoListStyle = "sequential" | "checklist";
56
+ /** One titled group in the floating todo dock (FR / Scenarios / ACs, etc.). */
57
+ export type WaygraphTodoGroupInput = {
58
+ /** Stable group id -> `data-wg-todo-group`. */
59
+ id?: string;
60
+ /** Alias for {@link WaygraphTodoGroupInput.id}. */
61
+ name?: string;
62
+ /** Group heading shown above the items. */
63
+ title?: string;
64
+ /** Per-group style; falls back to dock default ({@link StubCtx.todoStyle}). */
65
+ style?: TodoListStyle;
66
+ items: readonly WaygraphTodoInput[];
67
+ /** Sequential only: which item is current (0-based). */
68
+ todoIndex?: number;
69
+ };
70
+ export type WaygraphTodoGroup = {
71
+ id?: string;
72
+ title?: string;
73
+ style: TodoListStyle;
74
+ items: WaygraphTodoItem[];
75
+ };
76
+ /** Full floating dock payload passed to the browser. */
77
+ export type TodoDockState = {
78
+ /**
79
+ * Stable dock id for external targeting after redirects
80
+ * (`#wg-todo-dock[data-wg-todo-id="…"]`).
81
+ */
82
+ id?: string;
83
+ /** Default style for groups that omit their own. */
84
+ style: TodoListStyle;
85
+ /** Single-list title when using {@link StubCtx.todos} + {@link StubCtx.todoTitle}. */
86
+ title?: string;
87
+ groups: WaygraphTodoGroup[];
88
+ pos?: "left" | "right";
89
+ };
43
90
  /** Episode-level fixtures authored inside stubBefore/After/OnError(ctx). */
44
91
  export type StubPhaseFixtures = {
45
92
  todos?: readonly WaygraphTodoInput[];
46
93
  /** Which todo is current (0-based). Rows before = done, after = pending. */
47
94
  todoIndex?: number;
95
+ /** Heading above a single {@link StubCtx.todos} list. */
96
+ todoTitle?: string;
97
+ /**
98
+ * Stable dock id (`data-wg-todo-id`) for external redirect asserts
99
+ * (e.g. Mailhog -> back to app and still find the checklist).
100
+ */
101
+ todoId?: string;
102
+ /** `sequential` (default) or `checklist`. */
103
+ todoStyle?: TodoListStyle;
104
+ /** Multiple titled lists (FR / Scenarios / ACs). Wins over bare todos when set. */
105
+ todoGroups?: readonly WaygraphTodoGroupInput[];
48
106
  /**
49
107
  * Camera zoom (Screen Studio style) - scales the page toward the target /
50
108
  * cursor. Not element CSS zoom. Typical `1.25` .. `2`.
@@ -152,10 +210,34 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
152
210
  highlights(slots: HighlightStubPhase): void;
153
211
  /** Set / overwrite one named ring. */
154
212
  ring(id: string, stub: WaygraphHighlightStub): void;
155
- /** Episode checklist (demo panel). */
213
+ /** Episode checklist (demo panel). Single list; use {@link StubCtx.todoGroups} for FR/AC packs. */
156
214
  todos(items: readonly WaygraphTodoInput[]): void;
157
215
  /** Current checklist index (bump for sequential plans). */
158
216
  todoIndex(n: number): void;
217
+ /** Heading above the single {@link StubCtx.todos} list. */
218
+ todoTitle(text: string): void;
219
+ /**
220
+ * Stable dock id for external selectors after redirects
221
+ * (`#wg-todo-dock[data-wg-todo-id="ep10-coverage"]`).
222
+ */
223
+ todoId(id: string): void;
224
+ /**
225
+ * Dock presentation: `sequential` (arrow walkthrough) or `checklist`
226
+ * (independent checkboxes, no current arrow).
227
+ */
228
+ todoStyle(style: TodoListStyle): void;
229
+ /**
230
+ * Multiple titled lists (e.g. Functional Requirements / Scenarios / ACs).
231
+ * Replaces a bare {@link StubCtx.todos} list when set.
232
+ */
233
+ todoGroups(groups: readonly WaygraphTodoGroupInput[]): void;
234
+ /**
235
+ * Hide the floating todo dock. Omit todos on a later block = **keep** the
236
+ * previous dock (carry forward). Only hideTodos / clearTodos / todos([]) clears it.
237
+ */
238
+ hideTodos(): void;
239
+ /** Alias for {@link StubCtx.hideTodos}. */
240
+ clearTodos(): void;
159
241
  /** Default camera zoom for rings without their own zoom. */
160
242
  zoom(n: number): void;
161
243
  /**
@@ -187,8 +269,17 @@ export type HighlightStubPhaseOrFn<Out extends Checkpoint<string>> = HighlightSt
187
269
  /** Result of running a stub phase (rings + episode fixtures). */
188
270
  export type StubPhaseResult = {
189
271
  highlights: ResolvedHighlight[];
272
+ /** Flattened first-group items (compat). Prefer {@link todoDock}. */
190
273
  todos: WaygraphTodoItem[];
191
274
  todoIndex?: number;
275
+ todoDock?: TodoDockState;
276
+ /**
277
+ * Todo dock intent for this phase:
278
+ * - `set` - replace dock with {@link todoDock}
279
+ * - `clear` - hide dock (author called hideTodos / empty todos())
280
+ * - `keep` - author omitted todos; demo must leave previous dock alone
281
+ */
282
+ todoSync?: "set" | "clear" | "keep";
192
283
  zoom?: number;
193
284
  zoomOut?: boolean;
194
285
  title?: string;
@@ -329,12 +420,51 @@ export interface WaygraphHighlight {
329
420
  tone?: HighlightTone;
330
421
  }
331
422
  export type ResolvedHighlight = WaygraphHighlightStub;
423
+ /** Normalize checklist dock side. */
424
+ export declare function normalizeTodoPos(raw: unknown): "left" | "right" | undefined;
425
+ /** Normalize dock list style. */
426
+ export declare function normalizeTodoStyle(raw: unknown): TodoListStyle | undefined;
332
427
  /**
333
428
  * Normalize checklist rows; apply todoIndex for sequential done/current.
429
+ * In checklist mode, todoIndex is ignored unless items omit explicit done.
334
430
  */
335
- export declare function normalizeTodos(todos: readonly WaygraphTodoInput[] | undefined, todoIndex?: number): WaygraphTodoItem[];
336
- /** Normalize checklist dock side. */
337
- export declare function normalizeTodoPos(raw: unknown): "left" | "right" | undefined;
431
+ export declare function normalizeTodos(todos: readonly WaygraphTodoInput[] | undefined, todoIndex?: number, style?: TodoListStyle): WaygraphTodoItem[];
432
+ /** Build the floating dock from bag / phase fields. */
433
+ export declare function buildTodoDock(opts: {
434
+ todos?: readonly WaygraphTodoInput[] | undefined;
435
+ todoIndex?: number | undefined;
436
+ todoTitle?: string | undefined;
437
+ todoId?: string | undefined;
438
+ todoStyle?: TodoListStyle | undefined;
439
+ todoGroups?: readonly WaygraphTodoGroupInput[] | undefined;
440
+ todoPos?: "left" | "right" | undefined;
441
+ }): TodoDockState | undefined;
442
+ /**
443
+ * Carry-forward rules for the floating todo dock across demo steps.
444
+ * - clear: author called hideTodos / empty todos()
445
+ * - set: author authored a new dock this phase
446
+ * - keep: author omitted todos — preserve previous dock (do not wipe)
447
+ */
448
+ export declare function applyTodoPhase(prev: TodoDockState | undefined, phase: {
449
+ todoSync?: "set" | "clear" | "keep";
450
+ todoDock?: TodoDockState;
451
+ todos?: WaygraphTodoItem[];
452
+ todoPos?: "left" | "right";
453
+ todoId?: string;
454
+ }): {
455
+ dock: TodoDockState | undefined;
456
+ sync: "set" | "clear" | "keep";
457
+ };
458
+ /**
459
+ * Advance every sequential group to `index` (capped). Checklist groups unchanged.
460
+ * Used while cycling rings / fill/click stubs so the walkthrough moves.
461
+ */
462
+ export declare function advanceTodoDock(dock: TodoDockState | undefined, index: number): TodoDockState | undefined;
463
+ /**
464
+ * Mark sequential groups complete (all done, none current) - e.g. end of step.
465
+ * Checklist groups keep explicit done flags.
466
+ */
467
+ export declare function completeSequentialTodoDock(dock: TodoDockState | undefined): TodoDockState | undefined;
338
468
  /**
339
469
  * Run an open stub lifecycle (preferred) or resolve a static slot map.
340
470
  * Returns rings + episode fixtures (todos / todoIndex / zoom).
@@ -293,31 +293,6 @@ function pickStyleFields(base, patch) {
293
293
  out.weight = normalizeHighlightWeight(weight);
294
294
  return out;
295
295
  }
296
- /**
297
- * Normalize checklist rows; apply todoIndex for sequential done/current.
298
- */
299
- export function normalizeTodos(todos, todoIndex) {
300
- if (!todos || todos.length === 0)
301
- return [];
302
- const idx = todoIndex !== undefined && Number.isFinite(todoIndex) ? Math.floor(todoIndex) : undefined;
303
- return todos.map((t, i) => {
304
- const text = typeof t === "string" ? t.trim() : String(t?.text ?? "").trim();
305
- const item = { text: text || `Item ${i + 1}` };
306
- if (typeof t === "object" && t) {
307
- if (t.done !== undefined)
308
- item.done = !!t.done;
309
- if (t.current !== undefined)
310
- item.current = !!t.current;
311
- }
312
- if (idx !== undefined) {
313
- if (item.done === undefined)
314
- item.done = i < idx;
315
- if (item.current === undefined)
316
- item.current = i === idx;
317
- }
318
- return item;
319
- });
320
- }
321
296
  function pickZoom(base, patch) {
322
297
  const zoom = patch?.zoom !== undefined ? patch.zoom : base?.zoom;
323
298
  if (zoom !== undefined && Number.isFinite(zoom) && zoom > 0)
@@ -394,6 +369,180 @@ export function normalizeTodoPos(raw) {
394
369
  return "right";
395
370
  return undefined;
396
371
  }
372
+ /** Normalize dock list style. */
373
+ export function normalizeTodoStyle(raw) {
374
+ const s = String(raw ?? "")
375
+ .trim()
376
+ .toLowerCase();
377
+ if (s === "sequential" || s === "seq" || s === "steps" || s === "step")
378
+ return "sequential";
379
+ if (s === "checklist" || s === "check" || s === "checks" || s === "box")
380
+ return "checklist";
381
+ return undefined;
382
+ }
383
+ /**
384
+ * Normalize checklist rows; apply todoIndex for sequential done/current.
385
+ * In checklist mode, todoIndex is ignored unless items omit explicit done.
386
+ */
387
+ export function normalizeTodos(todos, todoIndex, style = "sequential") {
388
+ if (!todos || todos.length === 0)
389
+ return [];
390
+ const idx = todoIndex !== undefined && Number.isFinite(todoIndex) ? Math.floor(todoIndex) : undefined;
391
+ return todos.map((t, i) => {
392
+ const text = typeof t === "string" ? t.trim() : String(t?.text ?? "").trim();
393
+ const item = { text: text || `Item ${i + 1}` };
394
+ if (typeof t === "object" && t) {
395
+ if (t.done !== undefined)
396
+ item.done = !!t.done;
397
+ if (t.current !== undefined)
398
+ item.current = !!t.current;
399
+ const idRaw = t.id ?? t.name;
400
+ if (idRaw != null && String(idRaw).trim())
401
+ item.id = String(idRaw).trim();
402
+ }
403
+ if (style === "checklist") {
404
+ if (item.done === undefined)
405
+ item.done = false;
406
+ item.current = false;
407
+ return item;
408
+ }
409
+ if (idx !== undefined) {
410
+ if (item.done === undefined)
411
+ item.done = i < idx;
412
+ if (item.current === undefined)
413
+ item.current = i === idx;
414
+ }
415
+ return item;
416
+ });
417
+ }
418
+ /** Build the floating dock from bag / phase fields. */
419
+ export function buildTodoDock(opts) {
420
+ const style = normalizeTodoStyle(opts.todoStyle) || "sequential";
421
+ const pos = normalizeTodoPos(opts.todoPos);
422
+ const dockId = opts.todoId != null && String(opts.todoId).trim() ? String(opts.todoId).trim() : undefined;
423
+ if (opts.todoGroups && opts.todoGroups.length) {
424
+ const groups = opts.todoGroups
425
+ .map((g) => {
426
+ const gs = normalizeTodoStyle(g.style) || style;
427
+ const items = normalizeTodos(g.items, g.todoIndex, gs);
428
+ if (!items.length)
429
+ return null;
430
+ const group = { style: gs, items };
431
+ if (g.title && String(g.title).trim())
432
+ group.title = String(g.title).trim();
433
+ const gid = g.id ?? g.name;
434
+ if (gid != null && String(gid).trim())
435
+ group.id = String(gid).trim();
436
+ return group;
437
+ })
438
+ .filter((g) => !!g);
439
+ if (!groups.length)
440
+ return undefined;
441
+ const dock = { style, groups };
442
+ if (pos)
443
+ dock.pos = pos;
444
+ if (dockId)
445
+ dock.id = dockId;
446
+ return dock;
447
+ }
448
+ const items = normalizeTodos(opts.todos, opts.todoIndex, style);
449
+ if (!items.length)
450
+ return undefined;
451
+ const group = { style, items };
452
+ if (opts.todoTitle && String(opts.todoTitle).trim()) {
453
+ group.title = String(opts.todoTitle).trim();
454
+ }
455
+ const dock = { style, groups: [group] };
456
+ if (opts.todoTitle && String(opts.todoTitle).trim()) {
457
+ dock.title = String(opts.todoTitle).trim();
458
+ }
459
+ if (pos)
460
+ dock.pos = pos;
461
+ if (dockId)
462
+ dock.id = dockId;
463
+ return dock;
464
+ }
465
+ /**
466
+ * Carry-forward rules for the floating todo dock across demo steps.
467
+ * - clear: author called hideTodos / empty todos()
468
+ * - set: author authored a new dock this phase
469
+ * - keep: author omitted todos — preserve previous dock (do not wipe)
470
+ */
471
+ export function applyTodoPhase(prev, phase) {
472
+ const sync = phase.todoSync ||
473
+ (phase.todoDock ? "set" : phase.todos && phase.todos.length ? "set" : "keep");
474
+ if (sync === "clear")
475
+ return { dock: undefined, sync: "clear" };
476
+ if (sync === "set") {
477
+ if (phase.todoDock)
478
+ return { dock: phase.todoDock, sync: "set" };
479
+ if (phase.todos && phase.todos.length) {
480
+ const dock = buildTodoDock({
481
+ todos: phase.todos,
482
+ ...(phase.todoPos ? { todoPos: phase.todoPos } : {}),
483
+ ...(phase.todoId ? { todoId: phase.todoId } : {}),
484
+ });
485
+ return { dock, sync: "set" };
486
+ }
487
+ return { dock: undefined, sync: "clear" };
488
+ }
489
+ return { dock: prev, sync: "keep" };
490
+ }
491
+ /**
492
+ * Advance every sequential group to `index` (capped). Checklist groups unchanged.
493
+ * Used while cycling rings / fill/click stubs so the walkthrough moves.
494
+ */
495
+ export function advanceTodoDock(dock, index) {
496
+ if (!dock || !dock.groups.length)
497
+ return dock;
498
+ const i = Number.isFinite(index) ? Math.max(0, Math.floor(index)) : 0;
499
+ return {
500
+ ...dock,
501
+ groups: dock.groups.map((g) => {
502
+ if (g.style !== "sequential")
503
+ return g;
504
+ const capped = Math.min(i, Math.max(0, g.items.length - 1));
505
+ const raw = g.items.map((it) => {
506
+ const row = { text: it.text };
507
+ if (it.id)
508
+ row.id = it.id;
509
+ else if (it.name)
510
+ row.id = it.name;
511
+ return row;
512
+ });
513
+ return {
514
+ ...g,
515
+ items: normalizeTodos(raw, capped, "sequential"),
516
+ };
517
+ }),
518
+ };
519
+ }
520
+ /**
521
+ * Mark sequential groups complete (all done, none current) - e.g. end of step.
522
+ * Checklist groups keep explicit done flags.
523
+ */
524
+ export function completeSequentialTodoDock(dock) {
525
+ if (!dock)
526
+ return dock;
527
+ return {
528
+ ...dock,
529
+ groups: dock.groups.map((g) => {
530
+ if (g.style !== "sequential")
531
+ return g;
532
+ return {
533
+ ...g,
534
+ items: g.items.map((it) => {
535
+ const row = { text: it.text, done: true, current: false };
536
+ if (it.id)
537
+ row.id = it.id;
538
+ else if (it.name)
539
+ row.id = it.name;
540
+ return row;
541
+ }),
542
+ };
543
+ }),
544
+ };
545
+ }
397
546
  function createStubCtx(bag, opts) {
398
547
  return {
399
548
  out: opts.out,
@@ -405,11 +554,52 @@ function createStubCtx(bag, opts) {
405
554
  bag.highlights = { ...bag.highlights, [id]: stub };
406
555
  },
407
556
  todos(items) {
557
+ bag.todoTouched = true;
558
+ bag.hideTodos = false;
408
559
  bag.todos = items;
560
+ delete bag.todoGroups;
561
+ // Empty list = explicit clear (same as hideTodos).
562
+ if (!items || !items.length)
563
+ bag.hideTodos = true;
409
564
  },
410
565
  todoIndex(n) {
411
566
  bag.todoIndex = n;
412
567
  },
568
+ todoTitle(text) {
569
+ bag.todoTitle = String(text ?? "");
570
+ },
571
+ todoId(id) {
572
+ bag.todoTouched = true;
573
+ const cleaned = String(id ?? "").trim();
574
+ if (cleaned)
575
+ bag.todoId = cleaned;
576
+ else
577
+ delete bag.todoId;
578
+ },
579
+ todoStyle(style) {
580
+ const s = normalizeTodoStyle(style);
581
+ if (s)
582
+ bag.todoStyle = s;
583
+ },
584
+ todoGroups(groups) {
585
+ bag.todoTouched = true;
586
+ bag.hideTodos = false;
587
+ bag.todoGroups = groups;
588
+ if (!groups || !groups.length)
589
+ bag.hideTodos = true;
590
+ },
591
+ hideTodos() {
592
+ bag.todoTouched = true;
593
+ bag.hideTodos = true;
594
+ bag.todos = [];
595
+ delete bag.todoGroups;
596
+ },
597
+ clearTodos() {
598
+ bag.todoTouched = true;
599
+ bag.hideTodos = true;
600
+ bag.todos = [];
601
+ delete bag.todoGroups;
602
+ },
413
603
  zoom(n) {
414
604
  if (Number.isFinite(n) && n > 0)
415
605
  bag.zoom = n;
@@ -431,10 +621,33 @@ function createStubCtx(bag, opts) {
431
621
  set(partial) {
432
622
  if (partial.highlights)
433
623
  bag.highlights = { ...partial.highlights };
434
- if (partial.todos !== undefined)
624
+ if (partial.todos !== undefined) {
625
+ bag.todoTouched = true;
626
+ bag.hideTodos = !partial.todos || !partial.todos.length;
435
627
  bag.todos = partial.todos;
628
+ if (partial.todoGroups === undefined)
629
+ delete bag.todoGroups;
630
+ }
436
631
  if (partial.todoIndex !== undefined)
437
632
  bag.todoIndex = partial.todoIndex;
633
+ if (partial.todoTitle !== undefined)
634
+ bag.todoTitle = String(partial.todoTitle ?? "");
635
+ if (partial.todoId !== undefined) {
636
+ bag.todoTouched = true;
637
+ const cleaned = String(partial.todoId ?? "").trim();
638
+ if (cleaned)
639
+ bag.todoId = cleaned;
640
+ else
641
+ delete bag.todoId;
642
+ }
643
+ const ts = normalizeTodoStyle(partial.todoStyle);
644
+ if (ts)
645
+ bag.todoStyle = ts;
646
+ if (partial.todoGroups !== undefined) {
647
+ bag.todoTouched = true;
648
+ bag.hideTodos = !partial.todoGroups || !partial.todoGroups.length;
649
+ bag.todoGroups = partial.todoGroups;
650
+ }
438
651
  if (partial.zoom !== undefined && Number.isFinite(partial.zoom) && partial.zoom > 0) {
439
652
  bag.zoom = partial.zoom;
440
653
  }
@@ -576,11 +789,35 @@ export async function runStubPhase(block, phase, opts) {
576
789
  let highlights = mergePhaseMaps(bag.highlights, fixturePhase);
577
790
  highlights = applyDefaultZoom(highlights, bag.zoom);
578
791
  highlights = applyDefaultZoomOut(highlights, bag.zoomOut);
579
- const todos = normalizeTodos(bag.todos, bag.todoIndex);
792
+ const todoDock = bag.hideTodos
793
+ ? undefined
794
+ : buildTodoDock({
795
+ todos: bag.todos,
796
+ todoIndex: bag.todoIndex,
797
+ todoTitle: bag.todoTitle,
798
+ todoId: bag.todoId,
799
+ todoStyle: bag.todoStyle,
800
+ todoGroups: bag.todoGroups,
801
+ todoPos: bag.todoPos,
802
+ });
803
+ const todos = todoDock?.groups[0]?.items ||
804
+ (bag.hideTodos
805
+ ? []
806
+ : normalizeTodos(bag.todos, bag.todoIndex, normalizeTodoStyle(bag.todoStyle) || "sequential"));
807
+ // Omit todos entirely => keep previous dock. Explicit empty / hideTodos => clear.
808
+ const todoSync = bag.hideTodos
809
+ ? "clear"
810
+ : bag.todoTouched && todoDock
811
+ ? "set"
812
+ : bag.todoTouched && !todoDock
813
+ ? "clear"
814
+ : "keep";
580
815
  return {
581
816
  highlights,
582
817
  todos,
818
+ todoSync,
583
819
  ...(bag.todoIndex !== undefined ? { todoIndex: bag.todoIndex } : {}),
820
+ ...(todoDock ? { todoDock } : {}),
584
821
  ...(bag.zoom !== undefined ? { zoom: bag.zoom } : {}),
585
822
  ...(bag.zoomOut !== undefined ? { zoomOut: bag.zoomOut } : {}),
586
823
  ...(bag.title !== undefined && bag.title !== "" ? { title: bag.title } : {}),
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@ export { connect, checkpoint } from "./types.js";
3
3
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
4
4
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
5
5
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
6
- export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
7
- export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, } from "./highlights.js";
6
+ export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
7
+ export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, } from "./highlights.js";
8
8
  export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
9
9
  export type { ActionPage } from "./types.js";
10
10
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { connect, checkpoint } from "./types.js";
2
2
  export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
3
3
  export { Trait, urlMatches, textEquals, visible } from "./trait.js";
4
4
  export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
5
- export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
5
+ export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, } from "./highlights.js";
6
6
  export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
7
7
  export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
8
8
  export { EdgeLeaseCoordinator } from "./traverse-lease.js";
@@ -17,7 +17,7 @@
17
17
  }
18
18
  },
19
19
  "../..": {
20
- "version": "0.12.43",
20
+ "version": "0.12.46",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "tsx": "^4.0.0"
@@ -17,7 +17,12 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
17
17
  verify: [Trait.url({ pathname: "/cart.html" })],
18
18
  stubBefore: (ctx) => {
19
19
  ctx.title("Open cart");
20
- ctx.todos(["Find the product", "Add to cart", "Open cart"]);
20
+ ctx.todoId("saucedemo-cart");
21
+ ctx.todos([
22
+ { id: "cart-find", text: "Find the product" },
23
+ { id: "cart-add", text: "Add to cart" },
24
+ { id: "cart-open", text: "Open cart" },
25
+ ]);
21
26
  ctx.todoIndex(2);
22
27
  ctx.zoom(1.4);
23
28
  ctx.zoomOut(false);
@@ -36,7 +36,12 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
36
36
  // Demo: zoom product -> add (stay zoomed), then badge after act.
37
37
  stubBefore: (ctx) => {
38
38
  ctx.title("Pick a product");
39
- ctx.todos(["Find the product", "Add to cart", "Open cart"]);
39
+ ctx.todoId("saucedemo-cart");
40
+ ctx.todos([
41
+ { id: "cart-find", text: "Find the product" },
42
+ { id: "cart-add", text: "Add to cart" },
43
+ { id: "cart-open", text: "Open cart" },
44
+ ]);
40
45
  ctx.todoIndex(0);
41
46
  ctx.zoom(1.35);
42
47
  ctx.zoomOut(false);
@@ -63,7 +68,12 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
63
68
  },
64
69
  stubAfter: (ctx) => {
65
70
  ctx.banner("In the cart");
66
- ctx.todos(["Find the product", "Add to cart", "Open cart"]);
71
+ ctx.todoId("saucedemo-cart");
72
+ ctx.todos([
73
+ { id: "cart-find", text: "Find the product" },
74
+ { id: "cart-add", text: "Add to cart" },
75
+ { id: "cart-open", text: "Open cart" },
76
+ ]);
67
77
  ctx.todoIndex(1);
68
78
  ctx.zoomOut(false);
69
79
  ctx.ring("badge", {
@@ -49,7 +49,12 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
49
49
  // sequential highlight queue (appear / dwell / fade), focus + color.
50
50
  stubBefore: (ctx) => {
51
51
  ctx.title("Signing in");
52
- ctx.todos(["Enter username", "Enter password", "Click Login"]);
52
+ ctx.todoId("saucedemo-login");
53
+ ctx.todos([
54
+ { id: "login-user", text: "Enter username" },
55
+ { id: "login-pass", text: "Enter password" },
56
+ { id: "login-submit", text: "Click Login" },
57
+ ]);
53
58
  ctx.todoIndex(0);
54
59
  ctx.zoom(1.35);
55
60
  ctx.highlights({
@@ -82,7 +87,12 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
82
87
  // inventory shelf. Success (LoggedIn) hands off to shopFlow.
83
88
  if (ctx.out && ctx.out.__state === "LoginPage") {
84
89
  ctx.title("Login blocked");
85
- ctx.todos(["Enter username", "Enter password", "Click Login"]);
90
+ ctx.todoId("saucedemo-login");
91
+ ctx.todos([
92
+ { id: "login-user", text: "Enter username" },
93
+ { id: "login-pass", text: "Enter password" },
94
+ { id: "login-submit", text: "Click Login" },
95
+ ]);
86
96
  ctx.todoIndex(2);
87
97
  ctx.ring("error", {
88
98
  selector: '[data-test="error"]',
@@ -95,7 +105,12 @@ export const SubmitLoginActionBlock = defineMethodBlock<LoginPage, LoginSubmitOu
95
105
  return;
96
106
  }
97
107
  ctx.banner("Inventory");
98
- ctx.todos(["Enter username", "Enter password", "Click Login"]);
108
+ ctx.todoId("saucedemo-login");
109
+ ctx.todos([
110
+ { id: "login-user", text: "Enter username" },
111
+ { id: "login-pass", text: "Enter password" },
112
+ { id: "login-submit", text: "Click Login" },
113
+ ]);
99
114
  ctx.todoIndex(2);
100
115
  // Landed on inventory - hand off to shopFlow for product -> cart zoom.
101
116
  ctx.ring("shelf", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.44",
3
+ "version": "0.12.46",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",