waygraph 0.12.41 → 0.12.42

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.
Files changed (2) hide show
  1. package/dist/cli.js +363 -790
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -274,12 +274,12 @@ async function findBlock(projectDir, ref) {
274
274
  if (byRuntimeName.length === 1) return byRuntimeName[0];
275
275
  if (byRuntimeName.length > 1) {
276
276
  throw new Error(
277
- "waygraph chain: \\\"" + ref + "\\\" matches " + byRuntimeName.length + " Blocks by name - " +
277
+ "waygraph chain: \\"" + ref + "\\" matches " + byRuntimeName.length + " Blocks by name - " +
278
278
  byRuntimeName.map((b) => b.exportName + " (" + b.file + ")").join(", ") +
279
279
  ". Use the export name to disambiguate.",
280
280
  );
281
281
  }
282
- throw new Error("waygraph chain: no Block named \\\"" + ref + "\\\" found under " + projectDir);
282
+ throw new Error("waygraph chain: no Block named \\"" + ref + "\\" found under " + projectDir);
283
283
  }
284
284
 
285
285
  /**
@@ -316,7 +316,7 @@ function parseChainSpec(spec) {
316
316
  const m = /^([A-Za-z_$][\\w-]*)\\s*(?:\\(([\\s\\S]*)\\))?$/.exec(s);
317
317
  if (!m) {
318
318
  throw new Error(
319
- "waygraph chain: could not parse segment \\\"" + s + "\\\" - expected \\\"blockName\\\" or \\\"blockName({...json...})\\\"",
319
+ "waygraph chain: could not parse segment \\"" + s + "\\" - expected \\"blockName\\" or \\"blockName({...json...})\\"",
320
320
  );
321
321
  }
322
322
  const ref = m[1];
@@ -329,7 +329,7 @@ function seedMemFromRequires(mem, requires, json, label) {
329
329
  if (requires.length === 0) {
330
330
  if (json !== undefined) {
331
331
  throw new Error(
332
- "waygraph chain: \\\"" + label + "\\\" takes no input (empty requires) but got a payload: " + json,
332
+ "waygraph chain: \\"" + label + "\\" takes no input (empty requires) but got a payload: " + json,
333
333
  );
334
334
  }
335
335
  return;
@@ -341,7 +341,7 @@ function seedMemFromRequires(mem, requires, json, label) {
341
341
  json = fromEnv;
342
342
  } else {
343
343
  throw new Error(
344
- "waygraph chain: \\\"" + label + "\\\" requires " + requires.map((k) => k.name).join(", ") +
344
+ "waygraph chain: \\"" + label + "\\" requires " + requires.map((k) => k.name).join(", ") +
345
345
  " - give a JSON payload (inline blockName({...}) or --data '{...}')",
346
346
  );
347
347
  }
@@ -350,7 +350,7 @@ function seedMemFromRequires(mem, requires, json, label) {
350
350
  try {
351
351
  parsed = JSON.parse(json);
352
352
  } catch (err) {
353
- throw new Error("waygraph chain: \\\"" + label + "\\\" payload is not valid JSON - " + String(err));
353
+ throw new Error("waygraph chain: \\"" + label + "\\" payload is not valid JSON - " + String(err));
354
354
  }
355
355
  // Keyed-by-name when every require name is a top-level key (preferred for
356
356
  // --data and for {"saucedemo.credentials":{...}} even with one require).
@@ -367,13 +367,13 @@ function seedMemFromRequires(mem, requires, json, label) {
367
367
  }
368
368
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
369
369
  throw new Error(
370
- "waygraph chain: \\\"" + label + "\\\" requires " + requires.length + " keys (" +
370
+ "waygraph chain: \\"" + label + "\\" requires " + requires.length + " keys (" +
371
371
  requires.map((k) => k.name).join(", ") + ") - payload must be an object keyed by each key's name",
372
372
  );
373
373
  }
374
374
  for (const k of requires) {
375
375
  if (!(k.name in parsed)) {
376
- throw new Error("waygraph chain: \\\"" + label + "\\\" payload is missing required key \\\"" + k.name + "\\\"");
376
+ throw new Error("waygraph chain: \\"" + label + "\\" payload is missing required key \\"" + k.name + "\\"");
377
377
  }
378
378
  mem.set(k, parsed[k.name]);
379
379
  }
@@ -412,12 +412,10 @@ function seedMemForFlow(mem, flowBlocks, json, label) {
412
412
  const RING_CSS =
413
413
  "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
414
414
  "border:2.5px solid #7C3AED;border-radius:10px;" +
415
- "box-shadow:0 0 0 4px rgba(124,58,237,.16);" +
416
- "transition:opacity .32s ease,border-color .15s;}" +
417
- "#wg-focus-veil{position:fixed;z-index:2147483645;pointer-events:none;opacity:0;" +
418
- "border-radius:10px;box-shadow:0 0 0 9999px rgba(12,6,28,.62);" +
419
- "transition:opacity .28s ease;}" +
420
- "#wg-focus-veil.wg-focus-on{opacity:1;}" +
415
+ "box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease,border-color .15s,box-shadow .15s;}" +
416
+ // Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
417
+ // Automation (verify fallback, unmatched fill/click) = gray - operators
418
+ // can ignore engine checks and watch purple + semantic tones.
421
419
  "#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
422
420
  "#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
423
421
  "#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
@@ -433,7 +431,7 @@ const RING_CSS =
433
431
  // window.__wgPositionRing in installOverlay.
434
432
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
435
433
  "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
436
- "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .32s ease,background .15s,color .15s;}" +
434
+ "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease,background .15s,color .15s;}" +
437
435
  "#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
438
436
  "#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
439
437
  "#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
@@ -519,24 +517,6 @@ const RING_CSS =
519
517
  "#wg-todos .wg-todo-done{color:#6ee7b7;text-decoration:line-through;opacity:.85;}" +
520
518
  "#wg-todos .wg-todo-current{color:#fff;}" +
521
519
  "#wg-todos .wg-todo-pending{color:#9a7ad1;}" +
522
- "#wg-todo-modal{position:fixed;z-index:2147483647;left:14px;bottom:14px;min-width:240px;max-width:min(360px,92vw);" +
523
- "padding:14px 16px 12px;background:rgba(18,8,36,.94);color:#f0e8ff;" +
524
- "border:1px solid rgba(124,58,237,.55);border-radius:12px;" +
525
- "box-shadow:0 12px 40px rgba(0,0,0,.45);backdrop-filter:blur(8px);" +
526
- "opacity:0;transform:translateY(8px);pointer-events:none;" +
527
- "transition:opacity .28s ease,transform .28s ease;}" +
528
- "#wg-todo-modal.wg-todo-on{opacity:1;transform:none;pointer-events:auto;}" +
529
- "#wg-todo-modal .wg-todo-head{font:700 11px/1.2 system-ui,sans-serif;color:#c9a6ff;" +
530
- "letter-spacing:.04em;text-transform:uppercase;margin:0 0 10px;}" +
531
- "#wg-todo-modal ul{list-style:none;margin:0;padding:0;}" +
532
- "#wg-todo-modal li{display:flex;gap:10px;align-items:flex-start;margin:0 0 8px;" +
533
- "font:600 14px/1.4 system-ui,sans-serif;}" +
534
- "#wg-todo-modal li:last-child{margin-bottom:0;}" +
535
- "#wg-todo-modal .wg-todo-mark{flex:0 0 auto;width:1.2em;text-align:center;font-size:15px;}" +
536
- "#wg-todo-modal .wg-todo-done{color:#6ee7b7;text-decoration:line-through;opacity:.8;}" +
537
- "#wg-todo-modal .wg-todo-current{color:#fff;}" +
538
- "#wg-todo-modal .wg-todo-current .wg-todo-mark{color:#c9a6ff;}" +
539
- "#wg-todo-modal .wg-todo-pending{color:#9a7ad1;}" +
540
520
  "#wg-progress{height:4px;background:#2a1650;border-radius:2px;margin:0 0 12px;overflow:hidden;}" +
541
521
  "#wg-progress-bar{height:100%;background:#7C3AED;border-radius:2px;transition:width .3s ease;}" +
542
522
  // A real tab bar for episodes - the currently-active episode reads as
@@ -621,18 +601,23 @@ const RING_CSS =
621
601
  "#wg-panel .wg-expected-heading{color:#ffcf7a;}" +
622
602
  "#wg-panel .wg-expected-reason{font:600 12.5px/1.5 system-ui,sans-serif;background:#2a2410;" +
623
603
  "color:#ffe6ae;border-radius:8px;padding:10px;margin:0 0 8px;}" +
624
- // Hide / Show chrome: collapsed = compact "N / M · block" pill (not empty chrome).
604
+ // Hide / Show chrome: collapsed = compact "N / M · block" pill + Next (manual).
625
605
  "#wg-panel .wg-chrome{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 0 8px;}" +
626
606
  "#wg-panel .wg-chrome-title{font:700 11px/1.2 system-ui,sans-serif;color:#c9a6ff;" +
627
- "letter-spacing:.04em;text-transform:uppercase;}" +
628
- "#wg-panel button.wg-hide-btn{margin:0;padding:4px 10px;font:600 11px system-ui,sans-serif;" +
607
+ "letter-spacing:.04em;text-transform:uppercase;flex:1;min-width:0;}" +
608
+ "#wg-panel .wg-chrome-actions{display:flex;align-items:center;gap:6px;flex-shrink:0;}" +
609
+ "#wg-panel button.wg-hide-btn,#wg-panel button.wg-mini-next{margin:0;padding:4px 10px;font:600 11px system-ui,sans-serif;" +
629
610
  "background:#3a2a60;color:#e8dcff;border:1px solid #5b3aa8;border-radius:6px;cursor:pointer;}" +
630
- "#wg-panel button.wg-hide-btn:hover{background:#4b2a80;}" +
611
+ "#wg-panel button.wg-hide-btn:hover,#wg-panel button.wg-mini-next:hover{background:#4b2a80;}" +
612
+ "#wg-panel button.wg-mini-next{background:#2a9d6f;border-color:#22855e;color:#fff;display:none;}" +
613
+ "#wg-panel button.wg-mini-next:hover{background:#22855e;}" +
614
+ "#wg-panel button.wg-mini-next:disabled{opacity:.55;cursor:default;}" +
631
615
  "#wg-panel.wg-collapsed{width:auto;max-width:92vw;padding:8px 12px;max-height:none;overflow:hidden;}" +
632
616
  "#wg-panel.wg-collapsed .wg-body{display:none;}" +
633
617
  "#wg-panel.wg-collapsed .wg-chrome{margin:0;}" +
634
618
  "#wg-panel.wg-collapsed .wg-chrome-title{font:700 13px/1.25 system-ui,sans-serif;color:#fff;" +
635
619
  "letter-spacing:0;text-transform:none;}" +
620
+ "#wg-panel.wg-collapsed button.wg-mini-next.wg-mini-next-show{display:inline-block;}" +
636
621
  "@media (max-width:640px){" +
637
622
  "#wg-panel{left:8px;right:8px;bottom:8px;transform:none;max-width:none;width:auto;" +
638
623
  "max-height:min(55vh,calc(100vh - 16px));padding:12px 14px;border-radius:12px;}" +
@@ -640,6 +625,7 @@ const RING_CSS =
640
625
  "#wg-episodes{overflow-x:auto;-webkit-overflow-scrolling:touch;flex-wrap:nowrap;}" +
641
626
  "#wg-panel .wg-narration{font-size:13px;}" +
642
627
  "#wg-panel button{width:100%;}" +
628
+ "#wg-panel.wg-collapsed button.wg-mini-next,#wg-panel.wg-collapsed button.wg-hide-btn{width:auto;}" +
643
629
  "#wg-panel .wg-error-actions{flex-direction:column;}" +
644
630
  "#wg-banner{max-width:min(92vw,320px);font-size:13px;}" +
645
631
  "}";
@@ -675,11 +661,6 @@ async function installOverlay(page, title) {
675
661
  } catch {
676
662
  /* private mode / blocked storage - falls back to manual gating */
