waygraph 0.12.21 → 0.12.23

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/bin/waygraph CHANGED
@@ -1,13 +1,48 @@
1
- #!/bin/sh
2
- # Two cwd/symlink pitfalls this resolves, both real, both hit in practice:
3
- # - `--import tsx/esm` is a bare specifier - Node resolves it as if from an
4
- # unnamed module at the CALLER's cwd, not this script's install
5
- # directory, so it broke when invoked from anywhere else.
6
- # - `npm link` makes this script reachable via a SYMLINK (e.g.
7
- # ~/.nvm/.../bin/waygraph) - `dirname "$0"` alone follows argv[0], not the
8
- # symlink target, so it resolved to the nvm bin dir instead of this
9
- # package. `readlink -f` follows the symlink to the real file first.
10
- SOURCE="$(readlink -f "$0")"
11
- DIR="$(cd "$(dirname "$SOURCE")/.." && pwd)"
12
- TSX_ESM="$(cd "$DIR" && node -e "console.log(require.resolve('tsx/esm'))")"
13
- exec node --import "$TSX_ESM" "$DIR/dist/cli.js" "$@"
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cross-platform CLI launcher.
4
+ *
5
+ * Why Node (not #!/bin/sh + readlink -f):
6
+ * - macOS BSD `readlink` has no `-f` (GNU) -> "illegal option -- f" / broken bin
7
+ * - Windows npm expects a Node shebang for `.cmd` shims
8
+ *
9
+ * Still registers `tsx/esm` before `dist/cli.js` so consumer `.block.ts` /
10
+ * `.flow.ts` dynamic imports resolve the same as the old shell wrapper.
11
+ */
12
+ import { spawn } from "node:child_process";
13
+ import { createRequire } from "node:module";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
18
+ const require = createRequire(join(pkgRoot, "package.json"));
19
+ let tsxEsm;
20
+ try {
21
+ tsxEsm = require.resolve("tsx/esm");
22
+ } catch (err) {
23
+ console.error(
24
+ "waygraph: cannot resolve dependency \"tsx\" (required to load .ts Blocks).\n" +
25
+ " Reinstall: npm install waygraph\n" +
26
+ ` Detail: ${err instanceof Error ? err.message : String(err)}`,
27
+ );
28
+ process.exit(1);
29
+ }
30
+
31
+ const cli = join(pkgRoot, "dist", "cli.js");
32
+ const child = spawn(
33
+ process.execPath,
34
+ ["--import", tsxEsm, cli, ...process.argv.slice(2)],
35
+ { stdio: "inherit", env: process.env, windowsHide: true },
36
+ );
37
+
38
+ child.on("error", (err) => {
39
+ console.error(`waygraph: failed to start CLI: ${err.message}`);
40
+ process.exit(1);
41
+ });
42
+ child.on("exit", (code, signal) => {
43
+ if (signal) {
44
+ process.kill(process.pid, signal);
45
+ return;
46
+ }
47
+ process.exit(code ?? 1);
48
+ });
@@ -468,22 +468,16 @@ function printCliMenu(menu, library) {
468
468
  console.log(" [q] quit");
469
469
  console.log("");
470
470
  }
471
- async function cliPick(menu) {
472
- const rl = createInterface({ input, output });
473
- try {
474
- while (true) {
475
- const ans = (await rl.question("Choose: ")).trim().toLowerCase();
476
- if (ans === "q" || ans === "quit")
477
- return { type: "quit" };
478
- const n = Number(ans);
479
- if (Number.isInteger(n) && n >= 1 && n <= menu.flat.length) {
480
- return { type: "pick", index: n - 1 };
481
- }
482
- console.log("Enter a number from the list, or q to quit.");
471
+ async function cliPick(menu, rl) {
472
+ while (true) {
473
+ const ans = (await rl.question("Choose: ")).trim().toLowerCase();
474
+ if (ans === "q" || ans === "quit")
475
+ return { type: "quit" };
476
+ const n = Number(ans);
477
+ if (Number.isInteger(n) && n >= 1 && n <= menu.flat.length) {
478
+ return { type: "pick", index: n - 1 };
483
479
  }
484
- }
485
- finally {
486
- rl.close();
480
+ console.log("Enter a number from the list, or q to quit.");
487
481
  }
488
482
  }
489
483
  /**
@@ -544,6 +538,7 @@ export async function runAutoExplore(projectDir, options = {}) {
544
538
  wiredPage = target;
545
539
  }
546
540
  let here = null;
541
+ const cliRl = cli ? createInterface({ input, output }) : null;
547
542
  try {
548
543
  for (;;) {
549
544
  page = await ensureLivePage(context, page, startUrl);
@@ -572,9 +567,9 @@ export async function runAutoExplore(projectDir, options = {}) {
572
567
  break;
573
568
  }
574
569
  let pick;
575
- if (cli) {
570
+ if (cli && cliRl) {
576
571
  printCliMenu(menu, library.byName);
577
- pick = await cliPick(menu);
572
+ pick = await cliPick(menu, cliRl);
578
573
  }
579
574
  else {
580
575
  pick = await headfulPick(page, menu, library.byName, mem, () => pickWait.promise);
@@ -614,6 +609,7 @@ export async function runAutoExplore(projectDir, options = {}) {
614
609
  }
615
610
  }
616
611
  finally {
612
+ cliRl?.close();
617
613
  await context.close();
618
614
  await browser.close();
619
615
  }
package/dist/cli.js CHANGED
@@ -224,6 +224,7 @@ import {
224
224
  demoPaceIsSlow,
225
225
  formatDemoPaceBadge,
226
226
  formatDemoPaceLabel,
227
+ normalizeTodos,
227
228
  } from "waygraph";
228
229
 
229
230
  function walkDir(dir, pattern) {
@@ -507,6 +508,14 @@ const RING_CSS =
507
508
  "#wg-panel .wg-pace[data-pace-kind=blitz] .wg-pace-badge{background:#22C55E;color:#052e16;}" +
508
509
  "#wg-panel .wg-pace[data-pace-kind=ms] .wg-pace-badge{background:#3B82F6;color:#fff;}" +
509
510
  "#wg-panel .wg-narration{margin:0 0 12px;font:italic 14px/1.4 system-ui,sans-serif;color:#f0e8ff;}" +
511
+ "#wg-todos{list-style:none;margin:0 0 12px;padding:8px 10px;background:#0f0620;border-radius:8px;" +
512
+ "border:1px solid #3a2a60;}" +
513
+ "#wg-todos li{display:flex;gap:8px;align-items:flex-start;margin:0 0 6px;font:600 12.5px/1.35 system-ui,sans-serif;}" +
514
+ "#wg-todos li:last-child{margin-bottom:0;}" +
515
+ "#wg-todos .wg-todo-mark{flex:0 0 auto;width:1.1em;text-align:center;}" +
516
+ "#wg-todos .wg-todo-done{color:#6ee7b7;text-decoration:line-through;opacity:.85;}" +
517
+ "#wg-todos .wg-todo-current{color:#fff;}" +
518
+ "#wg-todos .wg-todo-pending{color:#9a7ad1;}" +
510
519
  "#wg-progress{height:4px;background:#2a1650;border-radius:2px;margin:0 0 12px;overflow:hidden;}" +
511
520
  "#wg-progress-bar{height:100%;background:#7C3AED;border-radius:2px;transition:width .3s ease;}" +
512
521
  // A real tab bar for episodes - the currently-active episode reads as
@@ -767,6 +776,31 @@ async function installOverlay(page, title) {
767
776
  const ringLabel = document.getElementById("wg-ring-label");
768
777
  if (ring) ring.style.opacity = "0";
769
778
  if (ringLabel) ringLabel.style.opacity = "0";
779
+ if (window.__wgClearZoom) window.__wgClearZoom();
780
+ };
781
+ window.__wgClearZoom = () => {
782
+ document.querySelectorAll("[data-wg-zoomed=\\"1\\"]").forEach((el) => {
783
+ el.style.removeProperty("transform");
784
+ el.style.removeProperty("transform-origin");
785
+ el.style.removeProperty("transition");
786
+ el.style.removeProperty("z-index");
787
+ el.style.removeProperty("position");
788
+ delete el.dataset.wgZoomed;
789
+ });
790
+ };
791
+ window.__wgApplyZoom = (sel, scale) => {
792
+ if (window.__wgClearZoom) window.__wgClearZoom();
793
+ const n = Number(scale);
794
+ if (!Number.isFinite(n) || n <= 1.001) return;
795
+ const el = document.querySelector(sel);
796
+ if (!el) return;
797
+ const st = getComputedStyle(el);
798
+ if (st.position === "static") el.style.position = "relative";
799
+ el.style.zIndex = "2147483645";
800
+ el.style.transition = "transform .35s ease";
801
+ el.style.transformOrigin = "center center";
802
+ el.style.transform = "scale(" + n + ")";
803
+ el.dataset.wgZoomed = "1";
770
804
  };
771
805
  const POSITIONS = ["left", "center", "right"];
772
806
  const applyPos = (el, pos) => {
@@ -988,7 +1022,8 @@ async function renderBeforeStep(page, info) {
988
1022
  paceHtml +
989
1023
  "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
990
1024
  "<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>" +
991
- narrationHtml;
1025
+ narrationHtml +
1026
+ todosHtmlFromInfo(info, esc);
992
1027
  if (info.keys.length === 0) {
993
1028
  html += "<div class=\\"wg-key\\">(no MemKeys required)</div>";
994
1029
  }
@@ -1021,12 +1056,7 @@ async function renderBeforeStep(page, info) {
1021
1056
  }
1022
1057
  if (window.__wgWirePanelChrome) {
1023
1058
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1024
- stepLabel:
1025
- (info.index + 1) +
1026
- " / " +
1027
- info.total +
1028
- (info.blockName ? " · " + info.blockName : "") +
1029
- (info.paceBadge ? " · " + info.paceBadge : ""),
1059
+ stepLabel: miniStepLabel(info),
1030
1060
  forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1031
1061
  });
1032
1062
  }
@@ -1103,6 +1133,102 @@ async function renderBeforeStep(page, info) {
1103
1133
  .catch(() => {});
1104
1134
  }
1105
1135
 
1136
+ async function ensureSelectorInView(page, selector) {
1137
+ try {
1138
+ const loc = page.locator(selector).first();
1139
+ await loc.scrollIntoViewIfNeeded().catch(() => {});
1140
+ await page
1141
+ .evaluate((sel) => {
1142
+ const el = document.querySelector(sel);
1143
+ if (!el) return;
1144
+ try {
1145
+ el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
1146
+ } catch {
1147
+ try {
1148
+ el.scrollIntoView(true);
1149
+ } catch {
1150
+ /* ignore */
1151
+ }
1152
+ }
1153
+ let p = el.parentElement;
1154
+ while (p && p !== document.documentElement && p !== document.body) {
1155
+ const st = getComputedStyle(p);
1156
+ const ox = st.overflowX;
1157
+ const oy = st.overflowY;
1158
+ if (/(auto|scroll|overlay)/.test(ox) || /(auto|scroll|overlay)/.test(oy)) {
1159
+ const er = el.getBoundingClientRect();
1160
+ const pr = p.getBoundingClientRect();
1161
+ if (er.left < pr.left || er.right > pr.right || er.top < pr.top || er.bottom > pr.bottom) {
1162
+ p.scrollLeft += er.left + er.width / 2 - (pr.left + pr.width / 2);
1163
+ p.scrollTop += er.top + er.height / 2 - (pr.top + pr.height / 2);
1164
+ }
1165
+ }
1166
+ p = p.parentElement;
1167
+ }
1168
+ }, selector)
1169
+ .catch(() => {});
1170
+ } catch {
1171
+ /* best-effort */
1172
+ }
1173
+ }
1174
+
1175
+ async function applyHighlightZoom(page, selector, zoom) {
1176
+ const z = Number(zoom);
1177
+ if (!Number.isFinite(z) || z <= 1.001) {
1178
+ await page.evaluate(() => {
1179
+ if (window.__wgClearZoom) window.__wgClearZoom();
1180
+ }).catch(() => {});
1181
+ return;
1182
+ }
1183
+ await page
1184
+ .evaluate(
1185
+ ({ sel, scale }) => {
1186
+ if (window.__wgApplyZoom) window.__wgApplyZoom(sel, scale);
1187
+ },
1188
+ { sel: selector, scale: z },
1189
+ )
1190
+ .catch(() => {});
1191
+ }
1192
+
1193
+ function todosHtmlFromInfo(info, escFn) {
1194
+ const list = info.todos || [];
1195
+ if (!list.length) return "";
1196
+ return (
1197
+ "<ul id=\\"wg-todos\\">" +
1198
+ list
1199
+ .map((t) => {
1200
+ const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
1201
+ const mark = t.done ? "\\u2713" : t.current ? "\\u2192" : "\\u25CB";
1202
+ return (
1203
+ "<li class=\\"" +
1204
+ cls +
1205
+ "\\"><span class=\\"wg-todo-mark\\">" +
1206
+ mark +
1207
+ "</span><span>" +
1208
+ escFn(t.text || "") +
1209
+ "</span></li>"
1210
+ );
1211
+ })
1212
+ .join("") +
1213
+ "</ul>"
1214
+ );
1215
+ }
1216
+
1217
+ function miniStepLabel(info) {
1218
+ const ep =
1219
+ info.episodeNumber !== undefined && info.episodeNumber !== null
1220
+ ? "Ep " + info.episodeNumber + " \\u00b7 "
1221
+ : "";
1222
+ return (
1223
+ ep +
1224
+ (info.index + 1) +
1225
+ " / " +
1226
+ info.total +
1227
+ (info.blockName ? " \\u00b7 " + info.blockName : "") +
1228
+ (info.paceBadge ? " \\u00b7 " + info.paceBadge : "")
1229
+ );
1230
+ }
1231
+
1106
1232
  async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1107
