waygraph 0.10.8 → 0.11.0

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
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Primary verbs (less is more):
6
6
  * auto Explore picker; `.flow.ts` / `--blocks From To` = run that path
7
- * demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--auto-play-video`
7
+ * demo Watch (step overlay); `--blocks` `--data` `--auto-next` `--fast` `--full`
8
8
  * run Execute; `--blocks` `--data` `--non-headless` `--video`
9
9
  *
10
10
  * Also: list / nav / validate / check / graph / init / try
@@ -200,6 +200,12 @@ const CHAIN_RUNNER_SCRIPT = `
200
200
  import { readdirSync, mkdirSync } from "node:fs";
201
201
  import { join } from "node:path";
202
202
  import { tmpdir } from "node:os";
203
+ import {
204
+ resolveHighlightSlots,
205
+ resolveSlides,
206
+ formatHighlightCaption,
207
+ hasAuthoredStubAfter,
208
+ } from "waygraph";
203
209
 
204
210
  function walkDir(dir, pattern) {
205
211
  const results = [];
@@ -395,8 +401,8 @@ const RING_CSS =
395
401
  // pushed back onto screen, same width, never shrunk. See
396
402
  // window.__wgPositionRing in installOverlay.
397
403
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
398
- "white-space:nowrap;padding:4px 9px;border-radius:7px;background:#7C3AED;color:#fff;" +
399
- "font:600 12px/1.2 system-ui,sans-serif;transition:opacity .3s ease;}" +
404
+ "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
405
+ "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease;}" +
400
406
  // Mouse cursor icon that travels to a target before it's acted on, plus a
401
407
  // quick expanding ripple at the moment of a click - same idea as
402
408
  // help-center-clip-engine's #clip-cursor/#clip-ring (video-pipeline). The
@@ -469,12 +475,16 @@ const RING_CSS =
469
475
  "@keyframes wg-ep-enter{0%{box-shadow:0 0 0 0 rgba(124,58,237,.55);}" +
470
476
  "100%{box-shadow:0 0 0 10px rgba(124,58,237,0);}}" +
471
477
  "#wg-episodes .wg-ep-entered{border-radius:6px;animation:wg-ep-enter 1.1s ease-out;}" +
472
- // The block breadcrumb below it is scoped to the CURRENT episode's own
473
- // blocks only, not the whole chain - "in episode 2 it shows 2nd to the
474
- // last... it should start from the first block" (Dan). An ad hoc block
475
- // chain with no episodes still shows the whole chain here, unchanged.
476
- "#wg-modules{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px;}" +
477
- "#wg-modules .wg-mod{padding:3px 9px;border-radius:6px;font:600 11px system-ui,sans-serif;}" +
478
+ // Block strip: carousel (default) or classic wrap (--full).
479
+ "#wg-modules.wg-modules-carousel{display:flex;flex-wrap:nowrap;gap:8px;margin:0 0 10px;" +
480
+ "overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;" +
481
+ "padding:4px 2px 8px;scrollbar-width:thin;}" +
482
+ "#wg-modules.wg-modules-carousel .wg-mod{flex:0 0 auto;scroll-snap-align:center;" +
483
+ "padding:6px 12px;border-radius:999px;font:600 12px system-ui,sans-serif;}" +
484
+ "#wg-modules.wg-modules-carousel .wg-mod-current{transform:scale(1.06);" +
485
+ "box-shadow:0 0 0 2px rgba(124,58,237,.45);}" +
486
+ "#wg-modules.wg-modules-full{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px;}" +
487
+ "#wg-modules.wg-modules-full .wg-mod{padding:3px 9px;border-radius:6px;font:600 11px system-ui,sans-serif;}" +
478
488
  "#wg-modules .wg-mod-done{background:#2a1650;color:#9a7ad1;}" +
479
489
  "#wg-modules .wg-mod-current{background:#7C3AED;color:#fff;}" +
480
490
  "#wg-modules .wg-mod-upcoming{background:transparent;color:#5a4a80;border:1px solid #3a2a60;}" +
@@ -525,22 +535,23 @@ const RING_CSS =
525
535
  "#wg-panel .wg-expected-heading{color:#ffcf7a;}" +
526
536
  "#wg-panel .wg-expected-reason{font:600 12.5px/1.5 system-ui,sans-serif;background:#2a2410;" +
527
537
  "color:#ffe6ae;border-radius:8px;padding:10px;margin:0 0 8px;}" +
528
- // Hide / Show chrome + mobile bottom-sheet layout (Dan: stepper + auto panels).
538
+ // Hide / Show chrome: collapsed = compact "N / M · block" pill (not empty chrome).
529
539
  "#wg-panel .wg-chrome{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 0 8px;}" +
530
540
  "#wg-panel .wg-chrome-title{font:700 11px/1.2 system-ui,sans-serif;color:#c9a6ff;" +
531
541
  "letter-spacing:.04em;text-transform:uppercase;}" +
532
542
  "#wg-panel button.wg-hide-btn{margin:0;padding:4px 10px;font:600 11px system-ui,sans-serif;" +
533
543
  "background:#3a2a60;color:#e8dcff;border:1px solid #5b3aa8;border-radius:6px;cursor:pointer;}" +
534
544
  "#wg-panel button.wg-hide-btn:hover{background:#4b2a80;}" +
535
- "#wg-panel.wg-collapsed{width:auto;max-width:90vw;padding:8px 12px;max-height:none;overflow:hidden;}" +
545
+ "#wg-panel.wg-collapsed{width:auto;max-width:92vw;padding:8px 12px;max-height:none;overflow:hidden;}" +
536
546
  "#wg-panel.wg-collapsed .wg-body{display:none;}" +
537
547
  "#wg-panel.wg-collapsed .wg-chrome{margin:0;}" +
548
+ "#wg-panel.wg-collapsed .wg-chrome-title{font:700 13px/1.25 system-ui,sans-serif;color:#fff;" +
549
+ "letter-spacing:0;text-transform:none;}" +
538
550
  "@media (max-width:640px){" +
539
551
  "#wg-panel{left:8px;right:8px;bottom:8px;transform:none;max-width:none;width:auto;" +
540
552
  "max-height:min(55vh,calc(100vh - 16px));padding:12px 14px;border-radius:12px;}" +
541
553
  "#wg-panel.wg-collapsed{left:50%;right:auto;transform:translateX(-50%);width:auto;}" +
542
554
  "#wg-episodes{overflow-x:auto;-webkit-overflow-scrolling:touch;flex-wrap:nowrap;}" +
543
- "#wg-modules{gap:4px;}" +
544
555
  "#wg-panel .wg-narration{font-size:13px;}" +
545
556
  "#wg-panel button{width:100%;}" +
546
557
  "#wg-panel .wg-error-actions{flex-direction:column;}" +
@@ -736,8 +747,11 @@ async function installOverlay(page, title) {
736
747
  }
737
748
  if (iconLink.href !== favicon) iconLink.href = favicon;
738
749
  // Hide/Show for #wg-panel - call after every panel.innerHTML refresh.
739
- window.__wgWirePanelChrome = (panel, storageKey, chromeTitle) => {
750
+ // Collapsed title shows "N / M · block" (compact progress). forceCollapsed
751
+ // is for auto-next navs / video - does not overwrite the user's Hide preference.
752
+ window.__wgWirePanelChrome = (panel, storageKey, chromeTitle, opts) => {
740
753
  if (!panel) return;
754
+ opts = opts || {};
741
755
  if (!panel.querySelector(":scope > .wg-chrome")) {
742
756
  const body = document.createElement("div");
743
757
  body.className = "wg-body";
@@ -757,31 +771,56 @@ async function installOverlay(page, title) {
757
771
  panel.appendChild(chrome);
758
772
  panel.appendChild(body);
759
773
  }
760
- const apply = (hidden) => {
774
+ const stepLabel = opts.stepLabel || panel.dataset.wgStepLabel || "";
775
+ if (stepLabel) panel.dataset.wgStepLabel = stepLabel;
776
+ const apply = (hidden, persist) => {
761
777
  panel.classList.toggle("wg-collapsed", hidden);
762
778
  const t = panel.querySelector("[data-wg-toggle]");
779
+ const titleEl = panel.querySelector(".wg-chrome-title");
780
+ const label = panel.dataset.wgStepLabel || stepLabel;
781
+ if (titleEl) {
782
+ titleEl.textContent = hidden
783
+ ? (label || chromeTitle || "waygraph demo")
784
+ : (chromeTitle || "waygraph demo");
785
+ }
763
786
  if (t) t.textContent = hidden ? "Show" : "Hide";
764
- try {
765
- localStorage.setItem(storageKey, hidden ? "1" : "0");
766
- } catch {
767
- /* private mode */
787
+ if (persist !== false) {
788
+ try {
789
+ localStorage.setItem(storageKey, hidden ? "1" : "0");
790
+ } catch {
791
+ /* private mode */
792
+ }
768
793
  }
769
794
  };
770
795
  let hidden = false;
771
- try {
772
- hidden = localStorage.getItem(storageKey) === "1";
773
- } catch {
774
- /* ignore */
796
+ if (opts.forceCollapsed === true) {
797
+ hidden = true;
798
+ } else if (opts.forceCollapsed === false) {
799
+ hidden = false;
800
+ } else {
801
+ try {
802
+ hidden = localStorage.getItem(storageKey) === "1";
803
+ } catch {
804
+ /* ignore */
805
+ }
775
806
  }
776
- apply(hidden);
807
+ apply(hidden, opts.forceCollapsed === true ? false : true);
777
808
  const toggle = panel.querySelector("[data-wg-toggle]");
778
809
  if (toggle && !toggle.dataset.wgWired) {
779
810
  toggle.dataset.wgWired = "1";
780
811
  toggle.addEventListener("click", (e) => {
781
812
  e.stopPropagation();
782
- apply(!panel.classList.contains("wg-collapsed"));
813
+ apply(!panel.classList.contains("wg-collapsed"), true);
783
814
  });
784
815
  }
816
+ const cur = panel.querySelector("#wg-modules .wg-mod-current");
817
+ if (cur && typeof cur.scrollIntoView === "function") {
818
+ try {
819
+ cur.scrollIntoView({ inline: "center", block: "nearest", behavior: "smooth" });
820
+ } catch {
821
+ cur.scrollIntoView(false);
822
+ }
823
+ }
785
824
  };
786
825
  },
787
826
  { title, favicon: WAYGRAPH_FAVICON, bannerPos, envAutoplay },
@@ -850,6 +889,7 @@ async function renderBeforeStep(page, info) {
850
889
  // step's position WITHIN that subset, not the whole chain's index -
851
890
  // "in episode 2 it shows 2nd to the last... it should start from the
852
891
  // first block" (Dan).
892
+ const modulesClass = info.stepperMode === "full" ? "wg-modules-full" : "wg-modules-carousel";
853
893
  const modulesHtml = info.allNames
854
894
  .map((name, idx) => {
855
895
  const cls = idx < info.moduleIndex ? "wg-mod-done" : idx === info.moduleIndex ? "wg-mod-current" : "wg-mod-upcoming";
@@ -864,7 +904,7 @@ async function renderBeforeStep(page, info) {
864
904
  let html =
865
905
  "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
866
906
  episodesHtml +
867
- "<div id=\\"wg-modules\\">" + modulesHtml + "</div>" +
907
+ "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
868
908
  "<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>" +
869
909
  narrationHtml;
870
910
  if (info.keys.length === 0) {
@@ -897,7 +937,12 @@ async function renderBeforeStep(page, info) {
897
937
  document.documentElement.appendChild(panel);
898
938
  requestAnimationFrame(() => panel.classList.add("wg-in"));
899
939
  }
900
- if (window.__wgWirePanelChrome) window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo");
940
+ if (window.__wgWirePanelChrome) {
941
+ window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
942
+ stepLabel: (info.index + 1) + " / " + info.total + (info.blockName ? " · " + info.blockName : ""),
943
+ forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
944
+ });
945
+ }
901
946
  // Live, human-readable preview of what a MemKey's raw JSON will
902
947
  // actually write - object fields become "Field: value" lines
903
948
  // (camelCase split the same way state tags already are); a
@@ -1040,6 +1085,7 @@ async function renderAfterStep(page, info) {
1040
1085
  : "Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + " done";
1041
1086
  const buttonLabel = info.isLast ? "Finish" : "Next \\u25B6";
1042
1087
  const escA = (s) => String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1088
+ const modulesClass = info.stepperMode === "full" ? "wg-modules-full" : "wg-modules-carousel";
1043
1089
  const modulesHtml = info.allNames
1044
1090
  .map((name, idx) => {
1045
1091
  const cls = idx <= info.moduleIndex ? "wg-mod-done" : "wg-mod-upcoming";
@@ -1094,7 +1140,7 @@ async function renderAfterStep(page, info) {
1094
1140
  panel.innerHTML =
1095
1141
  "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
1096
1142
  episodesHtml +
1097
- "<div id=\\"wg-modules\\">" + modulesHtml + "</div>" +
1143
+ "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1098
1144
  "<h3>" + heading + "</h3>" +
1099
1145
  resultHtml +
1100
1146
  gateHtml;
@@ -1102,7 +1148,12 @@ async function renderAfterStep(page, info) {
1102
1148
  document.documentElement.appendChild(panel);
1103
1149
  requestAnimationFrame(() => panel.classList.add("wg-in"));
1104
1150
  }
1105
- if (window.__wgWirePanelChrome) window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo");
1151
+ if (window.__wgWirePanelChrome) {
1152
+ window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1153
+ stepLabel: (info.index + 1) + " / " + info.total + (info.blockName ? " · " + info.blockName : ""),
1154
+ forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1155
+ });
1156
+ }
1106
1157
  panel.querySelectorAll(".wg-toggle-btn").forEach((btn) => {
1107
1158
  btn.addEventListener("click", () => {
1108
1159
  const wantPretty = btn.getAttribute("data-mode") === "pretty";
@@ -1169,6 +1220,7 @@ async function renderStepError(page, info) {
1169
1220
  .join("") +
1170
1221
  "</div>"
1171
1222
  : "";
1223
+ const modulesClass = info.stepperMode === "full" ? "wg-modules-full" : "wg-modules-carousel";
1172
1224
  const modulesHtml = info.allNames
1173
1225
  .map((name, idx) => {
1174
1226
  const cls = idx === info.moduleIndex ? "wg-mod-current" : idx < info.moduleIndex ? "wg-mod-done" : "wg-mod-upcoming";
@@ -1186,7 +1238,7 @@ async function renderStepError(page, info) {
1186
1238
  : "";
1187
1239
  panel.innerHTML =
1188
1240
  episodesHtml +
1189
- "<div id=\\"wg-modules\\">" + modulesHtml + "</div>" +
1241
+ "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1190
1242
  "<h3 class=\\"" + (isExpected ? "wg-expected-heading" : "wg-error-heading") + "\\">" + headingText + "</h3>" +
1191
1243
  reasonHtml +
1192
1244
  "<div class=\\"wg-error-msg\\">" + esc(info.message) + "</div>" +
@@ -1198,7 +1250,12 @@ async function renderStepError(page, info) {
1198
1250
  document.documentElement.appendChild(panel);
1199
1251
  requestAnimationFrame(() => panel.classList.add("wg-in"));
1200
1252
  }
1201
- if (window.__wgWirePanelChrome) window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo");
1253
+ if (window.__wgWirePanelChrome) {
1254
+ window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1255
+ stepLabel: (info.index + 1) + " / " + info.total + (info.blockName ? " · " + info.blockName : ""),
1256
+ forceCollapsed: info.forceCollapsed === true ? true : info.forceCollapsed === false ? false : undefined,
1257
+ });
1258
+ }
1202
1259
  const runBtn = document.getElementById("wg-run");
1203
1260
  if (runBtn) runBtn.addEventListener("click", () => window.__wgNext({}));
1204
1261
  const retryBtn = document.getElementById("wg-error-retry");
@@ -1308,6 +1365,9 @@ async function markStepRunning(page, opts) {
1308
1365
  panel.classList.add("wg-collapsed");
1309
1366
  const toggle = panel.querySelector("[data-wg-toggle]");
1310
1367
  if (toggle) toggle.textContent = "Show";
1368
+ const titleEl = panel.querySelector(".wg-chrome-title");
1369
+ const label = panel.dataset.wgStepLabel;
1370
+ if (titleEl && label) titleEl.textContent = label;
1311
1371
  }
1312
1372
  }
1313
1373
  }, { autoCollapsePanel: !!(opts && opts.autoCollapsePanel) })
@@ -1325,6 +1385,162 @@ async function showRing(page, box, label) {
1325
1385
  .catch(() => {});
1326
1386
  }
1327
1387
 
1388
+ /** Match a locator to a stubBefore slot by overlapping bounding boxes. */
1389
+ async function captionForLocator(page, locator, stubs, fallback) {
1390
+ if (!stubs || stubs.length === 0) return fallback;
1391
+ const box = await locator.boundingBox().catch(() => null);
1392
+ if (!box) return fallback;
1393
+ for (const s of stubs) {
1394
+ try {
1395
+ const b = await page.locator(s.selector).first().boundingBox();
1396
+ if (
1397
+ b &&
1398
+ Math.abs(b.x - box.x) < 4 &&
1399
+ Math.abs(b.y - box.y) < 4 &&
1400
+ Math.abs(b.width - box.width) < 8
1401
+ ) {
1402
+ return formatHighlightCaption(s);
1403
+ }
1404
+ } catch {
1405
+ /* skip */
1406
+ }
1407
+ }
1408
+ return fallback;
1409
+ }
1410
+
1411
+ /**
1412
+ * Multi-step yap captions - not block lifecycle. Each slide waits for Next
1413
+ * (or auto-next), optional ring via slide.selector.
1414
+ */
1415
+ async function presentSlides(page, slides, gate, opts) {
1416
+ const title = (opts && opts.title) || "waygraph demo";
1417
+ const blockName = (opts && opts.blockName) || "";
1418
+ for (let i = 0; i < slides.length; i++) {
1419
+ const s = slides[i];
1420
+ const caption = formatHighlightCaption(s);
1421
+ const isLast = i === slides.length - 1;
1422
+ await installOverlay(page, title);
1423
+ if (s.selector) {
1424
+ try {
1425
+ const box = await page.locator(s.selector).first().boundingBox();
1426
+ if (box) await showRing(page, box, caption);
1427
+ } catch {
1428
+ await hideRing(page);
1429
+ }
1430
+ } else {
1431
+ await hideRing(page);
1432
+ }
1433
+ await page
1434
+ .evaluate(
1435
+ (info) => {
1436
+ let panel = document.getElementById("wg-panel");
1437
+ const isNew = !panel;
1438
+ if (!panel) {
1439
+ panel = document.createElement("div");
1440
+ panel.id = "wg-panel";
1441
+ }
1442
+ panel.classList.remove("wg-collapsed", "wg-error", "wg-expected");
1443
+ const esc = (t) =>
1444
+ String(t).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
1445
+ panel.innerHTML =
1446
+ "<div class=\\"wg-chrome\\"><span class=\\"wg-chrome-title\\">waygraph demo</span>" +
1447
+ "<button type=\\"button\\" class=\\"wg-hide-btn\\" data-wg-toggle=\\"1\\">Hide</button></div>" +
1448
+ "<div class=\\"wg-body\\">" +
1449
+ "<h3>Slide " +
1450
+ (info.index + 1) +
1451
+ " / " +
1452
+ info.total +
1453
+ (info.blockName ? " · " + esc(info.blockName) : "") +
1454
+ "</h3>" +
1455
+ (info.tag
1456
+ ? "<div class=\\"wg-episode\\" style=\\"border:none;padding:0;margin:0 0 6px;font-size:11px;color:#c9a6ff\\">" +
1457
+ esc(info.tag) +
1458
+ "</div>"
1459
+ : "") +
1460
+ "<div class=\\"wg-narration\\">" +
1461
+ esc(info.caption) +
1462
+ "</div>" +
1463
+ (info.detail
1464
+ ? "<div class=\\"wg-result-pretty\\" style=\\"margin:0 0 12px;color:#f0e8ff\\">" +
1465
+ esc(info.detail) +
1466
+ "</div>"
1467
+ : "") +
1468
+ "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
1469
+ (info.autoNow ? " checked" : "") +
1470
+ "> Auto-advance</label></div>" +
1471
+ "<div id=\\"wg-gate-manual\\"" +
1472
+ (info.autoNow ? " style=\\"display:none\\"" : "") +
1473
+ "><button id=\\"wg-run\\">" +
1474
+ (info.isLast ? "Continue \\u25B6" : "Next slide \\u25B6") +
1475
+ "</button></div>" +
1476
+ "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" +
1477
+ (info.autoNow ? "" : " style=\\"display:none\\"") +
1478
+ ">Auto-advancing...</div>" +
1479
+ "</div>";
1480
+ if (isNew) {
1481
+ document.documentElement.appendChild(panel);
1482
+ requestAnimationFrame(() => panel.classList.add("wg-in"));
1483
+ }
1484
+ if (window.__wgWirePanelChrome) {
1485
+ window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1486
+ stepLabel:
1487
+ "Slide " +
1488
+ (info.index + 1) +
1489
+ " / " +
1490
+ info.total +
1491
+ (info.blockName ? " · " + info.blockName : ""),
1492
+ forceCollapsed: false,
1493
+ });
1494
+ }
1495
+ const cb = document.getElementById("wg-autoplay-cb");
1496
+ if (cb) {
1497
+ cb.addEventListener("change", () => {
1498
+ try {
1499
+ localStorage.setItem("wg-autoplay", cb.checked ? "1" : "0");
1500
+ } catch {
1501
+ /* ignore */
1502
+ }
1503
+ const manual = document.getElementById("wg-gate-manual");
1504
+ const auto = document.getElementById("wg-gate-auto");
1505
+ if (manual) manual.style.display = cb.checked ? "none" : "";
1506
+ if (auto) auto.style.display = cb.checked ? "" : "none";
1507
+ });
1508
+ }
1509
+ const runBtn = document.getElementById("wg-run");
1510
+ if (runBtn) runBtn.addEventListener("click", () => window.__wgNext({}));
1511
+ },
1512
+ {
1513
+ index: i,
1514
+ total: slides.length,
1515
+ caption: s.caption,
1516
+ detail: s.detail || "",
1517
+ tag: s.tag || "",
1518
+ blockName,
1519
+ isLast,
1520
+ autoNow: false,
1521
+ },
1522
+ )
1523
+ .catch(() => {});
1524
+ await page
1525
+ .evaluate(() => {
1526
+ try {
1527
+ const on = localStorage.getItem("wg-autoplay") === "1";
1528
+ const cb = document.getElementById("wg-autoplay-cb");
1529
+ if (cb) cb.checked = on;
1530
+ const manual = document.getElementById("wg-gate-manual");
1531
+ const auto = document.getElementById("wg-gate-auto");
1532
+ if (manual) manual.style.display = on ? "none" : "";
1533
+ if (auto) auto.style.display = on ? "" : "none";
1534
+ } catch {
1535
+ /* ignore */
1536
+ }
1537
+ })
1538
+ .catch(() => {});
1539
+ await gate();
1540
+ }
1541
+ await hideRing(page);
1542
+ }
1543
+
1328
1544
  async function hideRing(page) {
1329
1545
  await page
1330
1546
  .evaluate(() => {
@@ -1401,7 +1617,7 @@ async function wasJustNarrated(page) {
1401
1617
  .catch(() => false);
1402
1618
  }
1403
1619
 
1404
- function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1620
+ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef) {
1405
1621
  // Playwright's own slowMo ALREADY pauses after every single low-level
1406
1622
  // action it dispatches - and pressSequentially() fires one such action
1407
1623
  // PER CHARACTER. Also giving pressSequentially its own fixed delay
@@ -1415,6 +1631,7 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1415
1631
  // per-block (WAYGRAPH_FAST_BLOCKS), and the Locator.fill/click patches
1416
1632
  // below are installed once on the shared prototype but invoked once per
1417
1633
  // real interaction, long after this closure was created.
1634
+ // stubBeforeRef.current = resolved stubBefore slots for the active block.
1418
1635
  const typeDelay = () => (pacing.fast ? 0 : slowMo ? 0 : 30);
1419
1636
  // Click is a single action, not per-character, so it doesn't compound
1420
1637
  // the same way - but slowMo still adds its own pause around the actual
@@ -1451,9 +1668,11 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1451
1668
  // Only trust the "last mem.get()" as THIS fill's source if it
1452
1669
  // happened recently - a stale read from several actions ago is
1453
1670
  // more likely unrelated than actually describing this field.
1454
- const label = memTrack.lastKeyName && Date.now() - memTrack.at < 3000
1671
+ const fallback = memTrack.lastKeyName && Date.now() - memTrack.at < 3000
1455
1672
  ? "from mem: " + memTrack.lastKeyName
1456
1673
  : "writing from mem";
1674
+ const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
1675
+ const label = await captionForLocator(page, this, stubs, fallback);
1457
1676
  await moveCursorTo(page, box, cursorMs(500));
1458
1677
  await showRing(page, box, label);
1459
1678
  await new Promise((res) => setTimeout(res, pacing.fast ? 0 : 200));
@@ -1506,10 +1725,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1506
1725
  const box = await this.boundingBox();
1507
1726
  const narrated = box ? await wasJustNarrated(page) : false;
1508
1727
  if (box && !narrated) {
1509
- let label = "click";
1728
+ let fallback = "click";
1510
1729
  try {
1511
1730
  const text = (await this.textContent())?.trim();
1512
- if (text && text.length > 0 && text.length <= 30) label = text;
1731
+ if (text && text.length > 0 && text.length <= 30) fallback = text;
1513
1732
  } catch {
1514
1733
  // element has no simple text (an icon button, say) - generic label is fine
1515
1734
  }
@@ -1517,10 +1736,12 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing) {
1517
1736
  // (set on page by runStepMode just before act).
1518
1737
  try {
1519
1738
  const navLabel = await page.evaluate(() => window.__wgPendingNavClickLabel || null);
1520
- if (navLabel) label = String(navLabel);
1739
+ if (navLabel) fallback = String(navLabel);
1521
1740
  } catch {
1522
1741
  /* ignore */
1523
1742
  }
1743
+ const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
1744
+ const label = await captionForLocator(page, this, stubs, fallback);
1524
1745
  clickPoint = await moveCursorTo(page, box, cursorMs(600));
1525
1746
  await showRing(page, box, label);
1526
1747
  // "pop for a few seconds" - Dan's own phrase, matching the
@@ -1644,8 +1865,13 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1644
1865
  // own added dwell while the rest of the chain keeps the full theatrical
1645
1866
  // pace. Playwright's own slowMo is process-wide and untouched by this -
1646
1867
  // only OUR added pauses (ring pop, cursor travel, typing delay) shrink.
1647
- const pacing = { fast: false };
1648
- instrumentInteractionHighlighting(page, mem, slowMo, pacing);
1868
+ // --fast / WAYGRAPH_DEMO_FAST: every step uses fast interaction pacing + shorter gates.
1869
+ // WAYGRAPH_FAST_BLOCKS still names per-block overrides when --fast is off.
1870
+ const demoFast = process.env.WAYGRAPH_DEMO_FAST === "1";
1871
+ const stepperMode = process.env.WAYGRAPH_STEPPER === "full" ? "full" : "carousel";
1872
+ const pacing = { fast: demoFast };
1873
+ const stubBeforeRef = { current: [] };
1874
+ instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBeforeRef);
1649
1875
  // Force the panel checkbox from this process's flags/env at run start.
1650
1876
  // installOverlay only seeds localStorage when the key is null (so mid-run
1651
1877
  // checkbox clicks survive navigations). Without this, a prior --autoplay
@@ -1689,7 +1915,11 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1689
1915
  // which way the checkbox is set. That's the hybrid Dan asked for: some
1690
1916
  // steps auto-advance, some get a manual click, toggled as the demo goes,
1691
1917
  // not fixed for the whole run from a single env var.
1692
- const autoplayMs = process.env.WAYGRAPH_AUTOPLAY_MS ? Number(process.env.WAYGRAPH_AUTOPLAY_MS) : 1800;
1918
+ const autoplayMs = process.env.WAYGRAPH_AUTOPLAY_MS
1919
+ ? Number(process.env.WAYGRAPH_AUTOPLAY_MS)
1920
+ : demoFast
1921
+ ? 450
1922
+ : 1800;
1693
1923
  const currentAutoplay = () =>
1694
1924
  page
1695
1925
  .evaluate(() => {
@@ -1789,7 +2019,14 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1789
2019
  if ((clearSession || r.resetSession) && i > 0) {
1790
2020
  await resetPageState(context, page, baseURL);
1791
2021
  }
1792
- pacing.fast = fastBlockNames.has(r.block.name);
2022
+ pacing.fast = demoFast || fastBlockNames.has(r.block.name);
2023
+ const fixtures = r.highlightFixtures;
2024
+ stubBeforeRef.current = resolveHighlightSlots(r.block, "stubBefore", { fixtures });
2025
+ const isNavBlock = r.block.__waygraphKind === "nav";
2026
+ const autoNow = await currentAutoplay();
2027
+ // Auto-next: tuck stepper on NavBlocks so the page transition fills the frame.
2028
+ // Video recording still collapses every step.
2029
+ const forceCollapsed = !!process.env.WAYGRAPH_VIDEO || (autoNow && isNavBlock);
1793
2030
  const requires = r.block.requires ?? [];
1794
2031
  const keys = requires.map((k) => {
1795
2032
  let value = "<not yet set>";
@@ -1814,6 +2051,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1814
2051
  title,
1815
2052
  episodeNumber: r.episodeNumber,
1816
2053
  episodeTitle: r.episodeTitle,
2054
+ stepperMode,
2055
+ forceCollapsed,
1817
2056
  });
1818
2057
  const edits = await gate();
1819
2058
  for (const k of requires) {
@@ -1826,7 +2065,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1826
2065
  }
1827
2066
  }
1828
2067
  const recordingVideo = !!process.env.WAYGRAPH_VIDEO;
1829
- const autoCollapsePanel = recordingVideo || (await currentAutoplay());
2068
+ const autoCollapsePanel = recordingVideo || (await currentAutoplay() && isNavBlock) || forceCollapsed;
1830
2069
  await markStepRunning(page, { autoCollapsePanel });
1831
2070
  // NavBlock click-nav: label the upcoming Locator.click demo cursor as
1832
2071
  // "nav: <block>" so pia/demo watchers see cursor+pulse on click nav
@@ -1876,6 +2115,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1876
2115
  episodeNumber: r.episodeNumber,
1877
2116
  episodeTitle: r.episodeTitle,
1878
2117
  expectedFailureReason: r.expectedFailureReason,
2118
+ stepperMode,
2119
+ forceCollapsed: false,
1879
2120
  });
1880
2121
  const errEdits = await gate();
1881
2122
  if (errEdits && errEdits.__wgRetry) {
@@ -1904,7 +2145,26 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1904
2145
  })
1905
2146
  .catch(() => {});
1906
2147
  result = stepOutcome.result;
1907
- const highlights = extractVerifyHighlights(r.block, result.__state);
2148
+ const fixturesAfter = r.highlightFixtures;
2149
+ const slides = resolveSlides(r.block, { out: result, fixtures: fixturesAfter });
2150
+ if (slides.length > 0) {
2151
+ await presentSlides(page, slides, gate, {
2152
+ title,
2153
+ blockName: r.block.name,
2154
+ fast: pacing.fast,
2155
+ });
2156
+ }
2157
+ let highlights;
2158
+ if (hasAuthoredStubAfter(r.block, result, fixturesAfter)) {
2159
+ highlights = resolveHighlightSlots(r.block, "stubAfter", { out: result, fixtures: fixturesAfter }).map(
2160
+ (h) => ({
2161
+ selector: h.selector,
2162
+ label: formatHighlightCaption(h),
2163
+ }),
2164
+ );
2165
+ } else {
2166
+ highlights = extractVerifyHighlights(r.block, result.__state);
2167
+ }
1908
2168
  await renderAfterStep(page, {
1909
2169
  index: i,
1910
2170
  total: resolved.length,
@@ -1920,6 +2180,9 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
1920
2180
  title,
1921
2181
  episodeNumber: r.episodeNumber,
1922
2182
  episodeTitle: r.episodeTitle,
2183
+ stepperMode,
2184
+ // After a nav under auto-next, stay compact until the next action block expands.
2185
+ forceCollapsed: !!process.env.WAYGRAPH_VIDEO || ((await currentAutoplay()) && isNavBlock),
1923
2186
  });
1924
2187
  await gate();
1925
2188
  }
@@ -2052,6 +2315,7 @@ async function main() {
2052
2315
  block: bi.block,
2053
2316
  exportName: bi.name,
2054
2317
  seedMem: idx === 0 ? seedMem : undefined,
2318
+ highlightFixtures: flow.highlightFixtures,
2055
2319
  expectedFailureReason:
2056
2320
  flow.expectedFailureReason && idx === blockInfos.length - 1
2057
2321
  ? flow.expectedFailureReason
@@ -2113,6 +2377,7 @@ async function main() {
2113
2377
  episodeNumber: episodeCounter,
2114
2378
  episodeTitle: flow.title || seg.ref,
2115
2379
  expectedFailureReason: flow.expectedFailureReason,
2380
+ highlightFixtures: flow.highlightFixtures,
2116
2381
  seedMem: () => seedMemForFlow(mem, flow.blocks(), seg.json, seg.ref),
2117
2382
  });
2118
2383
  } else {
@@ -2121,6 +2386,7 @@ async function main() {
2121
2386
  flowMeta.push({
2122
2387
  episodeNumber: undefined,
2123
2388
  episodeTitle: undefined,
2389
+ highlightFixtures: undefined,
2124
2390
  seedMem: () => seedMemForBlock(mem, r, seg.json),
2125
2391
  });
2126
2392
  }
@@ -2142,6 +2408,7 @@ async function main() {
2142
2408
  resetSession: bi.resetSessionBefore === true,
2143
2409
  episodeNumber: meta.episodeNumber,
2144
2410
  episodeTitle: meta.episodeTitle,
2411
+ highlightFixtures: meta.highlightFixtures,
2145
2412
  expectedFailureReason:
2146
2413
  meta.expectedFailureReason && remainingInFlow === 1 ? meta.expectedFailureReason : undefined,
2147
2414
  seedMem: isFirstOfSegment ? meta.seedMem : undefined,
@@ -2840,6 +3107,12 @@ function parseRunFlags(argv) {
2840
3107
  else if (a === "--auto-play-video") {
2841
3108
  out.autoPlayVideo = true;
2842
3109
  }
3110
+ else if (a === "--fast") {
3111
+ out.fast = true;
3112
+ }
3113
+ else if (a === "--full") {
3114
+ out.fullStepper = true;
3115
+ }
2843
3116
  else if (a === "--non-headless") {
2844
3117
  out.nonHeadless = true;
2845
3118
  }
@@ -2945,6 +3218,10 @@ function applyRunFlags(flags, opts) {
2945
3218
  if (flags.step === undefined)
2946
3219
  flags.step = true;
2947
3220
  }
3221
+ if ((flags.fast || flags.fullStepper) && !opts?.allowDemoUi) {
3222
+ console.error("waygraph: --fast / --full are demo-only flags");
3223
+ process.exit(1);
3224
+ }
2948
3225
  if (flags.step === true) {
2949
3226
  process.env.WAYGRAPH_STEP = "1";
2950
3227
  process.env.WAYGRAPH_HEADED = "1";
@@ -2980,6 +3257,12 @@ function applyRunFlags(flags, opts) {
2980
3257
  if (flags.data !== undefined) {
2981
3258
  process.env.WAYGRAPH_DATA = flags.data;
2982
3259
  }
3260
+ if (flags.fast) {
3261
+ process.env.WAYGRAPH_DEMO_FAST = "1";
3262
+ }
3263
+ if (flags.fullStepper) {
3264
+ process.env.WAYGRAPH_STEPPER = "full";
3265
+ }
2983
3266
  }
2984
3267
  /**
2985
3268
  * BASE_URL when neither --base-url nor WAYGRAPH_BASE_URL is set:
@@ -3135,6 +3418,8 @@ Primary (less is more):
3135
3418
  waygraph demo [--blocks <flow|file|spec>] Watch with step overlay (QA path)
3136
3419
  --data '{...}' Mem seed JSON (or inline flow({...}))
3137
3420
  --auto-next Auto-advance steps (alias: --autoplay)
3421
+ --fast Faster transitions + shorter auto-next gates
3422
+ --full Classic wrap-all block chips (default: carousel)
3138
3423
  --auto-play-video QA: --auto-next + --video (+ step)
3139
3424
  --video-viewport WxH Recording size (default demo: 1920x1080)
3140
3425
  --title / --base-url
@@ -3155,7 +3440,8 @@ Examples:
3155
3440
  waygraph list # file → export map
3156
3441
  waygraph run src/flows/shop.flow.ts --data '{...}'
3157
3442
  waygraph run --blocks shopFlow --non-headless --video
3158
- waygraph demo --blocks src/flows/cart-bulk.flow.ts --auto-next
3443
+ waygraph demo --blocks src/flows/cart-bulk.flow.ts --auto-next --fast
3444
+ waygraph demo src/flows/shop.flow.ts --full
3159
3445
  waygraph try auto
3160
3446
  waygraph try auto:cli
3161
3447
  waygraph auto src/flows/shop.flow.ts --data '{...}' # run by file (same as run)
@@ -3168,6 +3454,7 @@ Aliases (compat): \`chain <spec>\` -> run --blocks; \`chain auto A B\` -> auto -
3168
3454
 
3169
3455
  Project path optional (defaults to cwd). Flags beat WAYGRAPH_* env.
3170
3456
  \`run\`/\`demo\`/\`auto\`: Flow export, .flow.ts path, or "a then b" chain (auto also explores).
3457
+ Hide stepper = compact "N / M · block" pill. Auto-next auto-hides on NavBlocks.
3171
3458
  `);
3172
3459
  process.exit(0);
3173
3460
  }
@@ -3282,13 +3569,13 @@ async function main() {
3282
3569
  }
3283
3570
  case "demo": {
3284
3571
  const flags = parseRunFlags(args.slice(1));
3285
- applyRunFlags(flags, { allowAutoPlayVideo: true });
3572
+ applyRunFlags(flags, { allowAutoPlayVideo: true, allowDemoUi: true });
3286
3573
  const spec = resolveSpec(flags);
3287
3574
  if (!spec) {
3288
3575
  console.error("waygraph demo: missing flow/spec — e.g.\n" +
3289
3576
  " waygraph demo src/flows/shop.flow.ts\n" +
3290
3577
  " waygraph demo --blocks shopFlow\n" +
3291
- " Flags: --blocks --data --auto-next --auto-play-video --title --base-url --video");
3578
+ " Flags: --blocks --data --auto-next --fast --full --auto-play-video --title --base-url --video");
3292
3579
  process.exit(1);
3293
3580
  }
3294
3581
  const proj = flags.blocks