677
663
  }
678
- if (!document.getElementById("wg-focus-veil")) {
679
- const veil = document.createElement("div");
680
- veil.id = "wg-focus-veil";
681
- document.documentElement.appendChild(veil);
682
- }
683
664
  if (!document.getElementById("wg-ring")) {
684
665
  const ring = document.createElement("div");
685
666
  ring.id = "wg-ring";
@@ -710,10 +691,7 @@ async function installOverlay(page, title) {
710
691
  window.__wgMoveCursorTo = (x, y, ms, instant) => {
711
692
  const cursor = document.getElementById("wg-cursor");
712
693
  if (!cursor) return;
713
- const travel = ms || 600;
714
- window.__wgCursorPos = { x: Number(x) || 0, y: Number(y) || 0 };
715
- window.__wgCursorTravelUntil = Date.now() + (instant ? 0 : travel) + 40;
716
- cursor.style.setProperty("--wg-cursor-ms", travel + "ms");
694
+ cursor.style.setProperty("--wg-cursor-ms", (ms || 600) + "ms");
717
695
  if (instant) {
718
696
  const prev = cursor.style.transition;
719
697
  cursor.style.transition = "none";
@@ -732,7 +710,6 @@ async function installOverlay(page, title) {
732
710
  window.__wgPositionRing = (box, label, tone, style) => {
733
711
  const ring = document.getElementById("wg-ring");
734
712
  const ringLabel = document.getElementById("wg-ring-label");
735
- const veil = document.getElementById("wg-focus-veil");
736
713
  if (!ring || !ringLabel) return;
737
714
  const raw = (tone || "planned") + "";
738
715
  const t =
@@ -750,15 +727,14 @@ async function installOverlay(page, title) {
750
727
  ring.dataset.size = size;
751
728
  ringLabel.dataset.size = size;
752
729
  ringLabel.dataset.weight = weight;
753
- ring.dataset.focus = st.focus ? "1" : "0";
754
730
  const pad = size === "sm" ? 3 : size === "lg" ? 10 : 6;
755
731
  const margin = 6;
756
732
  const vw = window.innerWidth;
757
733
  const vh = window.innerHeight;
758
734
  let left = box.x - pad;
759
735
  let top = box.y - pad;
760
- const width = Math.max(0, box.width + pad * 2);
761
- const height = Math.max(0, box.height + pad * 2);
736
+ const width = box.width + pad * 2;
737
+ const height = box.height + pad * 2;
762
738
  if (left < margin) left = margin;
763
739
  if (top < margin) top = margin;
764
740
  if (left + width > vw - margin) left = Math.max(margin, vw - margin - width);
@@ -768,92 +744,21 @@ async function installOverlay(page, title) {
768
744
  ring.style.width = width + "px";
769
745
  ring.style.height = height + "px";
770
746
  ring.style.opacity = "1";
771
- if (veil) {
772
- veil.style.left = left + "px";
773
- veil.style.top = top + "px";
774
- veil.style.width = width + "px";
775
- veil.style.height = height + "px";
776
- const br = size === "sm" ? "8px" : size === "lg" ? "12px" : "10px";
777
- veil.style.borderRadius = br;
778
- if (st.focus) veil.classList.add("wg-focus-on");
779
- else veil.classList.remove("wg-focus-on");
780
- }
781
747
  ringLabel.textContent = label;
782
748
  ringLabel.style.opacity = "1";
783
- const capColor = st.color || st.labelColor || st.textColor;
784
- if (capColor) ringLabel.style.color = String(capColor);
785
- else ringLabel.style.removeProperty("color");
749
+ // Measure the label's own natural width/height (its real
750
+ // rendered size, unchanged) before deciding where it fits.
786
751
  const lw = ringLabel.offsetWidth;
787
752
  const lh = ringLabel.offsetHeight;
788
753
  let labelLeft = left;
789
754
  let labelTop = top + height + 8;
790
- if (labelTop + lh > vh - margin) labelTop = top - lh - 8;
755
+ if (labelTop + lh > vh - margin) labelTop = top - lh - 8; // flip above if it'd overflow the bottom
791
756
  if (labelTop < margin) labelTop = margin;
792
757
  if (labelLeft + lw > vw - margin) labelLeft = Math.max(margin, vw - margin - lw);
793
758
  if (labelLeft < margin) labelLeft = margin;
794
759
  ringLabel.style.left = labelLeft + "px";
795
760
  ringLabel.style.top = labelTop + "px";
796
761
  };
797
- window.__wgHideFocusVeil = () => {
798
- const veil = document.getElementById("wg-focus-veil");
799
- if (veil) veil.classList.remove("wg-focus-on");
800
- };
801
- window.__wgStartHighlightTrack = (opts) => {
802
- if (window.__wgStopHighlightTrack) window.__wgStopHighlightTrack();
803
- const sel = opts.sel;
804
- const label = opts.label;
805
- const tone = opts.tone || "planned";
806
- const size = opts.size || "md";
807
- const weight = opts.weight || "normal";
808
- const focus = !!opts.focus;
809
- const color = opts.color || "";
810
- const zoom = opts.zoom != null && opts.zoom > 1 ? opts.zoom : 1;
811
- const camMs = opts.camMs != null ? opts.camMs : 700;
812
- const style = { size, weight, focus, color };
813
- // Auto-center only. Never chase the host mouse (that fights the
814
- // camera). Playwright's real mouse is driven separately by the CLI.
815
- window.__wgIgnoreHostMouse = true;
816
- const tick = () => {
817
- const el = document.querySelector(sel);
818
- if (el && window.__wgPositionRing) {
819
- const r = el.getBoundingClientRect();
820
- window.__wgPositionRing(
821
- { x: r.left, y: r.top, width: r.width, height: r.height },
822
- label,
823
- tone,
824
- style,
825
- );
826
- }
827
- window.__wgRingRaf = requestAnimationFrame(tick);
828
- };
829
- if (zoom > 1 && window.__wgSetCamera) {
830
- const el0 = document.querySelector(sel);
831
- if (el0) {
832
- const already =
833
- window.__wgCamera && window.__wgCamera.scale > 1.001;
834
- const dur = already ? Math.min(camMs, 420) : camMs;
835
- const pt = window.__wgElDocPoint
836
- ? window.__wgElDocPoint(el0)
837
- : {
838
- x: el0.getBoundingClientRect().left + el0.getBoundingClientRect().width / 2,
839
- y: el0.getBoundingClientRect().top + el0.getBoundingClientRect().height / 2,
840
- };
841
- // Lock for the whole ring - host mouse cannot steer.
842
- window.__wgCameraLock = true;
843
- window.__wgSetCamera(pt.x, pt.y, zoom, dur);
844
- }
845
- }
846
- window.__wgRingRaf = requestAnimationFrame(tick);
847
- // Do NOT listen to host mousemove - it was steering the camera.
848
- window.__wgRingTrack = null;
849
- window.__wgStopHighlightTrack = () => {
850
- if (window.__wgRingRaf) {
851
- cancelAnimationFrame(window.__wgRingRaf);
852
- window.__wgRingRaf = null;
853
- }
854
- window.__wgStopHighlightTrack = null;
855
- };
856
- };
857
762
  window.__wgClickPulse = (x, y, tone) => {
858
763
  const pulse = document.getElementById("wg-click-pulse");
859
764
  if (!pulse) return;
@@ -878,124 +783,31 @@ async function installOverlay(page, title) {
878
783
  const ringLabel = document.getElementById("wg-ring-label");
879
784
  if (ring) ring.style.opacity = "0";
880
785
  if (ringLabel) ringLabel.style.opacity = "0";
881
- if (window.__wgHideFocusVeil) window.__wgHideFocusVeil();
882
- // Do NOT clear camera zoom here - zoomOut:false keeps scale between
883
- // rings; only resetCameraZoom / teardown call __wgClearZoom.
884
- };
885
- // Screen Studio-style camera: scale <body>, keep overlays on <html> fixed.
886
- // Focus (cx,cy) is DOCUMENT space (pre-transform). Viewport mouse/rects
887
- // must go through __wgViewportToDoc before SetCamera.
888
- window.__wgCamera = {
889
- scale: 1,
890
- cx: window.innerWidth / 2,
891
- cy: window.innerHeight / 2,
892
- };
893
- window.__wgCameraLock = false;
894
- window.__wgViewportToDoc = (vx, vy) => {
895
- const cam = window.__wgCamera || {
896
- scale: 1,
897
- cx: window.innerWidth / 2,
898
- cy: window.innerHeight / 2,
899
- };
900
- const s = cam.scale > 1.001 ? cam.scale : 1;
901
- if (s <= 1.001) return { x: vx, y: vy };
902
- return {
903
- x: (vx - window.innerWidth / 2) / s + cam.cx,
904
- y: (vy - window.innerHeight / 2) / s + cam.cy,
905
- };
906
- };
907
- window.__wgElDocPoint = (el) => {
908
- const r = el.getBoundingClientRect();
909
- return window.__wgViewportToDoc(
910
- r.left + r.width / 2,
911
- r.top + r.height / 2,
912
- );
913
- };
914
- window.__wgSetCamera = (fx, fy, scale, ms) => {
915
- const body = document.body;
916
- if (!body) return;
917
- const s = Math.max(1, Math.min(Number(scale) || 1, 2.75));
918
- const vw = window.innerWidth;
919
- const vh = window.innerHeight;
920
- window.__wgCamera = { scale: s, cx: fx, cy: fy };
921
- const dur = ms != null && Number(ms) >= 0 ? Number(ms) : 650;
922
- body.style.transition =
923
- dur <= 0
924
- ? "none"
925
- : "transform " + dur / 1000 + "s cubic-bezier(.22,1,.36,1)";
926
- body.style.transformOrigin = "0 0";
927
- if (s <= 1.001) {
928
- body.style.transform = "";
929
- return;
930
- }
931
- // One transform: scale AND bring focus to viewport center.
932
- body.style.transform =
933
- "translate(" + vw / 2 + "px," + vh / 2 + "px) scale(" + s + ") translate(" + -fx + "px," + -fy + "px)";
934
- };
935
- window.__wgSetTodos = (todos) => {
936
- let modal = document.getElementById("wg-todo-modal");
937
- if (!todos || !todos.length) {
938
- if (modal) {
939
- modal.classList.remove("wg-todo-on");
940
- modal.innerHTML = "";
941
- }
942
- return;
943
- }
944
- if (!modal) {
945
- modal = document.createElement("div");
946
- modal.id = "wg-todo-modal";
947
- document.documentElement.appendChild(modal);
948
- }
949
- const esc = (s) =>
950
- String(s)
951
- .replace(/&/g, "&amp;")
952
- .replace(/</g, "&lt;")
953
- .replace(/>/g, "&gt;");
954
- const rows = todos
955
- .map((t) => {
956
- const cls = t.current
957
- ? "wg-todo-current"
958
- : t.done
959
- ? "wg-todo-done"
960
- : "wg-todo-pending";
961
- const mark = t.done
962
- ? String.fromCharCode(0x2713)
963
- : t.current
964
- ? String.fromCharCode(0x2192)
965
- : String.fromCharCode(0x25cb);
966
- return (
967
- '<li class="' +
968
- cls +
969
- '"><span class="wg-todo-mark">' +
970
- mark +
971
- '</span><span>' +
972
- esc(t.text || "") +
973
- '</span></li>'
974
- );
975
- })
976
- .join("");
977
- modal.innerHTML =
978
- '<div class="wg-todo-head">Checklist</div><ul>' + rows + '</ul>';
979
- modal.classList.add("wg-todo-on");
980
- };
981
- window.__wgResetCamera = () => {
982
- window.__wgSetCamera(window.innerWidth / 2, window.innerHeight / 2, 1);
786
+ if (window.__wgClearZoom) window.__wgClearZoom();
983
787
  };
984
788
  window.__wgClearZoom = () => {
985
- if (window.__wgResetCamera) window.__wgResetCamera();
789
+ document.querySelectorAll("[data-wg-zoomed=\\"1\\"]").forEach((el) => {
790
+ el.style.removeProperty("transform");
791
+ el.style.removeProperty("transform-origin");
792
+ el.style.removeProperty("transition");
793
+ el.style.removeProperty("z-index");
794
+ el.style.removeProperty("position");
795
+ delete el.dataset.wgZoomed;
796
+ });
986
797
  };
987
- window.__wgSetBannerTitle = (title) => {
988
- const banner = document.getElementById("wg-banner");
989
- if (!banner) return;
990
- let textEl = null;
991
- for (const sp of banner.querySelectorAll("span")) {
992
- if (!sp.classList.contains("wg-banner-tag")) { textEl = sp; break; }
993
- }
994
- if (!textEl) {
995
- textEl = document.createElement("span");
996
- banner.appendChild(textEl);
997
- }
998
- textEl.textContent = String(title || "");
798
+ window.__wgApplyZoom = (sel, scale) => {
799
+ if (window.__wgClearZoom) window.__wgClearZoom();
800
+ const n = Number(scale);
801
+ if (!Number.isFinite(n) || n <= 1.001) return;
802
+ const el = document.querySelector(sel);
803
+ if (!el) return;
804
+ const st = getComputedStyle(el);
805
+ if (st.position === "static") el.style.position = "relative";
806
+ el.style.zIndex = "2147483645";
807
+ el.style.transition = "transform .35s ease";
808
+ el.style.transformOrigin = "center center";
809
+ el.style.transform = "scale(" + n + ")";
810
+ el.dataset.wgZoomed = "1";
999
811
  };
1000
812
  const POSITIONS = ["left", "center", "right"];
1001
813
  const applyPos = (el, pos) => {
@@ -1047,33 +859,82 @@ async function installOverlay(page, title) {
1047
859
  }
1048
860
  if (iconLink.href !== favicon) iconLink.href = favicon;
1049
861
  // Hide/Show for #wg-panel - call after every panel.innerHTML refresh.
1050
- // Collapsed title shows "N / M · block" (compact progress).
1051
- // forceCollapsed:true = force mini (--mini / --video). Omit otherwise so
1052
- // the user's Hide preference (localStorage) wins. Never pass false.
862
+ // Collapsed = "N / M · block" + Next (when not auto). Never pass
863
+ // forceCollapsed:false - omit so Hide/localStorage wins.
1053
864
  window.__wgWirePanelChrome = (panel, storageKey, chromeTitle, opts) => {
1054
865
  if (!panel) return;
1055
866
  opts = opts || {};
1056
- if (!panel.querySelector(":scope > .wg-chrome")) {
1057
- const body = document.createElement("div");
1058
- body.className = "wg-body";
1059
- while (panel.firstChild) body.appendChild(panel.firstChild);
1060
- const chrome = document.createElement("div");
1061
- chrome.className = "wg-chrome";
1062
- const titleEl = document.createElement("span");
1063
- titleEl.className = "wg-chrome-title";
1064
- titleEl.textContent = chromeTitle || "waygraph demo";
1065
- const btn = document.createElement("button");
1066
- btn.type = "button";
1067
- btn.className = "wg-hide-btn";
1068
- btn.setAttribute("data-wg-toggle", "1");
1069
- btn.textContent = "Hide";
1070
- chrome.appendChild(titleEl);
1071
- chrome.appendChild(btn);
1072
- panel.appendChild(chrome);
1073
- panel.appendChild(body);
1074
- }
867
+ const ensureChrome = () => {
868
+ let chrome = panel.querySelector(":scope > .wg-chrome");
869
+ if (!chrome) {
870
+ const body = document.createElement("div");
871
+ body.className = "wg-body";
872
+ while (panel.firstChild) body.appendChild(panel.firstChild);
873
+ chrome = document.createElement("div");
874
+ chrome.className = "wg-chrome";
875
+ const titleEl = document.createElement("span");
876
+ titleEl.className = "wg-chrome-title";
877
+ titleEl.textContent = chromeTitle || "waygraph demo";
878
+ const actions = document.createElement("div");
879
+ actions.className = "wg-chrome-actions";
880
+ const nextBtn = document.createElement("button");
881
+ nextBtn.type = "button";
882
+ nextBtn.className = "wg-mini-next";
883
+ nextBtn.setAttribute("data-wg-mini-next", "1");
884
+ nextBtn.textContent = "Next \u25B6";
885
+ const btn = document.createElement("button");
886
+ btn.type = "button";
887
+ btn.className = "wg-hide-btn";
888
+ btn.setAttribute("data-wg-toggle", "1");
889
+ btn.textContent = "Hide";
890
+ actions.appendChild(nextBtn);
891
+ actions.appendChild(btn);
892
+ chrome.appendChild(titleEl);
893
+ chrome.appendChild(actions);
894
+ panel.appendChild(chrome);
895
+ panel.appendChild(body);
896
+ return;
897
+ }
898
+ // Yap slides ship a bare chrome - ensure actions + mini Next exist.
899
+ let actions = chrome.querySelector(".wg-chrome-actions");
900
+ if (!actions) {
901
+ actions = document.createElement("div");
902
+ actions.className = "wg-chrome-actions";
903
+ const hide = chrome.querySelector("[data-wg-toggle]");
904
+ if (hide) actions.appendChild(hide);
905
+ chrome.appendChild(actions);
906
+ }
907
+ if (!actions.querySelector("[data-wg-mini-next]")) {
908
+ const nextBtn = document.createElement("button");
909
+ nextBtn.type = "button";
910
+ nextBtn.className = "wg-mini-next";
911
+ nextBtn.setAttribute("data-wg-mini-next", "1");
912
+ nextBtn.textContent = "Next \u25B6";
913
+ actions.insertBefore(nextBtn, actions.firstChild);
914
+ }
915
+ };
916
+ ensureChrome();
1075
917
  const stepLabel = opts.stepLabel || panel.dataset.wgStepLabel || "";
1076
918
  if (stepLabel) panel.dataset.wgStepLabel = stepLabel;
919
+ const syncMiniNext = (hidden) => {
920
+ const miniNext = panel.querySelector("[data-wg-mini-next]");
921
+ if (!miniNext) return;
922
+ let auto = false;
923
+ try {
924
+ auto = localStorage.getItem("wg-autoplay") === "1";
925
+ } catch {
926
+ /* ignore */
927
+ }
928
+ const runBtn = document.getElementById("wg-run");
929
+ const show = !!hidden && !auto && !!runBtn;
930
+ miniNext.classList.toggle("wg-mini-next-show", show);
931
+ if (runBtn) {
932
+ miniNext.textContent = runBtn.textContent || "Next \u25B6";
933
+ miniNext.disabled = !!runBtn.disabled;
934
+ } else {
935
+ miniNext.disabled = true;
936
+ }
937
+ };
1077
938
  const apply = (hidden, persist) => {
1078
939
  panel.classList.toggle("wg-collapsed", hidden);
1079
940
  const t = panel.querySelector("[data-wg-toggle]");
@@ -1085,6 +946,7 @@ async function installOverlay(page, title) {
1085
946
  : (chromeTitle || "waygraph demo");
1086
947
  }
1087
948
  if (t) t.textContent = hidden ? "Show" : "Hide";
949
+ syncMiniNext(hidden);
1088
950
  if (persist !== false) {
1089
951
  try {
1090
952
  localStorage.setItem(storageKey, hidden ? "1" : "0");
@@ -1113,6 +975,26 @@ async function installOverlay(page, title) {
1113
975
  apply(!panel.classList.contains("wg-collapsed"), true);
1114
976
  });
1115
977
  }
978
+ const miniNext = panel.querySelector("[data-wg-mini-next]");
979
+ if (miniNext && !miniNext.dataset.wgWired) {
980
+ miniNext.dataset.wgWired = "1";
981
+ miniNext.addEventListener("click", (e) => {
982
+ e.stopPropagation();
983
+ const runBtn = document.getElementById("wg-run");
984
+ if (runBtn && !runBtn.disabled) {
985
+ runBtn.click();
986
+ return;
987
+ }
988
+ if (typeof window.__wgNext === "function") window.__wgNext({});
989
+ });
990
+ }
991
+ const autoCb = document.getElementById("wg-autoplay-cb");
992
+ if (autoCb && !autoCb.dataset.wgMiniWired) {
993
+ autoCb.dataset.wgMiniWired = "1";
994
+ autoCb.addEventListener("change", () => {
995
+ syncMiniNext(panel.classList.contains("wg-collapsed"));
996
+ });
997
+ }
1116
998
  const cur = panel.querySelector("#wg-modules .wg-mod-current");
1117
999
  if (cur && typeof cur.scrollIntoView === "function") {
1118
1000
  try {
@@ -1145,8 +1027,6 @@ async function renderBeforeStep(page, info) {
1145
1027
  if (window.__wgHideCursor) window.__wgHideCursor();
1146
1028
  })
1147
1029
  .catch(() => {});
1148
- // miniStepLabel is Node-only — compute before evaluate (browser has no closure).
1149
- const payload = { ...info, stepLabel: miniStepLabel(info) };
1150
1030
  await page
1151
1031
  .evaluate((info) => {
1152
1032
  // Reused in place, not removed + recreated, every step - the old
@@ -1169,7 +1049,7 @@ async function renderBeforeStep(page, info) {
1169
1049
  // active, the episode 2 tab is inactive"). Absent entirely for an ad
1170
1050
  // hoc block chain with no named Flows.
1171
1051
  const episodesHtml = info.allEpisodes && info.allEpisodes.length > 0
1172
- ? "<div id=\\\"wg-episodes\\\">" +
1052
+ ? "<div id=\\"wg-episodes\\">" +
1173
1053
  info.allEpisodes
1174
1054
  .map((e) => {
1175
1055
  const cls = e.episodeNumber < info.episodeNumber ? "wg-ep-done"
@@ -1180,7 +1060,7 @@ async function renderBeforeStep(page, info) {
1180
1060
  const enteredCls = info.justEnteredEpisode && e.episodeNumber === info.episodeNumber
1181
1061
  ? " wg-ep-entered"
1182
1062
  : "";
1183
- return "<span class=\\\"wg-ep-tab " + cls + enteredCls + "\\\">Episode " + e.episodeNumber + ": " +
1063
+ return "<span class=\\"wg-ep-tab " + cls + enteredCls + "\\">Episode " + e.episodeNumber + ": " +
1184
1064
  esc(e.episodeTitle || "") + "</span>";
1185
1065
  })
1186
1066
  .join("") +
@@ -1196,66 +1076,40 @@ async function renderBeforeStep(page, info) {
1196
1076
  .map((name, idx) => {
1197
1077
  const cls = idx < info.moduleIndex ? "wg-mod-done" : idx === info.moduleIndex ? "wg-mod-current" : "wg-mod-upcoming";
1198
1078
  const desc = info.allDescriptions && info.allDescriptions[idx];
1199
- const titleAttr = desc ? " title=\\\"" + esc(desc) + "\\\"" : "";
1200
- return "<span class=\\\"wg-mod " + cls + "\\\"" + titleAttr + ">" + name + "</span>";
1079
+ const titleAttr = desc ? " title=\\"" + esc(desc) + "\\"" : "";
1080
+ return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
1201
1081
  })
1202
1082
  .join("");
1203
1083
  const narrationHtml = info.description
1204
- ? "<div class=\\\"wg-narration\\\">" + esc(info.description) + "</div>"
1084
+ ? "<div class=\\"wg-narration\\">" + esc(info.description) + "</div>"
1205
1085
  : "";
1206
1086
  const paceHtml =
1207
1087
  info.paceBadge || info.paceLabel
1208
- ? "<div class=\\\"wg-pace\\\" data-pace-kind=\\\"" +
1088
+ ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
1209
1089
  esc(info.paceKind || "normal") +
1210
- "\\\"><span class=\\\"wg-pace-badge\\\">" +
1090
+ "\\"><span class=\\"wg-pace-badge\\">" +
1211
1091
  esc(info.paceBadge || "1x") +
1212
1092
  "</span><span>" +
1213
1093
  esc(info.paceLabel || "") +
1214
1094
  "</span></div>"
1215
1095
  : "";
1216
1096
  let html =
1217
- "<div id=\\\"wg-progress\\\"><div id=\\\"wg-progress-bar\\\" style=\\\"width:" + pct + "%\\\"></div></div>" +
1097
+ "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
1218
1098
  episodesHtml +
1219
1099
  paceHtml +
1220
- "<div id=\\\"wg-modules\\\" class=\\\"" + modulesClass + "\\\">" + modulesHtml + "</div>" +
1100
+ "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1221
1101
  "<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>" +
1222
1102
  narrationHtml +
1223
- ((() => {
1224
- const list = info.todos || [];
1225
- if (!list.length) return "";
1226
- return (
1227
- '<ul id="wg-todos">' +
1228
- list
1229
- .map((t) => {
1230
- const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
1231
- const mark = t.done
1232
- ? String.fromCharCode(0x2713)
1233
- : t.current
1234
- ? String.fromCharCode(0x2192)
1235
- : String.fromCharCode(0x25cb);
1236
- return (
1237
- '<li class="' +
1238
- cls +
1239
- '"><span class="wg-todo-mark">' +
1240
- mark +
1241
- '</span><span>' +
1242
- esc(t.text || "") +
1243
- '</span></li>'
1244
- );
1245
- })
1246
- .join("") +
1247
- '</ul>'
1248
- );
1249
- })());
1103
+ todosHtmlFromInfo(info, esc);
1250
1104
  if (info.keys.length === 0) {
1251
- html += "<div class=\\\"wg-key\\\">(no MemKeys required)</div>";
1105
+ html += "<div class=\\"wg-key\\">(no MemKeys required)</div>";
1252
1106
  }
1253
1107
  for (const k of info.keys) {
1254
1108
  html +=
1255
- "<div class=\\\"wg-key\\\"><label>" + k.name + "</label>" +
1256
- "<textarea data-key=\\\"" + k.name + "\\\" rows=\\\"2\\\">" +
1109
+ "<div class=\\"wg-key\\"><label>" + k.name + "</label>" +
1110
+ "<textarea data-key=\\"" + k.name + "\\" rows=\\"2\\">" +
1257
1111
  k.value.replace(/</g, "&lt;") + "</textarea>" +
1258
- "<div class=\\\"wg-key-pretty\\\" data-pretty-for=\\\"" + esc(k.name) + "\\\"></div>" +
1112
+ "<div class=\\"wg-key-pretty\\" data-pretty-for=\\"" + esc(k.name) + "\\"></div>" +
1259
1113
  "</div>";
1260
1114
  }
1261
1115
  let autoNow = false;
@@ -1265,12 +1119,12 @@ async function renderBeforeStep(page, info) {
1265
1119
  /* private mode / blocked storage - defaults to manual */
1266
1120
  }
1267
1121
  html +=
1268
- "<div class=\\\"wg-autoplay-row\\\"><label><input type=\\\"checkbox\\\" id=\\\"wg-autoplay-cb\\\"" +
1122
+ "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
1269
1123
  (autoNow ? " checked" : "") +
1270
1124
  "> Auto-advance</label></div>" +
1271
- "<div id=\\\"wg-gate-manual\\\"" + (autoNow ? " style=\\\"display:none\\\"" : "") +
1272
- "><button id=\\\"wg-run\\\">Run this step \\u25B6</button></div>" +
1273
- "<div id=\\\"wg-gate-auto\\\" class=\\\"wg-auto\\\"" + (autoNow ? "" : " style=\\\"display:none\\\"") +
1125
+ "<div id=\\"wg-gate-manual\\"" + (autoNow ? " style=\\"display:none\\"" : "") +
1126
+ "><button id=\\"wg-run\\">Run this step \\u25B6</button></div>" +
1127
+ "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" + (autoNow ? "" : " style=\\"display:none\\"") +
1274
1128
  ">Auto-advancing...</div>";
1275
1129
  panel.innerHTML = html;
1276
1130
  if (isNewPanel) {
@@ -1279,7 +1133,7 @@ async function renderBeforeStep(page, info) {
1279
1133
  }
1280
1134
  if (window.__wgWirePanelChrome) {
1281
1135
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1282
- stepLabel: info.stepLabel || "",
1136
+ stepLabel: miniStepLabel(info),
1283
1137
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
1284
1138
  });
1285
1139
  }
@@ -1303,7 +1157,7 @@ async function renderBeforeStep(page, info) {
1303
1157
  return Object.entries(parsed)
1304
1158
  .map(([field, value]) => {
1305
1159
  const label = field.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
1306
- return "<div class=\\\"wg-key-pretty-row\\\"><span class=\\\"wg-key-pretty-label\\\">" +
1160
+ return "<div class=\\"wg-key-pretty-row\\"><span class=\\"wg-key-pretty-label\\">" +
1307
1161
  esc(label) + ":</span> " + esc(String(value)) + "</div>";
1308
1162
  })
1309
1163
  .join("");
@@ -1312,7 +1166,7 @@ async function renderBeforeStep(page, info) {
1312
1166
  const prettyOn = window.__wgPretty !== false;
1313
1167
  panel.querySelectorAll("textarea[data-key]").forEach((ta) => {
1314
1168
  const key = ta.getAttribute("data-key");
1315
- const previewEl = panel.querySelector(".wg-key-pretty[data-pretty-for=\\\"" + key + "\\\"]");
1169
+ const previewEl = panel.querySelector(".wg-key-pretty[data-pretty-for=\\"" + key + "\\"]");
1316
1170
  if (!previewEl) return;
1317
1171
  previewEl.style.display = prettyOn ? "" : "none";
1318
1172
  if (prettyOn) previewEl.innerHTML = prettyMemValue(ta.value);
@@ -1352,31 +1206,12 @@ async function renderBeforeStep(page, info) {
1352
1206
  if (auto) auto.style.display = cb.checked ? "" : "none";
1353
1207
  });
1354
1208
  }
1355
- }, payload)
1209
+ }, info)
1356
1210
  .catch(() => {});
1357
1211
  }
1358
1212
 
1359
1213
  async function ensureSelectorInView(page, selector) {
1360
1214
  try {
1361
- // Body-scale camera + scrollIntoView = teleport / "outside viewport" loop.
1362
- // When zoomed, pan the camera to the target instead of scrolling the document.
1363
- const zoomed = await page
1364
- .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
1365
- .catch(() => false);
1366
- if (zoomed) {
1367
- await page
1368
- .evaluate((sel) => {
1369
- const el = document.querySelector(sel);
1370
- if (!el || !window.__wgSetCamera || !window.__wgElDocPoint) return;
1371
- const cam = window.__wgCamera;
1372
- const pt = window.__wgElDocPoint(el);
1373
- window.__wgCameraLock = true;
1374
- window.__wgSetCamera(pt.x, pt.y, cam.scale > 1 ? cam.scale : 1.4, 320);
1375
- }, selector)
1376
- .catch(() => {});
1377
- await new Promise((res) => setTimeout(res, 340));
1378
- return;
1379
- }
1380
1215
  const loc = page.locator(selector).first();
1381
1216
  await loc.scrollIntoViewIfNeeded().catch(() => {});
1382
1217
  await page
@@ -1443,54 +1278,48 @@ async function ensureSelectorInView(page, selector) {
1443
1278
  }
1444
1279
  }
1445
1280
 
1446
- async function applyCameraZoom(page, box, zoom, ms) {
1447
- const z = zoom != null && Number(zoom) > 1 ? Number(zoom) : 1;
1448
- if (!box || z <= 1) {
1281
+ async function applyHighlightZoom(page, selector, zoom) {
1282
+ const z = Number(zoom);
1283
+ if (!Number.isFinite(z) || z <= 1.001) {
1449
1284
  await page.evaluate(() => {
1450
- if (window.__wgResetCamera) window.__wgResetCamera();
1285
+ if (window.__wgClearZoom) window.__wgClearZoom();
1451
1286
  }).catch(() => {});
1452
1287
  return;
1453
1288
  }
1454
- const vx = box.x + box.width / 2;
1455
- const vy = box.y + box.height / 2;
1456
- const dur = ms != null ? ms : 650;
1457
- await page
1458
- .evaluate(({ vx, vy, scale, dur }) => {
1459
- if (!window.__wgSetCamera) return;
1460
- const pt = window.__wgViewportToDoc
1461
- ? window.__wgViewportToDoc(vx, vy)
1462
- : { x: vx, y: vy };
1463
- window.__wgSetCamera(pt.x, pt.y, scale, dur);
1464
- }, { vx, vy, scale: z, dur })
1465
- .catch(() => {});
1466
- }
1467
-
1468
- async function setDemoTodos(page, todos) {
1469
- await page
1470
- .evaluate((list) => {
1471
- if (window.__wgSetTodos) window.__wgSetTodos(list || []);
1472
- }, todos && todos.length ? todos : [])
1473
- .catch(() => {});
1474
- }
1475
-
1476
- async function resetCameraZoom(page) {
1477
1289
  await page
1478
- .evaluate(() => {
1479
- if (window.__wgResetCamera) window.__wgResetCamera();
1480
- })
1290
+ .evaluate(
1291
+ ({ sel, scale }) => {
1292
+ if (window.__wgApplyZoom) window.__wgApplyZoom(sel, scale);
1293
+ },
1294
+ { sel: selector, scale: z },
1295
+ )
1481
1296
  .catch(() => {});
1482
1297
  }
1483
1298
 
1484
- async function setDemoBannerTitle(page, title) {
1485
- if (title == null || title === "") return;
1486
- await page
1487
- .evaluate((t) => {
1488
- if (window.__wgSetBannerTitle) window.__wgSetBannerTitle(t);
1489
- }, String(title))
1490
- .catch(() => {});
1299
+ function todosHtmlFromInfo(info, escFn) {
1300
+ const list = info.todos || [];
1301
+ if (!list.length) return "";
1302
+ return (
1303
+ "<ul id=\\"wg-todos\\">" +
1304
+ list
1305
+ .map((t) => {
1306
+ const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
1307
+ const mark = t.done ? "\\u2713" : t.current ? "\\u2192" : "\\u25CB";
1308
+ return (
1309
+ "<li class=\\"" +
1310
+ cls +
1311
+ "\\"><span class=\\"wg-todo-mark\\">" +
1312
+ mark +
1313
+ "</span><span>" +
1314
+ escFn(t.text || "") +
1315
+ "</span></li>"
1316
+ );
1317
+ })
1318
+ .join("") +
1319
+ "</ul>"
1320
+ );
1491
1321
  }
1492
1322
 
1493
- /** Collapsed chrome title: Ep N / i / n / block / pace. */
1494
1323
  function miniStepLabel(info) {
1495
1324
  const ep =
1496
1325
  info.episodeNumber !== undefined && info.episodeNumber !== null
@@ -1508,174 +1337,94 @@ function miniStepLabel(info) {
1508
1337
 
1509
1338
  async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1510
1339
  const list = highlights || [];
1340
+ // defaultHoldMs: when set (fail path), use instead of legacy 900/200 so BUG
1341
+ // rings stay visible ~2s before the error panel (PIA stubOnError RFC).
1511
1342
  const defaultHoldMs = opts && opts.defaultHoldMs != null ? opts.defaultHoldMs : null;
1512
1343
  const pace = opts && opts.pace !== undefined ? opts.pace : undefined;
1513
- const fadeMs = 320;
1514
- const n = list.length;
1515
- const baseStepLabel = (opts && opts.stepLabel) || "";
1516
-
1517
- const updateHlProgress = async (i) => {
1518
- if (n <= 0) return;
1519
- const tag = "[" + (i + 1) + "/" + n + "]";
1520
- await page
1521
- .evaluate(
1522
- ({ tag, baseStepLabel, i, n }) => {
1523
- const panel = document.getElementById("wg-panel");
1524
- if (!panel) return;
1525
- panel.dataset.wgHlIdx = String(i + 1);
1526
- panel.dataset.wgHlN = String(n);
1527
- const titleEl = panel.querySelector(".wg-chrome-title");
1528
- if (titleEl) {
1529
- const base = panel.dataset.wgStepLabel || baseStepLabel || "waygraph demo";
1530
- if (panel.classList.contains("wg-collapsed")) {
1531
- titleEl.textContent = base + " " + tag;
1532
- }
1533
- }
1534
- let span = document.getElementById("wg-hl-idx");
1535
- const h3 = panel.querySelector("h3");
1536
- if (h3) {
1537
- if (!span) {
1538
- span = document.createElement("span");
1539
- span.id = "wg-hl-idx";
1540
- span.style.marginLeft = "8px";
1541
- span.style.color = "#c9a6ff";
1542
- span.style.fontWeight = "700";
1543
- h3.appendChild(span);
1544
- }
1545
- span.textContent = tag;
1546
- }
1547
- },
1548
- { tag, baseStepLabel, i, n },
1549
- )
1550
- .catch(() => {});
1551
- };
1552
-
1553
- const stopLiveFollow = async () => {
1554
- await page
1555
- .evaluate(() => {
1556
- if (window.__wgStopHighlightTrack) window.__wgStopHighlightTrack();
1557
- if (window.__wgRingRaf) {
1558
- cancelAnimationFrame(window.__wgRingRaf);
1559
- window.__wgRingRaf = null;
1560
- }
1561
- if (window.__wgRingTrack) {
1562
- window.removeEventListener("mousemove", window.__wgRingTrack);
1563
- window.removeEventListener("resize", window.__wgRingTrack);
1564
- window.removeEventListener("scroll", window.__wgRingTrack, true);
1565
- window.__wgRingTrack = null;
1566
- }
1567
- if (window.__wgHideFocusVeil) window.__wgHideFocusVeil();
1568
- })
1569
- .catch(() => {});
1570
- };
1571
-
1572
1344
  for (let i = 0; i < list.length; i++) {
1573
1345
  const h = list[i];
1574
1346
  try {
1575
- await stopLiveFollow();
1576
1347
  await ensureSelectorInView(page, h.selector);
1348
+ await applyHighlightZoom(page, h.selector, h.zoom);
1577
1349
  const box = await page.locator(h.selector).first().boundingBox();
1578
- if (!box) continue;
1579
- await updateHlProgress(i);
1580
- if (opts && opts.todos && opts.todos.length) {
1581
- const rows = opts.todos.map((t, ti) => ({
1582
- text: t.text,
1583
- done: ti < i,
1584
- current: ti === i,
1585
- }));
1586
- await setDemoTodos(page, rows);
1587
- } else if (h.todos && h.todos.length) {
1588
- await setDemoTodos(page, normalizeTodos(h.todos, h.todoIndex));
1589
- }
1590
- const z = h.zoom != null && Number(h.zoom) > 1 ? Number(h.zoom) : 1;
1591
- const camMs = 700;
1592
- // Camera auto-centers on the ring target. Host mouse never steers.
1593
- await page
1594
- .evaluate(
1595
- ({ sel, label, tone, size, weight, focus, color, zoom, camMs }) => {
1596
- if (window.__wgStartHighlightTrack) {
1597
- window.__wgStartHighlightTrack({
1598
- sel,
1599
- label,
1600
- tone,
1601
- size,
1602
- weight,
1603
- focus,
1604
- color,
1605
- zoom,
1606
- camMs,
1607
- });
1608
- }
1609
- },
1610
- {
1611
- sel: h.selector,
1612
- label: h.label,
1613
- tone: h.tone || "planned",
1614
- size: h.size || "md",
1615
- weight: h.weight || "normal",
1616
- focus: !!h.focus,
1617
- color: h.color || h.labelColor || h.textColor || "",
1618
- zoom: z,
1619
- camMs,
1620
- },
1621
- )
1622
- .catch(() => {});
1623
- const alreadyZoomed = await page
1624
- .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
1625
- .catch(() => false);
1626
- const waitMs = alreadyZoomed ? Math.min(camMs, 420) : camMs;
1627
- await new Promise((res) => setTimeout(res, waitMs));
1628
- if (h.cursor !== false) {
1629
- // Park Playwright real mouse + demo cursor on the LIVE centered target.
1630
- const live = await page
1631
- .evaluate((sel) => {
1632
- const el = document.querySelector(sel);
1633
- if (!el) return null;
1634
- const r = el.getBoundingClientRect();
1635
- return { x: r.left, y: r.top, width: r.width, height: r.height };
1636
- }, h.selector)
1637
- .catch(() => null);
1638
- if (live) {
1639
- await moveCursorTo(page, live, Math.min(280, camMs), { keepLock: true });
1350
+ if (box) {
1351
+ await showRing(page, box, h.label, h.tone || "planned", {
1352
+ size: h.size,
1353
+ weight: h.weight,
1354
+ });
1355
+ if ((opts && opts.todos && opts.todos.length) || (h.todos && h.todos.length)) {
1356
+ const rows =
1357
+ opts && opts.todos && opts.todos.length
1358
+ ? opts.todos
1359
+ : normalizeTodos(h.todos, h.todoIndex);
1360
+ await page
1361
+ .evaluate((todos) => {
1362
+ const ul = document.getElementById("wg-todos");
1363
+ if (!ul) return;
1364
+ ul.innerHTML = todos
1365
+ .map((t) => {
1366
+ const cls = t.current
1367
+ ? "wg-todo-current"
1368
+ : t.done
1369
+ ? "wg-todo-done"
1370
+ : "wg-todo-pending";
1371
+ const mark = t.done ? "\\u2713" : t.current ? "\\u2192" : "\\u25CB";
1372
+ const esc = (s) =>
1373
+ String(s)
1374
+ .replace(/&/g, "&amp;")
1375
+ .replace(/</g, "&lt;")
1376
+ .replace(/>/g, "&gt;");
1377
+ return (
1378
+ "<li class=\\"" +
1379
+ cls +
1380
+ "\\"><span class=\\"wg-todo-mark\\">" +
1381
+ mark +
1382
+ "</span><span>" +
1383
+ esc(t.text || "") +
1384
+ "</span></li>"
1385
+ );
1386
+ })
1387
+ .join("");
1388
+ }, rows)
1389
+ .catch(() => {});
1640
1390
  }
1391
+ const authored = resolveFixtureDwellMs(h, { gatesFast, pace });
1392
+ const legacyMs = i === list.length - 1 ? 200 : 900;
1393
+ const holdMs =
1394
+ authored != null ? authored : defaultHoldMs != null ? defaultHoldMs : legacyMs;
1395
+ await new Promise((res) => setTimeout(res, holdMs));
1641
1396
  }
1642
- // Appear settle
1643
- await new Promise((res) => setTimeout(res, fadeMs));
1644
- const authored = resolveFixtureDwellMs(h, { gatesFast, pace });
1645
- const legacyMs = 1100;
1646
- const holdMs =
1647
- authored != null ? authored : defaultHoldMs != null ? defaultHoldMs : legacyMs;
1648
- await new Promise((res) => setTimeout(res, holdMs));
1649
- // Disappear before next. zoomOut:false keeps camera for the next ring.
1650
- await stopLiveFollow();
1651
- await hideRing(page);
1652
- const doZoomOut = h.zoomOut !== false;
1653
- if (doZoomOut) await resetCameraZoom(page);
1654
- await new Promise((res) => setTimeout(res, doZoomOut ? fadeMs : 120));
1655
1397
  } catch {
1656
- /* best-effort */
1398
+ // best-effort - a selector that doesn't resolve just gets skipped
1657
1399
  }
1658
1400
  }
1659
- await stopLiveFollow();
1660
- // Keep camera if the last ring (or phase default) asked zoomOut:false.
1661
- const last = list.length > 0 ? list[list.length - 1] : null;
1662
- if (!last || last.zoomOut !== false) {
1663
- await resetCameraZoom(page);
1664
- } else {
1665
- // Hold lock into the upcoming act click so follow/scroll cannot yank
1666
- // the camera off the already-centered target (cart teleport bug).
1401
+ if (list.length > 0) {
1402
+ const last = list[list.length - 1];
1667
1403
  await page
1668
- .evaluate(() => {
1669
- window.__wgCameraLock = true;
1670
- })
1404
+ .evaluate((h) => {
1405
+ if (!window.__wgPositionRing) return;
1406
+ const reposition = () => {
1407
+ const el = document.querySelector(h.selector);
1408
+ if (!el) {
1409
+ if (window.__wgHideRing) window.__wgHideRing();
1410
+ return;
1411
+ }
1412
+ window.__wgPositionRing(el.getBoundingClientRect(), h.label, h.tone || "planned", {
1413
+ size: h.size || "md",
1414
+ weight: h.weight || "normal",
1415
+ });
1416
+ };
1417
+ reposition();
1418
+ window.__wgRingTrack = reposition;
1419
+ window.addEventListener("resize", reposition);
1420
+ window.addEventListener("scroll", reposition, true);
1421
+ }, last)
1671
1422
  .catch(() => {});
1672
- }
1673
- if (list.length === 0) {
1423
+ } else {
1674
1424
  await hideRing(page);
1675
1425
  }
1676
1426
  }
1677
1427
 
1678
-
1679
1428
  async function renderAfterStep(page, info) {
1680
1429
  await installOverlay(page, info.title);
1681
1430
  // Clear any tracker from a previous highlight before cycling through this
@@ -1698,9 +1447,7 @@ async function renderAfterStep(page, info) {
1698
1447
  // 900ms / 200ms holds when set.
1699
1448
  await cycleHighlightRings(page, highlights, gatesFast, {
1700
1449
  pace: info.pace,
1701
- stepLabel: miniStepLabel(info),
1702
1450
  });
1703
- const afterPayload = { ...info, stepLabel: miniStepLabel(info) };
1704
1451
  await page
1705
1452
  .evaluate((info) => {
1706
1453
  // Reused in place - see renderBeforeStep's own comment on this.
@@ -1721,8 +1468,8 @@ async function renderAfterStep(page, info) {
1721
1468
  .map((name, idx) => {
1722
1469
  const cls = idx <= info.moduleIndex ? "wg-mod-done" : "wg-mod-upcoming";
1723
1470
  const desc = info.allDescriptions && info.allDescriptions[idx];
1724
- const titleAttr = desc ? " title=\\\"" + escA(desc) + "\\\"" : "";
1725
- return "<span class=\\\"wg-mod " + cls + "\\\"" + titleAttr + ">" + name + "</span>";
1471
+ const titleAttr = desc ? " title=\\"" + escA(desc) + "\\"" : "";
1472
+ return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
1726
1473
  })
1727
1474
  .join("");
1728
1475
  // QA-friendly by default ("LoginSuccess" -> "Login Success") - raw
@@ -1733,13 +1480,13 @@ async function renderAfterStep(page, info) {
1733
1480
  .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") || info.resultTag;
1734
1481
  const pretty = window.__wgPretty !== false;
1735
1482
  const resultHtml =
1736
- "<div class=\\\"wg-toggle\\\">" +
1737
- "<button class=\\\"wg-toggle-btn" + (pretty ? " wg-active" : "") + "\\\" data-mode=\\\"pretty\\\">Pretty</button>" +
1738
- "<button class=\\\"wg-toggle-btn" + (pretty ? "" : " wg-active") + "\\\" data-mode=\\\"json\\\">JSON</button>" +
1483
+ "<div class=\\"wg-toggle\\">" +
1484
+ "<button class=\\"wg-toggle-btn" + (pretty ? " wg-active" : "") + "\\" data-mode=\\"pretty\\">Pretty</button>" +
1485
+ "<button class=\\"wg-toggle-btn" + (pretty ? "" : " wg-active") + "\\" data-mode=\\"json\\">JSON</button>" +
1739
1486
  "</div>" +
1740
- "<div class=\\\"wg-result wg-result-pretty\\\" style=\\\"display:" + (pretty ? "block" : "none") + "\\\">" +
1487
+ "<div class=\\"wg-result wg-result-pretty\\" style=\\"display:" + (pretty ? "block" : "none") + "\\">" +
1741
1488
  prettyText + "</div>" +
1742
- "<div class=\\\"wg-result wg-result-json\\\" style=\\\"display:" + (pretty ? "none" : "block") + "\\\">" +
1489
+ "<div class=\\"wg-result wg-result-json\\" style=\\"display:" + (pretty ? "none" : "block") + "\\">" +
1743
1490
  info.resultTag + "</div>";
1744
1491
  let autoNow = false;
1745
1492
  try {
@@ -1748,21 +1495,21 @@ async function renderAfterStep(page, info) {
1748
1495
  /* private mode / blocked storage - defaults to manual */
1749
1496
  }
1750
1497
  const gateHtml =
1751
- "<div class=\\\"wg-autoplay-row\\\"><label><input type=\\\"checkbox\\\" id=\\\"wg-autoplay-cb\\\"" +
1498
+ "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
1752
1499
  (autoNow ? " checked" : "") +
1753
1500
  "> Auto-advance</label></div>" +
1754
- "<div id=\\\"wg-gate-manual\\\"" + (autoNow ? " style=\\\"display:none\\\"" : "") +
1755
- "><button id=\\\"wg-run\\\">" + buttonLabel + "</button></div>" +
1756
- "<div id=\\\"wg-gate-auto\\\" class=\\\"wg-auto\\\"" + (autoNow ? "" : " style=\\\"display:none\\\"") +
1501
+ "<div id=\\"wg-gate-manual\\"" + (autoNow ? " style=\\"display:none\\"" : "") +
1502
+ "><button id=\\"wg-run\\">" + buttonLabel + "</button></div>" +
1503
+ "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" + (autoNow ? "" : " style=\\"display:none\\"") +
1757
1504
  ">Auto-advancing...</div>";
1758
1505
  const episodesHtml = info.allEpisodes && info.allEpisodes.length > 0
1759
- ? "<div id=\\\"wg-episodes\\\">" +
1506
+ ? "<div id=\\"wg-episodes\\">" +
1760
1507
  info.allEpisodes
1761
1508
  .map((e) => {
1762
1509
  const cls = e.episodeNumber < info.episodeNumber ? "wg-ep-done"
1763
1510
  : e.episodeNumber === info.episodeNumber ? "wg-ep-current"
1764
1511
  : "wg-ep-upcoming";
1765
- return "<span class=\\\"wg-ep-tab " + cls + "\\\">Episode " + e.episodeNumber + ": " +
1512
+ return "<span class=\\"wg-ep-tab " + cls + "\\">Episode " + e.episodeNumber + ": " +
1766
1513
  escA(e.episodeTitle || "") + "</span>";
1767
1514
  })
1768
1515
  .join("") +
@@ -1770,47 +1517,21 @@ async function renderAfterStep(page, info) {
1770
1517
  : "";
1771
1518
  const paceHtml =
1772
1519
  info.paceBadge || info.paceLabel
1773
- ? "<div class=\\\"wg-pace\\\" data-pace-kind=\\\"" +
1520
+ ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
1774
1521
  escA(info.paceKind || "normal") +
1775
- "\\\"><span class=\\\"wg-pace-badge\\\">" +
1522
+ "\\"><span class=\\"wg-pace-badge\\">" +
1776
1523
  escA(info.paceBadge || "1x") +
1777
1524
  "</span><span>" +
1778
1525
  escA(info.paceLabel || "") +
1779
1526
  "</span></div>"
1780
1527
  : "";
1781
1528
  panel.innerHTML =
1782
- "<div id=\\\"wg-progress\\\"><div id=\\\"wg-progress-bar\\\" style=\\\"width:" + pct + "%\\\"></div></div>" +
1529
+ "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
1783
1530
  episodesHtml +
1784
1531
  paceHtml +
1785
- "<div id=\\\"wg-modules\\\" class=\\\"" + modulesClass + "\\\">" + modulesHtml + "</div>" +
1532
+ "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1786
1533
  "<h3>" + heading + "</h3>" +
1787
- ((() => {
1788
- const list = info.todos || [];
1789
- if (!list.length) return "";
1790
- return (
1791
- '<ul id="wg-todos">' +
1792
- list
1793
- .map((t) => {
1794
- const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
1795
- const mark = t.done
1796
- ? String.fromCharCode(0x2713)
1797
- : t.current
1798
- ? String.fromCharCode(0x2192)
1799
- : String.fromCharCode(0x25cb);
1800
- return (
1801
- '<li class="' +
1802
- cls +
1803
- '"><span class="wg-todo-mark">' +
1804
- mark +
1805
- '</span><span>' +
1806
- escA(t.text || "") +
1807
- '</span></li>'
1808
- );
1809
- })
1810
- .join("") +
1811
- '</ul>'
1812
- );
1813
- })()) +
1534
+ todosHtmlFromInfo(info, escA) +
1814
1535
  resultHtml +
1815
1536
  gateHtml;
1816
1537
  if (isNewPanel) {
@@ -1819,7 +1540,7 @@ async function renderAfterStep(page, info) {
1819
1540
  }
1820
1541
  if (window.__wgWirePanelChrome) {
1821
1542
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1822
- stepLabel: info.stepLabel || "",
1543
+ stepLabel: miniStepLabel(info),
1823
1544
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
1824
1545
  });
1825
1546
  }
@@ -1849,7 +1570,7 @@ async function renderAfterStep(page, info) {
1849
1570
  if (auto) auto.style.display = cb.checked ? "" : "none";
1850
1571
  });
1851
1572
  }
1852
- }, afterPayload)
1573
+ }, info)
1853
1574
  .catch(() => {});
1854
1575
  }
1855
1576
 
@@ -1861,7 +1582,6 @@ async function renderAfterStep(page, info) {
1861
1582
  */
1862
1583
  async function renderStepError(page, info) {
1863
1584
  await installOverlay(page, info.title);
1864
- const errorPayload = { ...info, stepLabel: miniStepLabel(info) };
1865
1585
  await page
1866
1586
  .evaluate((info) => {
1867
1587
  // Reused in place - see renderBeforeStep's own comment on this.
@@ -1878,13 +1598,13 @@ async function renderStepError(page, info) {
1878
1598
  if (!isNewPanel) panel.classList.add("wg-in");
1879
1599
  const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1880
1600
  const episodesHtml = info.allEpisodes && info.allEpisodes.length > 0
1881
- ? "<div id=\\\"wg-episodes\\\">" +
1601
+ ? "<div id=\\"wg-episodes\\">" +
1882
1602
  info.allEpisodes
1883
1603
  .map((e) => {
1884
1604
  const cls = e.episodeNumber < info.episodeNumber ? "wg-ep-done"
1885
1605
  : e.episodeNumber === info.episodeNumber ? "wg-ep-current"
1886
1606
  : "wg-ep-upcoming";
1887
- return "<span class=\\\"wg-ep-tab " + cls + "\\\">Episode " + e.episodeNumber + ": " +
1607
+ return "<span class=\\"wg-ep-tab " + cls + "\\">Episode " + e.episodeNumber + ": " +
1888
1608
  esc(e.episodeTitle || "") + "</span>";
1889
1609
  })
1890
1610
  .join("") +
@@ -1895,8 +1615,8 @@ async function renderStepError(page, info) {
1895
1615
  .map((name, idx) => {
1896
1616
  const cls = idx === info.moduleIndex ? "wg-mod-current" : idx < info.moduleIndex ? "wg-mod-done" : "wg-mod-upcoming";
1897
1617
  const desc = info.allDescriptions && info.allDescriptions[idx];
1898
- const titleAttr = desc ? " title=\\\"" + esc(desc) + "\\\"" : "";
1899
- return "<span class=\\\"wg-mod " + cls + "\\\"" + titleAttr + ">" + name + "</span>";
1618
+ const titleAttr = desc ? " title=\\"" + esc(desc) + "\\"" : "";
1619
+ return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
1900
1620
  })
1901
1621
  .join("");
1902
1622
  const retryLabel = info.episodeNumber !== undefined ? "Retry Episode " + info.episodeNumber : "Retry";
@@ -1904,17 +1624,17 @@ async function renderStepError(page, info) {
1904
1624
  ? "Expected outcome - Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + " failed as intended"
1905
1625
  : "Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + " threw";
1906
1626
  const reasonHtml = isExpected
1907
- ? "<div class=\\\"wg-expected-reason\\\">" + esc(info.expectedFailureReason) + "</div>"
1627
+ ? "<div class=\\"wg-expected-reason\\">" + esc(info.expectedFailureReason) + "</div>"
1908
1628
  : "";
1909
1629
  panel.innerHTML =
1910
1630
  episodesHtml +
1911
- "<div id=\\\"wg-modules\\\" class=\\\"" + modulesClass + "\\\">" + modulesHtml + "</div>" +
1912
- "<h3 class=\\\"" + (isExpected ? "wg-expected-heading" : "wg-error-heading") + "\\\">" + headingText + "</h3>" +
1631
+ "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1632
+ "<h3 class=\\"" + (isExpected ? "wg-expected-heading" : "wg-error-heading") + "\\">" + headingText + "</h3>" +
1913
1633
  reasonHtml +
1914
- "<div class=\\\"wg-error-msg\\\">" + esc(info.message) + "</div>" +
1915
- "<div class=\\\"wg-error-actions\\\">" +
1916
- "<button id=\\\"wg-error-retry\\\" class=\\\"wg-error-retry\\\">" + esc(retryLabel) + "</button>" +
1917
- "<button id=\\\"wg-run\\\" class=\\\"wg-error-stop\\\">Stop</button>" +
1634
+ "<div class=\\"wg-error-msg\\">" + esc(info.message) + "</div>" +
1635
+ "<div class=\\"wg-error-actions\\">" +
1636
+ "<button id=\\"wg-error-retry\\" class=\\"wg-error-retry\\">" + esc(retryLabel) + "</button>" +
1637
+ "<button id=\\"wg-run\\" class=\\"wg-error-stop\\">Stop</button>" +
1918
1638
  "</div>";
1919
1639
  if (isNewPanel) {
1920
1640
  document.documentElement.appendChild(panel);
@@ -1922,7 +1642,7 @@ async function renderStepError(page, info) {
1922
1642
  }
1923
1643
  if (window.__wgWirePanelChrome) {
1924
1644
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
1925
- stepLabel: info.stepLabel || "",
1645
+ stepLabel: miniStepLabel(info),
1926
1646
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
1927
1647
  });
1928
1648
  }
@@ -1930,7 +1650,7 @@ async function renderStepError(page, info) {
1930
1650
  if (runBtn) runBtn.addEventListener("click", () => window.__wgNext({}));
1931
1651
  const retryBtn = document.getElementById("wg-error-retry");
1932
1652
  if (retryBtn) retryBtn.addEventListener("click", () => window.__wgNext({ __wgRetry: "1" }));
1933
- }, errorPayload)
1653
+ }, info)
1934
1654
  .catch(() => {});
1935
1655
  }
1936
1656
 
@@ -2046,6 +1766,11 @@ async function markStepRunning(page, _opts) {
2046
1766
  const titleEl = panel.querySelector(".wg-chrome-title");
2047
1767
  const label = panel.dataset.wgStepLabel;
2048
1768
  if (titleEl && label) titleEl.textContent = label;
1769
+ const miniNext = panel.querySelector("[data-wg-mini-next]");
1770
+ if (miniNext) {
1771
+ miniNext.disabled = true;
1772
+ miniNext.classList.remove("wg-mini-next-show");
1773
+ }
2049
1774
  }
2050
1775
  })
2051
1776
  .catch(() => {});
@@ -2054,16 +1779,14 @@ async function markStepRunning(page, _opts) {
2054
1779
  async function showRing(page, box, label, tone, style) {
2055
1780
  const size = normalizeHighlightSize(style && style.size);
2056
1781
  const weight = normalizeHighlightWeight(style && style.weight);
2057
- const focus = !!(style && style.focus);
2058
- const color = (style && (style.color || style.labelColor || style.textColor)) || "";
2059
1782
  await page
2060
1783
  .evaluate(
2061
- ({ box, label, tone, size, weight, focus, color }) => {
1784
+ ({ box, label, tone, size, weight }) => {
2062
1785
  if (window.__wgPositionRing) {
2063
- window.__wgPositionRing(box, label, tone || "planned", { size, weight, focus, color });
1786
+ window.__wgPositionRing(box, label, tone || "planned", { size, weight });
2064
1787
  }
2065
1788
  },
2066
- { box, label, tone: tone || "planned", size, weight, focus, color },
1789
+ { box, label, tone: tone || "planned", size, weight },
2067
1790
  )
2068
1791
  .catch(() => {});
2069
1792
  }
@@ -2153,10 +1876,7 @@ async function presentSlides(page, slides, _gate, opts) {
2153
1876
  if (s.selector) {
2154
1877
  try {
2155
1878
  await ensureSelectorInView(page, s.selector);
2156
- {
2157
- const zb = await page.locator(s.selector).first().boundingBox().catch(() => null);
2158
- await applyCameraZoom(page, zb, s.zoom);
2159
- }
1879
+ await applyHighlightZoom(page, s.selector, s.zoom);
2160
1880
  const box = await page.locator(s.selector).first().boundingBox();
2161
1881
  if (box) await showRing(page, box, caption, slideTone, slideStyle);
2162
1882
  } catch {
@@ -2174,31 +1894,31 @@ async function presentSlides(page, slides, _gate, opts) {
2174
1894
  panel.id = "wg-panel";
2175
1895
  }
2176
1896
  panel.classList.remove("wg-error", "wg-expected");
2177
- // Do not clear wg-collapsed here - WirePanelChrome honors Hide (localStorage)
1897
+ // Do not clear wg-collapsed - WirePanelChrome honors Hide (localStorage)
2178
1898
  // unless forceCollapsed:true (--mini / --video).
2179
1899
  const esc = (t) =>
2180
1900
  String(t).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
2181
1901
  const episodeLine =
2182
1902
  info.episodeNumber !== undefined && info.episodeNumber !== null
2183
- ? "<div class=\\\"wg-episode\\\">Episode " +
1903
+ ? "<div class=\\"wg-episode\\">Episode " +
2184
1904
  info.episodeNumber +
2185
1905
  (info.episodeTitle ? ": " + esc(info.episodeTitle) : "") +
2186
1906
  " · yap slides</div>"
2187
1907
  : "";
2188
1908
  const paceHtml =
2189
1909
  info.paceBadge || info.paceLabel
2190
- ? "<div class=\\\"wg-pace\\\" data-pace-kind=\\\"" +
1910
+ ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
2191
1911
  esc(info.paceKind || "normal") +
2192
- "\\\"><span class=\\\"wg-pace-badge\\\">" +
1912
+ "\\"><span class=\\"wg-pace-badge\\">" +
2193
1913
  esc(info.paceBadge || "1x") +
2194
1914
  "</span><span>" +
2195
1915
  esc(info.paceLabel || "") +
2196
1916
  "</span></div>"
2197
1917
  : "";
2198
1918
  panel.innerHTML =
2199
- "<div class=\\\"wg-chrome\\\"><span class=\\\"wg-chrome-title\\\">waygraph demo</span>" +
2200
- "<button type=\\\"button\\\" class=\\\"wg-hide-btn\\\" data-wg-toggle=\\\"1\\\">Hide</button></div>" +
2201
- "<div class=\\\"wg-body\\\">" +
1919
+ "<div class=\\"wg-chrome\\"><span class=\\"wg-chrome-title\\">waygraph demo</span>" +
1920
+ "<button type=\\"button\\" class=\\"wg-hide-btn\\" data-wg-toggle=\\"1\\">Hide</button></div>" +
1921
+ "<div class=\\"wg-body\\">" +
2202
1922
  episodeLine +
2203
1923
  paceHtml +
2204
1924
  "<h3>Slide " +
@@ -2208,35 +1928,35 @@ async function presentSlides(page, slides, _gate, opts) {
2208
1928
  (info.blockName ? " · " + esc(info.blockName) : "") +
2209
1929
  "</h3>" +
2210
1930
  (info.tag
2211
- ? "<div style=\\\"border:none;padding:0;margin:0 0 6px;font-size:11px;color:#c9a6ff\\\">" +
1931
+ ? "<div style=\\"border:none;padding:0;margin:0 0 6px;font-size:11px;color:#c9a6ff\\">" +
2212
1932
  esc(info.tag) +
2213
1933
  "</div>"
2214
1934
  : "") +
2215
- "<div class=\\\"wg-narration\\\">" +
1935
+ "<div class=\\"wg-narration\\">" +
2216
1936
  esc(info.caption) +
2217
1937
  "</div>" +
2218
1938
  (info.detail
2219
- ? "<div class=\\\"wg-result-pretty\\\" style=\\\"margin:0 0 12px;color:#f0e8ff\\\">" +
1939
+ ? "<div class=\\"wg-result-pretty\\" style=\\"margin:0 0 12px;color:#f0e8ff\\">" +
2220
1940
  esc(info.detail) +
2221
1941
  "</div>"
2222
1942
  : "") +
2223
1943
  (info.dwellMs != null
2224
- ? "<div class=\\\"wg-auto\\\" id=\\\"wg-dwell-hint\\\" style=\\\"margin:0 0 8px\\\">Hold " +
1944
+ ? "<div class=\\"wg-auto\\" id=\\"wg-dwell-hint\\" style=\\"margin:0 0 8px\\">Hold " +
2225
1945
  (info.dwellMs / 1000).toFixed(1) +
2226
1946
  "s before next...</div>"
2227
1947
  : "") +
2228
- "<div class=\\\"wg-autoplay-row\\\"><label><input type=\\\"checkbox\\\" id=\\\"wg-autoplay-cb\\\"" +
1948
+ "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
2229
1949
  (info.autoNow ? " checked" : "") +
2230
1950
  "> Auto-advance</label></div>" +
2231
- "<div id=\\\"wg-gate-manual\\\"" +
2232
- (info.autoNow ? " style=\\\"display:none\\\"" : "") +
2233
- "><button type=\\\"button\\\" id=\\\"wg-run\\\"" +
1951
+ "<div id=\\"wg-gate-manual\\"" +
1952
+ (info.autoNow ? " style=\\"display:none\\"" : "") +
1953
+ "><button type=\\"button\\" id=\\"wg-run\\"" +
2234
1954
  (info.dwellMs != null ? " disabled" : "") +
2235
1955
  ">" +
2236
1956
  (info.isLast ? "Continue \\u25B6" : "Next slide \\u25B6") +
2237
1957
  "</button></div>" +
2238
- "<div id=\\\"wg-gate-auto\\\" class=\\\"wg-auto\\\"" +
2239
- (info.autoNow ? "" : " style=\\\"display:none\\\"") +
1958
+ "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" +
1959
+ (info.autoNow ? "" : " style=\\"display:none\\"") +
2240
1960
  ">Auto-advancing...</div>" +
2241
1961
  "</div>";
2242
1962
  if (isNew) {
@@ -2254,7 +1974,7 @@ async function presentSlides(page, slides, _gate, opts) {
2254
1974
  " / " +
2255
1975
  info.total +
2256
1976
  (info.blockName ? " \\u00b7 " + info.blockName : ""),
2257
- // Video / --mini stays on the mini stepper (Ep N · step · block pill).
1977
+ // Video / --mini: compact pill. Else omit so Hide sticks.
2258
1978
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
2259
1979
  });
2260
1980
  }
@@ -2408,8 +2128,7 @@ async function teardownOverlay(page) {
2408
2128
  window.__wgRingTrack = null;
2409
2129
  }
2410
2130
  window.__wgNarrateOwnsRing = false;
2411
- if (window.__wgClearZoom) window.__wgClearZoom();
2412
- for (const id of ["wg-panel", "wg-ring", "wg-ring-label", "wg-cursor", "wg-click-pulse", "wg-banner", "wg-todo-modal", "wg-focus-veil"]) {
2131
+ for (const id of ["wg-panel", "wg-ring", "wg-ring-label", "wg-cursor", "wg-click-pulse", "wg-banner"]) {
2413
2132
  const el = document.getElementById(id);
2414
2133
  if (el) el.remove();
2415
2134
  }
@@ -2417,53 +2136,24 @@ async function teardownOverlay(page) {
2417
2136
  .catch(() => {});
2418
2137
  }
2419
2138
 
2420
- async function moveCursorTo(page, box, ms, opts) {
2139
+ async function moveCursorTo(page, box, ms) {
2421
2140
  const x = box.x + box.width / 2;
2422
2141
  const y = box.y + box.height / 2;
2423
2142
  const travel = Math.max(0, Number(ms) || 0);
2424
- const keepLock = !!(opts && opts.keepLock);
2425
- // Camera stays locked; demo cursor + Playwright real mouse move together.
2426
- // Host OS mouse is ignored (no mousemove camera steer).
2427
2143
  await page
2428
2144
  .evaluate(({ x, y, ms }) => {
2429
- window.__wgCameraLock = true;
2430
- window.__wgIgnoreHostMouse = true;
2431
2145
  if (window.__wgMoveCursorTo) window.__wgMoveCursorTo(x, y, ms);
2432
2146
  }, { x, y, ms: travel })
2433
2147
  .catch(() => {});
2434
- try {
2435
- if (travel > 0) {
2436
- const steps = Math.max(1, Math.min(30, Math.round(travel / 20)));
2437
- await page.mouse.move(x, y, { steps });
2438
- } else {
2439
- await page.mouse.move(x, y);
2440
- }
2441
- } catch {
2442
- /* headed / detached - demo cursor still moved */
2443
- }
2148
+ // CSS transition is async - wait the full travel so the cursor is ON the
2149
+ // target before ring/pulse/real click (NavBlock click nav was skipping
2150
+ // this and looked like a teleport).
2444
2151
  if (travel > 0) {
2445
- await new Promise((res) => setTimeout(res, Math.min(80, travel)));
2446
- }
2447
- if (!keepLock) {
2448
- await page
2449
- .evaluate(() => {
2450
- window.__wgCameraLock = false;
2451
- })
2452
- .catch(() => {});
2152
+ await new Promise((res) => setTimeout(res, travel));
2453
2153
  }
2454
2154
  return { x, y };
2455
2155
  }
2456
2156
 
2457
- /** Live viewport center of a locator (after camera transform) - no scroll. */
2458
- async function liveCenterOf(locator) {
2459
- return locator
2460
- .evaluate((el) => {
2461
- const r = el.getBoundingClientRect();
2462
- return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: r.width, h: r.height };
2463
- })
2464
- .catch(() => null);
2465
- }
2466
-
2467
2157
  async function clickPulseAt(page, x, y, tone) {
2468
2158
  await page
2469
2159
  .evaluate(({ x, y, tone }) => {
@@ -2588,47 +2278,17 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2588
2278
  proto.click = async function (options) {
2589
2279
  let clickPoint = null;
2590
2280
  let clickTone = "auto";
2591
- let keptCameraLock = false;
2592
2281
  try {
2593
2282
  await installOverlay(page);
2283
+ // Wait until the target is visible BEFORE measuring - otherwise
2284
+ // defineNavBlock({ click }) (and any slow-to-appear control) skips
2285
+ // the cursor entirely: boundingBox was null, then originalClick
2286
+ // waited and clicked with no demo animation.
2594
2287
  await this.waitFor({
2595
2288
  state: "visible",
2596
2289
  timeout: (options && options.timeout) || 30000,
2597
2290
  }).catch(() => {});
2598
- const zoomed = await page
2599
- .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
2600
- .catch(() => false);
2601
- if (zoomed) {
2602
- // Re-auto-center on this target, then lock - no host mouse, no scroll.
2603
- await page
2604
- .evaluate(() => {
2605
- window.__wgCameraLock = true;
2606
- window.__wgIgnoreHostMouse = true;
2607
- })
2608
- .catch(() => {});
2609
- keptCameraLock = true;
2610
- const handle = await this.elementHandle().catch(() => null);
2611
- if (handle) {
2612
- await page
2613
- .evaluate(
2614
- (el) => {
2615
- if (!el || !window.__wgSetCamera || !window.__wgElDocPoint) return;
2616
- const cam = window.__wgCamera || { scale: 1.4 };
2617
- const pt = window.__wgElDocPoint(el);
2618
- const s = cam.scale > 1.001 ? cam.scale : 1.4;
2619
- window.__wgSetCamera(pt.x, pt.y, s, 280);
2620
- },
2621
- handle,
2622
- )
2623
- .catch(() => {});
2624
- await handle.dispose().catch(() => {});
2625
- await new Promise((res) => setTimeout(res, 300));
2626
- }
2627
- }
2628
- const live = await liveCenterOf(this);
2629
- const box = live
2630
- ? { x: live.x - live.w / 2, y: live.y - live.h / 2, width: live.w, height: live.h }
2631
- : await this.boundingBox();
2291
+ const box = await this.boundingBox();
2632
2292
  const narrated = box ? await wasJustNarrated(page) : false;
2633
2293
  if (box && !narrated) {
2634
2294
  let fallback = "click";
@@ -2636,8 +2296,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2636
2296
  const text = (await this.textContent())?.trim();
2637
2297
  if (text && text.length > 0 && text.length <= 30) fallback = text;
2638
2298
  } catch {
2639
- /* ignore */
2299
+ // element has no simple text (an icon button, say) - generic label is fine
2640
2300
  }
2301
+ // Prefer "nav: …" when this locator is a NavBlock click target
2302
+ // (set on page by runStepMode just before act).
2641
2303
  try {
2642
2304
  const navLabel = await page.evaluate(() => window.__wgPendingNavClickLabel || null);
2643
2305
  if (navLabel) fallback = String(navLabel);
@@ -2647,80 +2309,39 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2647
2309
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
2648
2310
  const cap = await captionForLocator(page, this, stubs, fallback);
2649
2311
  clickTone = cap.tone;
2650
- const travel = zoomed ? cursorMs(200) : cursorMs(600);
2651
- const again = (await liveCenterOf(this)) || live;
2652
- const aim = again
2653
- ? { x: again.x - again.w / 2, y: again.y - again.h / 2, width: again.w, height: again.h }
2654
- : box;
2655
- clickPoint = await moveCursorTo(page, aim, travel, { keepLock: zoomed });
2656
- const liveBox = aim;
2657
- clickPoint = {
2658
- x: liveBox.x + liveBox.width / 2,
2659
- y: liveBox.y + liveBox.height / 2,
2660
- };
2661
- await showRing(page, liveBox, cap.label, cap.tone, {
2312
+ clickPoint = await moveCursorTo(page, box, cursorMs(600));
2313
+ await showRing(page, box, cap.label, cap.tone, {
2662
2314
  size: cap.size,
2663
2315
  weight: cap.weight,
2664
2316
  });
2665
2317
  await dwellMatchedStub(page, this, stubs, pacing);
2318
+ // "pop for a few seconds" - Dan's own phrase, matching the
2319
+ // zsign demo-engine's ring-before-click pattern in
2320
+ // services/help-center-clip-engine's video-pipeline.
2666
2321
  await new Promise((res) => setTimeout(res, clickPrePop()));
2667
2322
  } else if (box) {
2668
- const travel = zoomed ? cursorMs(120) : cursorMs(200);
2669
- clickPoint = await moveCursorTo(page, box, travel, { keepLock: zoomed });
2323
+ // Ring/caption already handled by narrate() - still move the
2324
+ // cursor + pulse the click point, just skip re-showing the ring.
2325
+ clickPoint = await moveCursorTo(page, box, cursorMs(200));
2670
2326
  clickTone = "planned";
2671
2327
  }
2672
2328
  } catch {
2673
- /* best-effort */
2329
+ // best-effort - the real click below still runs either way
2674
2330
  }
2675
2331
  if (clickPoint) {
2676
2332
  await clickPulseAt(page, clickPoint.x, clickPoint.y, clickTone);
2333
+ // clickPulseAt only triggers the CSS animation class - it doesn't
2334
+ // wait for it. Without a pause here, the real click (and any
2335
+ // resulting navigation/DOM change) fires while the ripple is still
2336
+ // mid-animation, sometimes cutting it off before it's even visible.
2337
+ // Wait out the same .5s the "@keyframes wg-pulse" rule uses, so the
2338
+ // mock click visually completes before the real one fires.
2677
2339
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 500));
2678
2340
  }
2679
- const zoomedClick = await page
2680
- .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
2681
- .catch(() => false);
2682
- try {
2683
- let result;
2684
- if (zoomedClick || keptCameraLock) {
2685
- // Real Playwright mouse at live visual center - never scrollIntoView.
2686
- const pt =
2687
- clickPoint ||
2688
- (await liveCenterOf(this)) ||
2689
- null;
2690
- if (pt && Number.isFinite(pt.x) && Number.isFinite(pt.y)) {
2691
- await page.mouse.click(pt.x, pt.y);
2692
- result = undefined;
2693
- } else {
2694
- const handle = await this.elementHandle().catch(() => null);
2695
- if (handle) {
2696
- await handle.evaluate((el) => {
2697
- if (el && typeof el.click === "function") el.click();
2698
- });
2699
- await handle.dispose().catch(() => {});
2700
- result = undefined;
2701
- } else {
2702
- result = await originalClick.call(this, {
2703
- ...(options || {}),
2704
- force: true,
2705
- timeout: (options && options.timeout) || 5000,
2706
- });
2707
- }
2708
- }
2709
- } else {
2710
- result = await originalClick.call(this, options);
2711
- }
2712
- await new Promise((res) => setTimeout(res, clickPostPop()));
2713
- await hideRing(page);
2714
- return result;
2715
- } finally {
2716
- if (keptCameraLock) {
2717
- await page
2718
- .evaluate(() => {
2719
- window.__wgCameraLock = false;
2720
- })
2721
- .catch(() => {});
2722
- }
2723
- }
2341
+ const result = await originalClick.call(this, options);
2342
+ await new Promise((res) => setTimeout(res, clickPostPop()));
2343
+ await hideRing(page);
2344
+ return result;
2724
2345
  };
2725
2346
  }
2726
2347
 
@@ -3034,9 +2655,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3034
2655
  const stubBeforeTodos = stubBeforePhase.todos || [];
3035
2656
  const isNavBlock = r.block.__waygraphKind === "nav";
3036
2657
  const autoNow = await currentAutoplay();
3037
- // Video / --mini: compact "Ep N · step · block" pill only.
3038
- // Otherwise omit (undefined) so Hide/localStorage is honored - never pass false.
3039
- // WAYGRAPH_STEPPER_MINI=1 remains a compat env alias for WAYGRAPH_MINI.
2658
+ // Video / --mini: compact pill. Never pass false - Hide/localStorage wins.
3040
2659
  const forceCollapsed =
3041
2660
  !!process.env.WAYGRAPH_VIDEO ||
3042
2661
  process.env.WAYGRAPH_MINI === "1" ||
@@ -3086,7 +2705,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3086
2705
  todos: stubBeforeTodos,
3087
2706
  ...paceSpeak,
3088
2707
  });
3089
- await setDemoTodos(page, stubBeforeTodos);
3090
2708
  const edits = await gate();
3091
2709
  for (const k of requires) {
3092
2710
  if (edits[k.name] !== undefined) {
@@ -3098,40 +2716,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3098
2716
  }
3099
2717
  }
3100
2718
  const recordingVideo = !!process.env.WAYGRAPH_VIDEO;
3101
- // Sequential stubBefore queue (appear -> dwell -> fade) before act.
3102
- if (stubBeforeRef.current && stubBeforeRef.current.length) {
3103
- await setDemoBannerTitle(page, stubBeforePhase.title);
3104
- const beforeRings = stubBeforeRef.current.map((h) => {
3105
- const styled = applyHighlightStyleDefaults(h, flowStyle);
3106
- return {
3107
- selector: styled.selector,
3108
- label: formatHighlightCaption(styled),
3109
- duration: styled.duration,
3110
- fastMode: styled.fastMode,
3111
- tone: styled.tone,
3112
- size: styled.size,
3113
- weight: styled.weight,
3114
- zoom: styled.zoom,
3115
- focus: styled.focus,
3116
- color: styled.color,
3117
- cursor: styled.cursor,
3118
- zoomOut: styled.zoomOut,
3119
- };
3120
- });
3121
- await cycleHighlightRings(page, beforeRings, !!pacing.gatesFast, {
3122
- pace: pacing.demoPace,
3123
- todos: stubBeforeTodos,
3124
- stepLabel: miniStepLabel({
3125
- index: i,
3126
- total: resolved.length,
3127
- blockName: r.block.name,
3128
- episodeNumber: r.episodeNumber,
3129
- paceBadge: paceSpeak.paceBadge,
3130
- }),
3131
- });
3132
- } else if (stubBeforePhase && stubBeforePhase.title) {
3133
- await setDemoBannerTitle(page, stubBeforePhase.title);
3134
- }
3135
2719
  await markStepRunning(page, {});
3136
2720
  // NavBlock click-nav: label the upcoming Locator.click demo cursor as
3137
2721
  // "nav: <block>" so pia/demo watchers see cursor+pulse on click nav
@@ -3181,7 +2765,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3181
2765
  fixtures: fixturesOnError,
3182
2766
  error: err,
3183
2767
  });
3184
- await setDemoBannerTitle(page, errPhase.title);
3185
2768
  const errHighlights = errPhase.highlights.map((h) => {
3186
2769
  const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
3187
2770
  return {
@@ -3193,10 +2776,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3193
2776
  size: styled.size,
3194
2777
  weight: styled.weight,
3195
2778
  zoom: styled.zoom,
3196
- focus: styled.focus,
3197
- color: styled.color,
3198
- cursor: styled.cursor,
3199
- zoomOut: styled.zoomOut,
3200
2779
  };
3201
2780
  });
3202
2781
  await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
@@ -3223,7 +2802,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3223
2802
  episodeTitle: r.episodeTitle,
3224
2803
  expectedFailureReason: r.expectedFailureReason,
3225
2804
  stepperMode,
3226
- // Omit forceCollapsed on errors so Hide preference is not stomped.
2805
+ // Omit forceCollapsed on errors so Hide is not stomped.
3227
2806
  });
3228
2807
  const errEdits = await gate();
3229
2808
  if (errEdits && errEdits.__wgRetry) {
@@ -3275,8 +2854,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3275
2854
  fixtures: fixturesAfter,
3276
2855
  });
3277
2856
  stubAfterTodos = afterPhase.todos || [];
3278
- await setDemoBannerTitle(page, afterPhase.title);
3279
- await setDemoTodos(page, afterPhase.todos || []);
3280
2857
  highlights = afterPhase.highlights.map((h) => {
3281
2858
  const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
3282
2859
  return {
@@ -3288,10 +2865,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3288
2865
  size: styled.size,
3289
2866
  weight: styled.weight,
3290
2867
  zoom: styled.zoom,
3291
- focus: styled.focus,
3292
- color: styled.color,
3293
- cursor: styled.cursor,
3294
- zoomOut: styled.zoomOut,
3295
2868
  };
3296
2869
  });
3297
2870
  } else {
@@ -4674,7 +4247,7 @@ Primary (less is more):
4674
4247
  --auto-next Auto-advance steps (alias: --autoplay)
4675
4248
  --fast Shorter auto-next / Next gates (keeps smooth cursor)
4676
4249
  --full Classic wrap-all block chips (default: carousel)
4677
- --mini Force compact mini panel (Hide pill); alias --stepper-mini
4250
+ --mini Force compact mini panel (Hide pill + Next); alias --stepper-mini
4678
4251
  --ff-expand Expand fastForwardComposeBlock inners as separate steps
4679
4252
  --ff-disabled Dispute: expand FF (alias --no-ff); blitz kept on those inners
4680
4253
  --auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless by default