1233
  const list = highlights || [];
1108
1234
  // defaultHoldMs: when set (fail path), use instead of legacy 900/200 so BUG
@@ -1112,12 +1238,47 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1112
1238
  for (let i = 0; i < list.length; i++) {
1113
1239
  const h = list[i];
1114
1240
  try {
1241
+ await ensureSelectorInView(page, h.selector);
1242
+ await applyHighlightZoom(page, h.selector, h.zoom);
1115
1243
  const box = await page.locator(h.selector).first().boundingBox();
1116
1244
  if (box) {
1117
1245
  await showRing(page, box, h.label, h.tone || "planned", {
1118
1246
  size: h.size,
1119
1247
  weight: h.weight,
1120
1248
  });
1249
+ if (h.todos && h.todos.length) {
1250
+ const rows = normalizeTodos(h.todos, h.todoIndex);
1251
+ await page
1252
+ .evaluate((todos) => {
1253
+ const ul = document.getElementById("wg-todos");
1254
+ if (!ul) return;
1255
+ ul.innerHTML = todos
1256
+ .map((t) => {
1257
+ const cls = t.current
1258
+ ? "wg-todo-current"
1259
+ : t.done
1260
+ ? "wg-todo-done"
1261
+ : "wg-todo-pending";
1262
+ const mark = t.done ? "\\u2713" : t.current ? "\\u2192" : "\\u25CB";
1263
+ const esc = (s) =>
1264
+ String(s)
1265
+ .replace(/&/g, "&amp;")
1266
+ .replace(/</g, "&lt;")
1267
+ .replace(/>/g, "&gt;");
1268
+ return (
1269
+ "<li class=\\"" +
1270
+ cls +
1271
+ "\\"><span class=\\"wg-todo-mark\\">" +
1272
+ mark +
1273
+ "</span><span>" +
1274
+ esc(t.text || "") +
1275
+ "</span></li>"
1276
+ );
1277
+ })
1278
+ .join("");
1279
+ }, rows)
1280
+ .catch(() => {});
1281
+ }
1121
1282
  const authored = resolveFixtureDwellMs(h, { gatesFast, pace });
1122
1283
  const legacyMs = i === list.length - 1 ? 200 : 900;
1123
1284
  const holdMs =
@@ -1261,6 +1422,7 @@ async function renderAfterStep(page, info) {
1261
1422
  paceHtml +
1262
1423
  "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1263
1424
  "<h3>" + heading + "</h3>" +
1425
+ todosHtmlFromInfo(info, escA) +
1264
1426
  resultHtml +
1265
1427
  gateHtml;
1266
1428
  if (isNewPanel) {
@@ -1269,12 +1431,7 @@ async function renderAfterStep(page, info) {
1269
1431
  }
1270
1432
  if (window.__wgWirePanelChrome) {
1271
1433
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1272
- stepLabel:
1273
- (info.index + 1) +
1274
- " / " +
1275
- info.total +
1276
- (info.blockName ? " · " + info.blockName : "") +
1277
- (info.paceBadge ? " · " + info.paceBadge : ""),
1434
+ stepLabel: miniStepLabel(info),
1278
1435
  forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1279
1436
  });
1280
1437
  }
@@ -1376,12 +1533,7 @@ async function renderStepError(page, info) {
1376
1533
  }
1377
1534
  if (window.__wgWirePanelChrome) {
1378
1535
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1379
- stepLabel:
1380
- (info.index + 1) +
1381
- " / " +
1382
- info.total +
1383
- (info.blockName ? " · " + info.blockName : "") +
1384
- (info.paceBadge ? " · " + info.paceBadge : ""),
1536
+ stepLabel: miniStepLabel(info),
1385
1537
  forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1386
1538
  });
1387
1539
  }
@@ -1609,6 +1761,8 @@ async function presentSlides(page, slides, _gate, opts) {
1609
1761
  await installOverlay(page, title);
1610
1762
  if (s.selector) {
1611
1763
  try {
1764
+ await ensureSelectorInView(page, s.selector);
1765
+ await applyHighlightZoom(page, s.selector, s.zoom);
1612
1766
  const box = await page.locator(s.selector).first().boundingBox();
1613
1767
  if (box) await showRing(page, box, caption, slideTone, slideStyle);
1614
1768
  } catch {
@@ -1696,11 +1850,14 @@ async function presentSlides(page, slides, _gate, opts) {
1696
1850
  if (window.__wgWirePanelChrome) {
1697
1851
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1698
1852
  stepLabel:
1853
+ (info.episodeNumber !== undefined && info.episodeNumber !== null
1854
+ ? "Ep " + info.episodeNumber + " \\u00b7 "
1855
+ : "") +
1699
1856
  "Slide " +
1700
1857
  (info.index + 1) +
1701
1858
  " / " +
1702
1859
  info.total +
1703
- (info.blockName ? " · " + info.blockName : ""),
1860
+ (info.blockName ? " \\u00b7 " + info.blockName : ""),
1704
1861
  forceCollapsed: false,
1705
1862
  });
1706
1863
  }
@@ -2416,6 +2573,12 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2416
2573
  episodeTitle: r.episodeTitle,
2417
2574
  stepperMode,
2418
2575
  forceCollapsed,
2576
+ todos: (() => {
2577
+ for (const h of stubBeforeRef.current || []) {
2578
+ if (h.todos && h.todos.length) return normalizeTodos(h.todos, h.todoIndex);
2579
+ }
2580
+ return [];
2581
+ })(),
2419
2582
  ...paceSpeak,
2420
2583
  });
2421
2584
  const edits = await gate();
@@ -2487,6 +2650,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2487
2650
  tone: styled.tone,
2488
2651
  size: styled.size,
2489
2652
  weight: styled.weight,
2653
+ todos: styled.todos,
2654
+ todoIndex: styled.todoIndex,
2655
+ zoom: styled.zoom,
2490
2656
  };
2491
2657
  });
2492
2658
  await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
@@ -2569,6 +2735,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2569
2735
  tone: styled.tone,
2570
2736
  size: styled.size,
2571
2737
  weight: styled.weight,
2738
+ todos: styled.todos,
2739
+ todoIndex: styled.todoIndex,
2740
+ zoom: styled.zoom,
2572
2741
  };
2573
2742
  },
2574
2743
  );
@@ -2596,6 +2765,12 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2596
2765
  episodeNumber: r.episodeNumber,
2597
2766
  episodeTitle: r.episodeTitle,
2598
2767
  stepperMode,
2768
+ todos: (() => {
2769
+ for (const h of highlights || []) {
2770
+ if (h.todos && h.todos.length) return normalizeTodos(h.todos, h.todoIndex);
2771
+ }
2772
+ return [];
2773
+ })(),
2599
2774
  // After a nav under auto-next, stay compact until the next action block expands.
2600
2775
  forceCollapsed: !!process.env.WAYGRAPH_VIDEO || ((await currentAutoplay()) && isNavBlock),
2601
2776
  });
@@ -2690,7 +2865,7 @@ async function runChainedFlows(chainFlow, chainFlows, context, mem, page) {
2690
2865
  /** Parses WAYGRAPH_VIDEO_VIEWPORT / --video-viewport: "1920x1080" or "1920,1080". */
2691
2866
  function parseVideoViewport(raw) {
2692
2867
  if (!raw || !raw.trim()) return null;
2693
- const m = raw.trim().match(/^(\d{3,5})[xX,](\d{3,5})$/);
2868
+ const m = raw.trim().match(/^(\\d{3,5})[xX,](\\d{3,5})$/);
2694
2869
  if (!m) return null;
2695
2870
  const width = Number(m[1]);
2696
2871
  const height = Number(m[2]);
@@ -2981,7 +3156,13 @@ async function main() {
2981
3156
  ...(executablePath ? { executablePath } : {}),
2982
3157
  });
2983
3158
  const contextOpts = fixedVideoViewport
2984
- ? { baseURL, viewport: fixedVideoViewport }
3159
+ ? {
3160
+ baseURL,
3161
+ viewport: fixedVideoViewport,
3162
+ // Lock DPR so --video-viewport 800x450 stays 800x450 in the .webm
3163
+ // (HiDPI hosts otherwise scale recordVideo to e.g. 1920x1080).
3164
+ deviceScaleFactor: 1,
3165
+ }
2985
3166
  : step
2986
3167
  ? { baseURL, viewport: null }
2987
3168
  : { baseURL, viewport: { width: 1280, height: 720 } };
@@ -24,6 +24,18 @@ export type FixtureDurationFields = {
24
24
  */
25
25
  fastMode?: number;
26
26
  };
27
+ /**
28
+ * One checklist row for episode planning (demo panel).
29
+ * Prefer {@link WaygraphHighlightStub.todoIndex} for sequential done/current.
30
+ */
31
+ export type WaygraphTodoItem = {
32
+ text: string;
33
+ /** Explicit done. If omitted, derived from todoIndex (index < todoIndex). */
34
+ done?: boolean;
35
+ /** Explicit current. If omitted, derived from todoIndex (index === todoIndex). */
36
+ current?: boolean;
37
+ };
38
+ export type WaygraphTodoInput = string | WaygraphTodoItem;
27
39
  /** One named highlight slot on a block (selector + caption). */
28
40
  export interface WaygraphHighlightStub extends FixtureDurationFields {
29
41
  selector: string;
@@ -50,9 +62,25 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
50
62
  * Aliases: regular, strong.
51
63
  */
52
64
  weight?: HighlightWeight;
65
+ /**
66
+ * Simple episode checklist (demo panel). Same lifecycle as this stub
67
+ * (stubBefore / stubAfter / stubOnError). Strings or `{ text, done?, current? }`.
68
+ */
69
+ todos?: readonly WaygraphTodoInput[];
70
+ /**
71
+ * Which todo is current (0-based). Rows before = done, after = pending.
72
+ * Authored `done`/`current` on an item still win when set.
73
+ * Bump in a stub phase function for sequential plans.
74
+ */
75
+ todoIndex?: number;
76
+ /**
77
+ * Magnify the target element while this ring is shown (e.g. `1.35`).
78
+ * Cleared when the ring hides / next highlight starts. `1` or omit = off.
79
+ */
80
+ zoom?: number;
53
81
  }
54
82
  /** Flow fixture patch: label required; selector optional (inherits from stub). */
55
- export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone" | "size" | "weight"> & FixtureDurationFields;
83
+ export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone" | "size" | "weight" | "todos" | "todoIndex" | "zoom"> & FixtureDurationFields;
56
84
  export type HighlightStubPhase = Record<string, WaygraphHighlightStub>;
57
85
  export type HighlightStubPhaseOrFn<Out extends Checkpoint<string>> = HighlightStubPhase | ((out: Out) => HighlightStubPhase);
58
86
  /**
@@ -69,6 +97,8 @@ export interface WaygraphSlide extends FixtureDurationFields {
69
97
  tone?: HighlightTone;
70
98
  size?: HighlightSize;
71
99
  weight?: HighlightWeight;
100
+ /** Magnify selector target while this slide is showing (same as stub zoom). */
101
+ zoom?: number;
72
102
  }
73
103
  /**
74
104
  * Demo ring tones (iconified).
@@ -188,6 +218,10 @@ export interface WaygraphHighlight {
188
218
  tone?: HighlightTone;
189
219
  }
190
220
  export type ResolvedHighlight = WaygraphHighlightStub;
221
+ /**
222
+ * Normalize checklist rows; apply todoIndex for sequential done/current.
223
+ */
224
+ export declare function normalizeTodos(todos: readonly WaygraphTodoInput[] | undefined, todoIndex?: number): WaygraphTodoItem[];
191
225
  /**
192
226
  * Resolve ordered highlight slots for a phase, merging flow fixtures over block stubs.
193
227
  * Shim: empty stubAfter + legacy `instruction.highlights` -> auto-keyed stubAfter.
@@ -289,11 +289,50 @@ function pickStyleFields(base, patch) {
289
289
  out.weight = normalizeHighlightWeight(weight);
290
290
  return out;
291
291
  }
292
+ /**
293
+ * Normalize checklist rows; apply todoIndex for sequential done/current.
294
+ */
295
+ export function normalizeTodos(todos, todoIndex) {
296
+ if (!todos || todos.length === 0)
297
+ return [];
298
+ const idx = todoIndex !== undefined && Number.isFinite(todoIndex) ? Math.floor(todoIndex) : undefined;
299
+ return todos.map((t, i) => {
300
+ const text = typeof t === "string" ? t.trim() : String(t?.text ?? "").trim();
301
+ const item = { text: text || `Item ${i + 1}` };
302
+ if (typeof t === "object" && t) {
303
+ if (t.done !== undefined)
304
+ item.done = !!t.done;
305
+ if (t.current !== undefined)
306
+ item.current = !!t.current;
307
+ }
308
+ if (idx !== undefined) {
309
+ if (item.done === undefined)
310
+ item.done = i < idx;
311
+ if (item.current === undefined)
312
+ item.current = i === idx;
313
+ }
314
+ return item;
315
+ });
316
+ }
317
+ function pickTodoFields(base, patch) {
318
+ const out = {};
319
+ const todos = patch?.todos !== undefined ? patch.todos : base?.todos;
320
+ const todoIndex = patch?.todoIndex !== undefined ? patch.todoIndex : base?.todoIndex;
321
+ const zoom = patch?.zoom !== undefined ? patch.zoom : base?.zoom;
322
+ if (todos !== undefined)
323
+ out.todos = todos;
324
+ if (todoIndex !== undefined)
325
+ out.todoIndex = todoIndex;
326
+ if (zoom !== undefined && Number.isFinite(zoom) && zoom > 0)
327
+ out.zoom = zoom;
328
+ return out;
329
+ }
292
330
  function mergeSlot(base, patch) {
293
331
  if (!base && !patch)
294
332
  return null;
295
333
  const dwell = pickDurationFields(base, patch);
296
334
  const style = pickStyleFields(base, patch);
335
+ const todos = pickTodoFields(base, patch);
297
336
  if (!base && patch) {
298
337
  if (!patch.selector)
299
338
  return null;
@@ -304,6 +343,7 @@ function mergeSlot(base, patch) {
304
343
  ...(patch.tag ? { tag: patch.tag } : {}),
305
344
  ...style,
306
345
  ...dwell,
346
+ ...todos,
307
347
  };
308
348
  }
309
349
  if (base && !patch) {
@@ -312,6 +352,7 @@ function mergeSlot(base, patch) {
312
352
  ...(base.tone ? { tone: normalizeHighlightTone(base.tone) } : {}),
313
353
  ...(base.size ? { size: normalizeHighlightSize(base.size) } : {}),
314
354
  ...(base.weight ? { weight: normalizeHighlightWeight(base.weight) } : {}),
355
+ ...pickTodoFields(base, undefined),
315
356
  };
316
357
  }
317
358
  return {
@@ -325,6 +366,7 @@ function mergeSlot(base, patch) {
325
366
  ...(patch.tag !== undefined ? { tag: patch.tag } : base.tag ? { tag: base.tag } : {}),
326
367
  ...style,
327
368
  ...dwell,
369
+ ...todos,
328
370
  };
329
371
  }
330
372
  function phaseFromLegacyHighlights(highlights) {
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, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, 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, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, } from "./highlights.js";
6
+ export { resolveHighlightSlots, resolveSlides, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, 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, } 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, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, 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, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, 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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.21",
3
+ "version": "0.12.23",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",