waygraph 0.12.41 → 0.12.43

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
@@ -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) => {
@@ -1046,36 +858,127 @@ async function installOverlay(page, title) {
1046
858
  document.head.appendChild(iconLink);
1047
859
  }
1048
860
  if (iconLink.href !== favicon) iconLink.href = favicon;
861
+ // Blind-agent overlay beacons: every modal root gets data-wg-ui /
862
+ // data-wg-modal / data-wg-ready so tests can fail on a blank panel.
863
+ window.__wgStampModal = (el, kind, meta) => {
864
+ if (!el) return;
865
+ meta = meta || {};
866
+ el.setAttribute("data-wg-ui", "1");
867
+ el.setAttribute("data-wg-modal", kind || "panel");
868
+ if (meta.phase != null) el.setAttribute("data-wg-phase", String(meta.phase));
869
+ if (meta.step != null) el.setAttribute("data-wg-step", String(meta.step));
870
+ if (meta.total != null) el.setAttribute("data-wg-total", String(meta.total));
871
+ if (meta.block != null) el.setAttribute("data-wg-block", String(meta.block));
872
+ el.setAttribute(
873
+ "data-wg-collapsed",
874
+ el.classList.contains("wg-collapsed") ? "1" : "0",
875
+ );
876
+ el.setAttribute("data-wg-ready", meta.ready === false ? "0" : "1");
877
+ };
878
+ window.__wgOverlayBeacon = () => {
879
+ return Array.from(document.querySelectorAll("[data-wg-ui=\\"1\\"]")).map((el) => {
880
+ const r = el.getBoundingClientRect();
881
+ const st = getComputedStyle(el);
882
+ return {
883
+ id: el.id || null,
884
+ modal: el.getAttribute("data-wg-modal"),
885
+ ready: el.getAttribute("data-wg-ready") === "1",
886
+ phase: el.getAttribute("data-wg-phase"),
887
+ step: el.getAttribute("data-wg-step"),
888
+ block: el.getAttribute("data-wg-block"),
889
+ collapsed:
890
+ el.getAttribute("data-wg-collapsed") === "1" ||
891
+ el.classList.contains("wg-collapsed"),
892
+ opacity: st.opacity,
893
+ textLen: (el.innerText || "").trim().length,
894
+ w: Math.round(r.width),
895
+ h: Math.round(r.height),
896
+ visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
897
+ };
898
+ });
899
+ };
900
+ const existingBanner = document.getElementById("wg-banner");
901
+ if (existingBanner) window.__wgStampModal(existingBanner, "banner", { ready: true });
1049
902
  // 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.
903
+ // Collapsed = "N / M · block" + Next (when not auto). Never pass
904
+ // forceCollapsed:false - omit so Hide/localStorage wins.
1053
905
  window.__wgWirePanelChrome = (panel, storageKey, chromeTitle, opts) => {
1054
906
  if (!panel) return;
1055
907
  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
- }
908
+ const ensureChrome = () => {
909
+ let chrome = panel.querySelector(":scope > .wg-chrome");
910
+ if (!chrome) {
911
+ const body = document.createElement("div");
912
+ body.className = "wg-body";
913
+ while (panel.firstChild) body.appendChild(panel.firstChild);
914
+ chrome = document.createElement("div");
915
+ chrome.className = "wg-chrome";
916
+ const titleEl = document.createElement("span");
917
+ titleEl.className = "wg-chrome-title";
918
+ titleEl.textContent = chromeTitle || "waygraph demo";
919
+ const actions = document.createElement("div");
920
+ actions.className = "wg-chrome-actions";
921
+ const nextBtn = document.createElement("button");
922
+ nextBtn.type = "button";
923
+ nextBtn.className = "wg-mini-next";
924
+ nextBtn.setAttribute("data-wg-mini-next", "1");
925
+ nextBtn.textContent = "Next \u25B6";
926
+ const btn = document.createElement("button");
927
+ btn.type = "button";
928
+ btn.className = "wg-hide-btn";
929
+ btn.setAttribute("data-wg-toggle", "1");
930
+ btn.textContent = "Hide";
931
+ actions.appendChild(nextBtn);
932
+ actions.appendChild(btn);
933
+ chrome.appendChild(titleEl);
934
+ chrome.appendChild(actions);
935
+ panel.appendChild(chrome);
936
+ panel.appendChild(body);
937
+ return;
938
+ }
939
+ // Yap slides ship a bare chrome - ensure actions + mini Next exist.
940
+ let actions = chrome.querySelector(".wg-chrome-actions");
941
+ if (!actions) {
942
+ actions = document.createElement("div");
943
+ actions.className = "wg-chrome-actions";
944
+ const hide = chrome.querySelector("[data-wg-toggle]");
945
+ if (hide) actions.appendChild(hide);
946
+ chrome.appendChild(actions);
947
+ }
948
+ if (!actions.querySelector("[data-wg-mini-next]")) {
949
+ const nextBtn = document.createElement("button");
950
+ nextBtn.type = "button";
951
+ nextBtn.className = "wg-mini-next";
952
+ nextBtn.setAttribute("data-wg-mini-next", "1");
953
+ nextBtn.textContent = "Next \u25B6";
954
+ actions.insertBefore(nextBtn, actions.firstChild);
955
+ }
956
+ };
957
+ ensureChrome();
1075
958
  const stepLabel = opts.stepLabel || panel.dataset.wgStepLabel || "";
1076
959
  if (stepLabel) panel.dataset.wgStepLabel = stepLabel;
960
+ const syncMiniNext = (hidden) => {
961
+ const miniNext = panel.querySelector("[data-wg-mini-next]");
962
+ if (!miniNext) return;
963
+ let auto = false;
964
+ try {
965
+ auto = localStorage.getItem("wg-autoplay") === "1";
966
+ } catch {
967
+ /* ignore */
968
+ }
969
+ const runBtn = document.getElementById("wg-run");
970
+ const show = !!hidden && !auto && !!runBtn;
971
+ miniNext.classList.toggle("wg-mini-next-show", show);
972
+ if (runBtn) {
973
+ miniNext.textContent = runBtn.textContent || "Next \u25B6";
974
+ miniNext.disabled = !!runBtn.disabled;
975
+ } else {
976
+ miniNext.disabled = true;
977
+ }
978
+ };
1077
979
  const apply = (hidden, persist) => {
1078
980
  panel.classList.toggle("wg-collapsed", hidden);
981
+ panel.setAttribute("data-wg-collapsed", hidden ? "1" : "0");
1079
982
  const t = panel.querySelector("[data-wg-toggle]");
1080
983
  const titleEl = panel.querySelector(".wg-chrome-title");
1081
984
  const label = panel.dataset.wgStepLabel || stepLabel;
@@ -1085,6 +988,7 @@ async function installOverlay(page, title) {
1085
988
  : (chromeTitle || "waygraph demo");
1086
989
  }
1087
990
  if (t) t.textContent = hidden ? "Show" : "Hide";
991
+ syncMiniNext(hidden);
1088
992
  if (persist !== false) {
1089
993
  try {
1090
994
  localStorage.setItem(storageKey, hidden ? "1" : "0");
@@ -1113,6 +1017,26 @@ async function installOverlay(page, title) {
1113
1017
  apply(!panel.classList.contains("wg-collapsed"), true);
1114
1018
  });
1115
1019
  }
1020
+ const miniNext = panel.querySelector("[data-wg-mini-next]");
1021
+ if (miniNext && !miniNext.dataset.wgWired) {
1022
+ miniNext.dataset.wgWired = "1";
1023
+ miniNext.addEventListener("click", (e) => {
1024
+ e.stopPropagation();
1025
+ const runBtn = document.getElementById("wg-run");
1026
+ if (runBtn && !runBtn.disabled) {
1027
+ runBtn.click();
1028
+ return;
1029
+ }
1030
+ if (typeof window.__wgNext === "function") window.__wgNext({});
1031
+ });
1032
+ }
1033
+ const autoCb = document.getElementById("wg-autoplay-cb");
1034
+ if (autoCb && !autoCb.dataset.wgMiniWired) {
1035
+ autoCb.dataset.wgMiniWired = "1";
1036
+ autoCb.addEventListener("change", () => {
1037
+ syncMiniNext(panel.classList.contains("wg-collapsed"));
1038
+ });
1039
+ }
1116
1040
  const cur = panel.querySelector("#wg-modules .wg-mod-current");
1117
1041
  if (cur && typeof cur.scrollIntoView === "function") {
1118
1042
  try {
@@ -1145,7 +1069,7 @@ async function renderBeforeStep(page, info) {
1145
1069
  if (window.__wgHideCursor) window.__wgHideCursor();
1146
1070
  })
1147
1071
  .catch(() => {});
1148
- // miniStepLabel is Node-only — compute before evaluate (browser has no closure).
1072
+ // miniStepLabel / todosHtmlFromInfo are Node-only — browser evaluate has no closure.
1149
1073
  const payload = { ...info, stepLabel: miniStepLabel(info) };
1150
1074
  await page
1151
1075
  .evaluate((info) => {
@@ -1169,7 +1093,7 @@ async function renderBeforeStep(page, info) {
1169
1093
  // active, the episode 2 tab is inactive"). Absent entirely for an ad
1170
1094
  // hoc block chain with no named Flows.
1171
1095
  const episodesHtml = info.allEpisodes && info.allEpisodes.length > 0
1172
- ? "<div id=\\\"wg-episodes\\\">" +
1096
+ ? "<div id=\\"wg-episodes\\">" +
1173
1097
  info.allEpisodes
1174
1098
  .map((e) => {
1175
1099
  const cls = e.episodeNumber < info.episodeNumber ? "wg-ep-done"
@@ -1180,7 +1104,7 @@ async function renderBeforeStep(page, info) {
1180
1104
  const enteredCls = info.justEnteredEpisode && e.episodeNumber === info.episodeNumber
1181
1105
  ? " wg-ep-entered"
1182
1106
  : "";
1183
- return "<span class=\\\"wg-ep-tab " + cls + enteredCls + "\\\">Episode " + e.episodeNumber + ": " +
1107
+ return "<span class=\\"wg-ep-tab " + cls + enteredCls + "\\">Episode " + e.episodeNumber + ": " +
1184
1108
  esc(e.episodeTitle || "") + "</span>";
1185
1109
  })
1186
1110
  .join("") +
@@ -1196,66 +1120,67 @@ async function renderBeforeStep(page, info) {
1196
1120
  .map((name, idx) => {
1197
1121
  const cls = idx < info.moduleIndex ? "wg-mod-done" : idx === info.moduleIndex ? "wg-mod-current" : "wg-mod-upcoming";
1198
1122
  const desc = info.allDescriptions && info.allDescriptions[idx];
1199
- const titleAttr = desc ? " title=\\\"" + esc(desc) + "\\\"" : "";
1200
- return "<span class=\\\"wg-mod " + cls + "\\\"" + titleAttr + ">" + name + "</span>";
1123
+ const titleAttr = desc ? " title=\\"" + esc(desc) + "\\"" : "";
1124
+ return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
1201
1125
  })
1202
1126
  .join("");
1203
1127
  const narrationHtml = info.description
1204
- ? "<div class=\\\"wg-narration\\\">" + esc(info.description) + "</div>"
1128
+ ? "<div class=\\"wg-narration\\">" + esc(info.description) + "</div>"
1205
1129
  : "";
1206
1130
  const paceHtml =
1207
1131
  info.paceBadge || info.paceLabel
1208
- ? "<div class=\\\"wg-pace\\\" data-pace-kind=\\\"" +
1132
+ ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
1209
1133
  esc(info.paceKind || "normal") +
1210
- "\\\"><span class=\\\"wg-pace-badge\\\">" +
1134
+ "\\"><span class=\\"wg-pace-badge\\">" +
1211
1135
  esc(info.paceBadge || "1x") +
1212
1136
  "</span><span>" +
1213
1137
  esc(info.paceLabel || "") +
1214
1138
  "</span></div>"
1215
1139
  : "";
1140
+ const todosHtml = (() => {
1141
+ const list = info.todos || [];
1142
+ if (!list.length) return "";
1143
+ return (
1144
+ '<ul id=\\"wg-todos\\">' +
1145
+ list
1146
+ .map((t) => {
1147
+ const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
1148
+ const mark = t.done
1149
+ ? String.fromCharCode(0x2713)
1150
+ : t.current
1151
+ ? String.fromCharCode(0x2192)
1152
+ : String.fromCharCode(0x25cb);
1153
+ return (
1154
+ '<li class=\\"' +
1155
+ cls +
1156
+ '\\"><span class=\\"wg-todo-mark\\">' +
1157
+ mark +
1158
+ '</span><span>' +
1159
+ esc(t.text || "") +
1160
+ "</span></li>"
1161
+ );
1162
+ })
1163
+ .join("") +
1164
+ "</ul>"
1165
+ );
1166
+ })();
1216
1167
  let html =
1217
- "<div id=\\\"wg-progress\\\"><div id=\\\"wg-progress-bar\\\" style=\\\"width:" + pct + "%\\\"></div></div>" +
1168
+ "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
1218
1169
  episodesHtml +
1219
1170
  paceHtml +
1220
- "<div id=\\\"wg-modules\\\" class=\\\"" + modulesClass + "\\\">" + modulesHtml + "</div>" +
1171
+ "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1221
1172
  "<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>" +
1222
1173
  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
- })());
1174
+ todosHtml;
1250
1175
  if (info.keys.length === 0) {
1251
- html += "<div class=\\\"wg-key\\\">(no MemKeys required)</div>";
1176
+ html += "<div class=\\"wg-key\\">(no MemKeys required)</div>";
1252
1177
  }
1253
1178
  for (const k of info.keys) {
1254
1179
  html +=
1255
- "<div class=\\\"wg-key\\\"><label>" + k.name + "</label>" +
1256
- "<textarea data-key=\\\"" + k.name + "\\\" rows=\\\"2\\\">" +
1180
+ "<div class=\\"wg-key\\"><label>" + k.name + "</label>" +
1181
+ "<textarea data-key=\\"" + k.name + "\\" rows=\\"2\\">" +
1257
1182
  k.value.replace(/</g, "&lt;") + "</textarea>" +
1258
- "<div class=\\\"wg-key-pretty\\\" data-pretty-for=\\\"" + esc(k.name) + "\\\"></div>" +
1183
+ "<div class=\\"wg-key-pretty\\" data-pretty-for=\\"" + esc(k.name) + "\\"></div>" +
1259
1184
  "</div>";
1260
1185
  }
1261
1186
  let autoNow = false;
@@ -1265,17 +1190,19 @@ async function renderBeforeStep(page, info) {
1265
1190
  /* private mode / blocked storage - defaults to manual */
1266
1191
  }
1267
1192
  html +=
1268
- "<div class=\\\"wg-autoplay-row\\\"><label><input type=\\\"checkbox\\\" id=\\\"wg-autoplay-cb\\\"" +
1193
+ "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
1269
1194
  (autoNow ? " checked" : "") +
1270
1195
  "> 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\\\"") +
1196
+ "<div id=\\"wg-gate-manual\\"" + (autoNow ? " style=\\"display:none\\"" : "") +
1197
+ "><button id=\\"wg-run\\">Run this step \\u25B6</button></div>" +
1198
+ "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" + (autoNow ? "" : " style=\\"display:none\\"") +
1274
1199
  ">Auto-advancing...</div>";
1275
1200
  panel.innerHTML = html;
1276
1201
  if (isNewPanel) {
1277
1202
  document.documentElement.appendChild(panel);
1278
1203
  requestAnimationFrame(() => panel.classList.add("wg-in"));
1204
+ } else {
1205
+ panel.classList.add("wg-in");
1279
1206
  }
1280
1207
  if (window.__wgWirePanelChrome) {
1281
1208
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
@@ -1283,6 +1210,15 @@ async function renderBeforeStep(page, info) {
1283
1210
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
1284
1211
  });
1285
1212
  }
1213
+ if (window.__wgStampModal) {
1214
+ window.__wgStampModal(panel, "panel", {
1215
+ phase: "before",
1216
+ step: info.index + 1,
1217
+ total: info.total,
1218
+ block: info.blockName || "",
1219
+ ready: true,
1220
+ });
1221
+ }
1286
1222
  // Live, human-readable preview of what a MemKey's raw JSON will
1287
1223
  // actually write - object fields become "Field: value" lines
1288
1224
  // (camelCase split the same way state tags already are); a
@@ -1303,7 +1239,7 @@ async function renderBeforeStep(page, info) {
1303
1239
  return Object.entries(parsed)
1304
1240
  .map(([field, value]) => {
1305
1241
  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\\\">" +
1242
+ return "<div class=\\"wg-key-pretty-row\\"><span class=\\"wg-key-pretty-label\\">" +
1307
1243
  esc(label) + ":</span> " + esc(String(value)) + "</div>";
1308
1244
  })
1309
1245
  .join("");
@@ -1312,7 +1248,7 @@ async function renderBeforeStep(page, info) {
1312
1248
  const prettyOn = window.__wgPretty !== false;
1313
1249
  panel.querySelectorAll("textarea[data-key]").forEach((ta) => {
1314
1250
  const key = ta.getAttribute("data-key");
1315
- const previewEl = panel.querySelector(".wg-key-pretty[data-pretty-for=\\\"" + key + "\\\"]");
1251
+ const previewEl = panel.querySelector(".wg-key-pretty[data-pretty-for=\\"" + key + "\\"]");
1316
1252
  if (!previewEl) return;
1317
1253
  previewEl.style.display = prettyOn ? "" : "none";
1318
1254
  if (prettyOn) previewEl.innerHTML = prettyMemValue(ta.value);
@@ -1354,29 +1290,42 @@ async function renderBeforeStep(page, info) {
1354
1290
  }
1355
1291
  }, payload)
1356
1292
  .catch(() => {});
1293
+ if (process.env.WAYGRAPH_PROVE_SHOT || process.env.WAYGRAPH_PROVE_EXIT === "1") {
1294
+ // Default: prove once on first painted panel. PROVE_EVERY=1 re-checks
1295
+ // every step (noisy; useful when hunting a mid-chain blank).
1296
+ const proveEvery = process.env.WAYGRAPH_PROVE_EVERY === "1";
1297
+ const already = globalThis.__wgProveDone;
1298
+ if (!already || proveEvery || process.env.WAYGRAPH_PROVE_EXIT === "1") {
1299
+ await page.waitForTimeout(500).catch(() => {});
1300
+ if (process.env.WAYGRAPH_PROVE_SHOT) {
1301
+ await page.screenshot({ path: process.env.WAYGRAPH_PROVE_SHOT }).catch(() => {});
1302
+ }
1303
+ const ok = await page
1304
+ .evaluate(() => {
1305
+ const beacons =
1306
+ typeof window.__wgOverlayBeacon === "function" ? window.__wgOverlayBeacon() : [];
1307
+ const panel = beacons.find((b) => b.modal === "panel" && b.ready && b.visible);
1308
+ const textOk = panel && panel.textLen >= 12;
1309
+ const opacityOk = panel && Number(panel.opacity) > 0.5;
1310
+ return {
1311
+ ok: !!(panel && textOk && opacityOk),
1312
+ beacons,
1313
+ hasRun: !!document.getElementById("wg-run"),
1314
+ readyAttr: document.getElementById("wg-panel")?.getAttribute("data-wg-ready") || null,
1315
+ };
1316
+ })
1317
+ .catch((e) => ({ ok: false, reason: String(e && e.message ? e.message : e) }));
1318
+ console.error("WAYGRAPH_PROVE " + JSON.stringify(ok));
1319
+ globalThis.__wgProveDone = true;
1320
+ if (process.env.WAYGRAPH_PROVE_EXIT === "1") {
1321
+ process.exit(ok && ok.ok ? 0 : 2);
1322
+ }
1323
+ }
1324
+ }
1357
1325
  }
1358
1326
 
1359
1327
  async function ensureSelectorInView(page, selector) {
1360
1328
  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
1329
  const loc = page.locator(selector).first();
1381
1330
  await loc.scrollIntoViewIfNeeded().catch(() => {});
1382
1331
  await page
@@ -1443,54 +1392,48 @@ async function ensureSelectorInView(page, selector) {
1443
1392
  }
1444
1393
  }
1445
1394
 
1446
- async function applyCameraZoom(page, box, zoom, ms) {
1447
- const z = zoom != null && Number(zoom) > 1 ? Number(zoom) : 1;
1448
- if (!box || z <= 1) {
1395
+ async function applyHighlightZoom(page, selector, zoom) {
1396
+ const z = Number(zoom);
1397
+ if (!Number.isFinite(z) || z <= 1.001) {
1449
1398
  await page.evaluate(() => {
1450
- if (window.__wgResetCamera) window.__wgResetCamera();
1399
+ if (window.__wgClearZoom) window.__wgClearZoom();
1451
1400
  }).catch(() => {});
1452
1401
  return;
1453
1402
  }
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
1403
  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
- await page
1478
- .evaluate(() => {
1479
- if (window.__wgResetCamera) window.__wgResetCamera();
1480
- })
1404
+ .evaluate(
1405
+ ({ sel, scale }) => {
1406
+ if (window.__wgApplyZoom) window.__wgApplyZoom(sel, scale);
1407
+ },
1408
+ { sel: selector, scale: z },
1409
+ )
1481
1410
  .catch(() => {});
1482
1411
  }
1483
1412
 
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(() => {});
1413
+ function todosHtmlFromInfo(info, escFn) {
1414
+ const list = info.todos || [];
1415
+ if (!list.length) return "";
1416
+ return (
1417
+ "<ul id=\\"wg-todos\\">" +
1418
+ list
1419
+ .map((t) => {
1420
+ const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
1421
+ const mark = t.done ? "\\u2713" : t.current ? "\\u2192" : "\\u25CB";
1422
+ return (
1423
+ "<li class=\\"" +
1424
+ cls +
1425
+ "\\"><span class=\\"wg-todo-mark\\">" +
1426
+ mark +
1427
+ "</span><span>" +
1428
+ escFn(t.text || "") +
1429
+ "</span></li>"
1430
+ );
1431
+ })
1432
+ .join("") +
1433
+ "</ul>"
1434
+ );
1491
1435
  }
1492
1436
 
1493
- /** Collapsed chrome title: Ep N / i / n / block / pace. */
1494
1437
  function miniStepLabel(info) {
1495
1438
  const ep =
1496
1439
  info.episodeNumber !== undefined && info.episodeNumber !== null
@@ -1506,176 +1449,175 @@ function miniStepLabel(info) {
1506
1449
  );
1507
1450
  }
1508
1451
 
1509
- async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1510
- const list = highlights || [];
1511
- const defaultHoldMs = opts && opts.defaultHoldMs != null ? opts.defaultHoldMs : null;
1512
- 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 () => {
1452
+ /**
1453
+ * stubOnError rings + amber "expected outcome" / red error panel, then wait
1454
+ * for Stop/Retry. Used when a step throws OR when withExpectedFailure's last
1455
+ * block succeeds on the intentional fail branch (e.g. LoginPage + error banner).
1456
+ */
1457
+ async function presentFailPanel(page, opts) {
1458
+ const {
1459
+ block,
1460
+ fixtures,
1461
+ error,
1462
+ index,
1463
+ total,
1464
+ blockName,
1465
+ message,
1466
+ allNames,
1467
+ allDescriptions,
1468
+ moduleIndex,
1469
+ allEpisodes,
1470
+ title,
1471
+ episodeNumber,
1472
+ episodeTitle,
1473
+ expectedFailureReason,
1474
+ stepperMode,
1475
+ gatesFast,
1476
+ demoPace,
1477
+ gate,
1478
+ } = opts;
1479
+ if (hasAuthoredStubOnError(block, fixtures)) {
1554
1480
  await page
1555
1481
  .evaluate(() => {
1556
- if (window.__wgStopHighlightTrack) window.__wgStopHighlightTrack();
1557
- if (window.__wgRingRaf) {
1558
- cancelAnimationFrame(window.__wgRingRaf);
1559
- window.__wgRingRaf = null;
1560
- }
1561
1482
  if (window.__wgRingTrack) {
1562
- window.removeEventListener("mousemove", window.__wgRingTrack);
1563
1483
  window.removeEventListener("resize", window.__wgRingTrack);
1564
1484
  window.removeEventListener("scroll", window.__wgRingTrack, true);
1565
1485
  window.__wgRingTrack = null;
1566
1486
  }
1567
- if (window.__wgHideFocusVeil) window.__wgHideFocusVeil();
1568
1487
  })
1569
1488
  .catch(() => {});
1570
- };
1489
+ const errPhase = await runStubPhase(block, "stubOnError", {
1490
+ fixtures,
1491
+ error,
1492
+ out: opts.out,
1493
+ });
1494
+ const errHighlights = errPhase.highlights.map((h) => {
1495
+ const styled = applyHighlightStyleDefaults(h, opts.highlightStyle);
1496
+ return {
1497
+ selector: styled.selector,
1498
+ label: formatHighlightCaption(styled),
1499
+ duration: styled.duration,
1500
+ fastMode: styled.fastMode,
1501
+ tone: styled.tone,
1502
+ size: styled.size,
1503
+ weight: styled.weight,
1504
+ zoom: styled.zoom,
1505
+ };
1506
+ });
1507
+ await cycleHighlightRings(page, errHighlights, !!gatesFast, {
1508
+ defaultHoldMs: 2000,
1509
+ pace: demoPace,
1510
+ todos: errPhase.todos,
1511
+ });
1512
+ }
1513
+ await renderStepError(page, {
1514
+ index,
1515
+ total,
1516
+ blockName,
1517
+ message,
1518
+ allNames,
1519
+ allDescriptions,
1520
+ moduleIndex,
1521
+ allEpisodes,
1522
+ title,
1523
+ episodeNumber,
1524
+ episodeTitle,
1525
+ expectedFailureReason,
1526
+ stepperMode,
1527
+ });
1528
+ return gate();
1529
+ }
1571
1530
 
1531
+ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1532
+ const list = highlights || [];
1533
+ // defaultHoldMs: when set (fail path), use instead of legacy 900/200 so BUG
1534
+ // rings stay visible ~2s before the error panel (PIA stubOnError RFC).
1535
+ const defaultHoldMs = opts && opts.defaultHoldMs != null ? opts.defaultHoldMs : null;
1536
+ const pace = opts && opts.pace !== undefined ? opts.pace : undefined;
1572
1537
  for (let i = 0; i < list.length; i++) {
1573
1538
  const h = list[i];
1574
1539
  try {
1575
- await stopLiveFollow();
1576
1540
  await ensureSelectorInView(page, h.selector);
1541
+ await applyHighlightZoom(page, h.selector, h.zoom);
1577
1542
  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 });
1543
+ if (box) {
1544
+ await showRing(page, box, h.label, h.tone || "planned", {
1545
+ size: h.size,
1546
+ weight: h.weight,
1547
+ });
1548
+ if ((opts && opts.todos && opts.todos.length) || (h.todos && h.todos.length)) {
1549
+ const rows =
1550
+ opts && opts.todos && opts.todos.length
1551
+ ? opts.todos
1552
+ : normalizeTodos(h.todos, h.todoIndex);
1553
+ await page
1554
+ .evaluate((todos) => {
1555
+ const ul = document.getElementById("wg-todos");
1556
+ if (!ul) return;
1557
+ ul.innerHTML = todos
1558
+ .map((t) => {
1559
+ const cls = t.current
1560
+ ? "wg-todo-current"
1561
+ : t.done
1562
+ ? "wg-todo-done"
1563
+ : "wg-todo-pending";
1564
+ const mark = t.done ? "\\u2713" : t.current ? "\\u2192" : "\\u25CB";
1565
+ const esc = (s) =>
1566
+ String(s)
1567
+ .replace(/&/g, "&amp;")
1568
+ .replace(/</g, "&lt;")
1569
+ .replace(/>/g, "&gt;");
1570
+ return (
1571
+ "<li class=\\"" +
1572
+ cls +
1573
+ "\\"><span class=\\"wg-todo-mark\\">" +
1574
+ mark +
1575
+ "</span><span>" +
1576
+ esc(t.text || "") +
1577
+ "</span></li>"
1578
+ );
1579
+ })
1580
+ .join("");
1581
+ }, rows)
1582
+ .catch(() => {});
1640
1583
  }
1584
+ const authored = resolveFixtureDwellMs(h, { gatesFast, pace });
1585
+ const legacyMs = i === list.length - 1 ? 200 : 900;
1586
+ const holdMs =
1587
+ authored != null ? authored : defaultHoldMs != null ? defaultHoldMs : legacyMs;
1588
+ await new Promise((res) => setTimeout(res, holdMs));
1641
1589
  }
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
1590
  } catch {
1656
- /* best-effort */
1591
+ // best-effort - a selector that doesn't resolve just gets skipped
1657
1592
  }
1658
1593
  }
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).
1594
+ if (list.length > 0) {
1595
+ const last = list[list.length - 1];
1667
1596
  await page
1668
- .evaluate(() => {
1669
- window.__wgCameraLock = true;
1670
- })
1597
+ .evaluate((h) => {
1598
+ if (!window.__wgPositionRing) return;
1599
+ const reposition = () => {
1600
+ const el = document.querySelector(h.selector);
1601
+ if (!el) {
1602
+ if (window.__wgHideRing) window.__wgHideRing();
1603
+ return;
1604
+ }
1605
+ window.__wgPositionRing(el.getBoundingClientRect(), h.label, h.tone || "planned", {
1606
+ size: h.size || "md",
1607
+ weight: h.weight || "normal",
1608
+ });
1609
+ };
1610
+ reposition();
1611
+ window.__wgRingTrack = reposition;
1612
+ window.addEventListener("resize", reposition);
1613
+ window.addEventListener("scroll", reposition, true);
1614
+ }, last)
1671
1615
  .catch(() => {});
1672
- }
1673
- if (list.length === 0) {
1616
+ } else {
1674
1617
  await hideRing(page);
1675
1618
  }
1676
1619
  }
1677
1620
 
1678
-
1679
1621
  async function renderAfterStep(page, info) {
1680
1622
  await installOverlay(page, info.title);
1681
1623
  // Clear any tracker from a previous highlight before cycling through this
@@ -1721,8 +1663,8 @@ async function renderAfterStep(page, info) {
1721
1663
  .map((name, idx) => {
1722
1664
  const cls = idx <= info.moduleIndex ? "wg-mod-done" : "wg-mod-upcoming";
1723
1665
  const desc = info.allDescriptions && info.allDescriptions[idx];
1724
- const titleAttr = desc ? " title=\\\"" + escA(desc) + "\\\"" : "";
1725
- return "<span class=\\\"wg-mod " + cls + "\\\"" + titleAttr + ">" + name + "</span>";
1666
+ const titleAttr = desc ? " title=\\"" + escA(desc) + "\\"" : "";
1667
+ return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
1726
1668
  })
1727
1669
  .join("");
1728
1670
  // QA-friendly by default ("LoginSuccess" -> "Login Success") - raw
@@ -1733,13 +1675,13 @@ async function renderAfterStep(page, info) {
1733
1675
  .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") || info.resultTag;
1734
1676
  const pretty = window.__wgPretty !== false;
1735
1677
  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>" +
1678
+ "<div class=\\"wg-toggle\\">" +
1679
+ "<button class=\\"wg-toggle-btn" + (pretty ? " wg-active" : "") + "\\" data-mode=\\"pretty\\">Pretty</button>" +
1680
+ "<button class=\\"wg-toggle-btn" + (pretty ? "" : " wg-active") + "\\" data-mode=\\"json\\">JSON</button>" +
1739
1681
  "</div>" +
1740
- "<div class=\\\"wg-result wg-result-pretty\\\" style=\\\"display:" + (pretty ? "block" : "none") + "\\\">" +
1682
+ "<div class=\\"wg-result wg-result-pretty\\" style=\\"display:" + (pretty ? "block" : "none") + "\\">" +
1741
1683
  prettyText + "</div>" +
1742
- "<div class=\\\"wg-result wg-result-json\\\" style=\\\"display:" + (pretty ? "none" : "block") + "\\\">" +
1684
+ "<div class=\\"wg-result wg-result-json\\" style=\\"display:" + (pretty ? "none" : "block") + "\\">" +
1743
1685
  info.resultTag + "</div>";
1744
1686
  let autoNow = false;
1745
1687
  try {
@@ -1748,21 +1690,21 @@ async function renderAfterStep(page, info) {
1748
1690
  /* private mode / blocked storage - defaults to manual */
1749
1691
  }
1750
1692
  const gateHtml =
1751
- "<div class=\\\"wg-autoplay-row\\\"><label><input type=\\\"checkbox\\\" id=\\\"wg-autoplay-cb\\\"" +
1693
+ "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
1752
1694
  (autoNow ? " checked" : "") +
1753
1695
  "> 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\\\"") +
1696
+ "<div id=\\"wg-gate-manual\\"" + (autoNow ? " style=\\"display:none\\"" : "") +
1697
+ "><button id=\\"wg-run\\">" + buttonLabel + "</button></div>" +
1698
+ "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" + (autoNow ? "" : " style=\\"display:none\\"") +
1757
1699
  ">Auto-advancing...</div>";
1758
1700
  const episodesHtml = info.allEpisodes && info.allEpisodes.length > 0
1759
- ? "<div id=\\\"wg-episodes\\\">" +
1701
+ ? "<div id=\\"wg-episodes\\">" +
1760
1702
  info.allEpisodes
1761
1703
  .map((e) => {
1762
1704
  const cls = e.episodeNumber < info.episodeNumber ? "wg-ep-done"
1763
1705
  : e.episodeNumber === info.episodeNumber ? "wg-ep-current"
1764
1706
  : "wg-ep-upcoming";
1765
- return "<span class=\\\"wg-ep-tab " + cls + "\\\">Episode " + e.episodeNumber + ": " +
1707
+ return "<span class=\\"wg-ep-tab " + cls + "\\">Episode " + e.episodeNumber + ": " +
1766
1708
  escA(e.episodeTitle || "") + "</span>";
1767
1709
  })
1768
1710
  .join("") +
@@ -1770,52 +1712,55 @@ async function renderAfterStep(page, info) {
1770
1712
  : "";
1771
1713
  const paceHtml =
1772
1714
  info.paceBadge || info.paceLabel
1773
- ? "<div class=\\\"wg-pace\\\" data-pace-kind=\\\"" +
1715
+ ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
1774
1716
  escA(info.paceKind || "normal") +
1775
- "\\\"><span class=\\\"wg-pace-badge\\\">" +
1717
+ "\\"><span class=\\"wg-pace-badge\\">" +
1776
1718
  escA(info.paceBadge || "1x") +
1777
1719
  "</span><span>" +
1778
1720
  escA(info.paceLabel || "") +
1779
1721
  "</span></div>"
1780
1722
  : "";
1723
+ const todosHtml = (() => {
1724
+ const list = info.todos || [];
1725
+ if (!list.length) return "";
1726
+ return (
1727
+ '<ul id=\\"wg-todos\\">' +
1728
+ list
1729
+ .map((t) => {
1730
+ const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
1731
+ const mark = t.done
1732
+ ? String.fromCharCode(0x2713)
1733
+ : t.current
1734
+ ? String.fromCharCode(0x2192)
1735
+ : String.fromCharCode(0x25cb);
1736
+ return (
1737
+ '<li class=\\"' +
1738
+ cls +
1739
+ '\\"><span class=\\"wg-todo-mark\\">' +
1740
+ mark +
1741
+ '</span><span>' +
1742
+ escA(t.text || "") +
1743
+ "</span></li>"
1744
+ );
1745
+ })
1746
+ .join("") +
1747
+ "</ul>"
1748
+ );
1749
+ })();
1781
1750
  panel.innerHTML =
1782
- "<div id=\\\"wg-progress\\\"><div id=\\\"wg-progress-bar\\\" style=\\\"width:" + pct + "%\\\"></div></div>" +
1751
+ "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
1783
1752
  episodesHtml +
1784
1753
  paceHtml +
1785
- "<div id=\\\"wg-modules\\\" class=\\\"" + modulesClass + "\\\">" + modulesHtml + "</div>" +
1754
+ "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1786
1755
  "<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
- })()) +
1756
+ todosHtml +
1814
1757
  resultHtml +
1815
1758
  gateHtml;
1816
1759
  if (isNewPanel) {
1817
1760
  document.documentElement.appendChild(panel);
1818
1761
  requestAnimationFrame(() => panel.classList.add("wg-in"));
1762
+ } else {
1763
+ panel.classList.add("wg-in");
1819
1764
  }
1820
1765
  if (window.__wgWirePanelChrome) {
1821
1766
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
@@ -1823,6 +1768,15 @@ async function renderAfterStep(page, info) {
1823
1768
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
1824
1769
  });
1825
1770
  }
1771
+ if (window.__wgStampModal) {
1772
+ window.__wgStampModal(panel, "panel", {
1773
+ phase: "after",
1774
+ step: info.index + 1,
1775
+ total: info.total,
1776
+ block: info.blockName || "",
1777
+ ready: true,
1778
+ });
1779
+ }
1826
1780
  panel.querySelectorAll(".wg-toggle-btn").forEach((btn) => {
1827
1781
  btn.addEventListener("click", () => {
1828
1782
  const wantPretty = btn.getAttribute("data-mode") === "pretty";
@@ -1878,13 +1832,13 @@ async function renderStepError(page, info) {
1878
1832
  if (!isNewPanel) panel.classList.add("wg-in");
1879
1833
  const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1880
1834
  const episodesHtml = info.allEpisodes && info.allEpisodes.length > 0
1881
- ? "<div id=\\\"wg-episodes\\\">" +
1835
+ ? "<div id=\\"wg-episodes\\">" +
1882
1836
  info.allEpisodes
1883
1837
  .map((e) => {
1884
1838
  const cls = e.episodeNumber < info.episodeNumber ? "wg-ep-done"
1885
1839
  : e.episodeNumber === info.episodeNumber ? "wg-ep-current"
1886
1840
  : "wg-ep-upcoming";
1887
- return "<span class=\\\"wg-ep-tab " + cls + "\\\">Episode " + e.episodeNumber + ": " +
1841
+ return "<span class=\\"wg-ep-tab " + cls + "\\">Episode " + e.episodeNumber + ": " +
1888
1842
  esc(e.episodeTitle || "") + "</span>";
1889
1843
  })
1890
1844
  .join("") +
@@ -1895,8 +1849,8 @@ async function renderStepError(page, info) {
1895
1849
  .map((name, idx) => {
1896
1850
  const cls = idx === info.moduleIndex ? "wg-mod-current" : idx < info.moduleIndex ? "wg-mod-done" : "wg-mod-upcoming";
1897
1851
  const desc = info.allDescriptions && info.allDescriptions[idx];
1898
- const titleAttr = desc ? " title=\\\"" + esc(desc) + "\\\"" : "";
1899
- return "<span class=\\\"wg-mod " + cls + "\\\"" + titleAttr + ">" + name + "</span>";
1852
+ const titleAttr = desc ? " title=\\"" + esc(desc) + "\\"" : "";
1853
+ return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
1900
1854
  })
1901
1855
  .join("");
1902
1856
  const retryLabel = info.episodeNumber !== undefined ? "Retry Episode " + info.episodeNumber : "Retry";
@@ -1904,21 +1858,23 @@ async function renderStepError(page, info) {
1904
1858
  ? "Expected outcome - Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + " failed as intended"
1905
1859
  : "Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + " threw";
1906
1860
  const reasonHtml = isExpected
1907
- ? "<div class=\\\"wg-expected-reason\\\">" + esc(info.expectedFailureReason) + "</div>"
1861
+ ? "<div class=\\"wg-expected-reason\\">" + esc(info.expectedFailureReason) + "</div>"
1908
1862
  : "";
1909
1863
  panel.innerHTML =
1910
1864
  episodesHtml +
1911
- "<div id=\\\"wg-modules\\\" class=\\\"" + modulesClass + "\\\">" + modulesHtml + "</div>" +
1912
- "<h3 class=\\\"" + (isExpected ? "wg-expected-heading" : "wg-error-heading") + "\\\">" + headingText + "</h3>" +
1865
+ "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1866
+ "<h3 class=\\"" + (isExpected ? "wg-expected-heading" : "wg-error-heading") + "\\">" + headingText + "</h3>" +
1913
1867
  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>" +
1868
+ "<div class=\\"wg-error-msg\\">" + esc(info.message) + "</div>" +
1869
+ "<div class=\\"wg-error-actions\\">" +
1870
+ "<button id=\\"wg-error-retry\\" class=\\"wg-error-retry\\">" + esc(retryLabel) + "</button>" +
1871
+ "<button id=\\"wg-run\\" class=\\"wg-error-stop\\">Stop</button>" +
1918
1872
  "</div>";
1919
1873
  if (isNewPanel) {
1920
1874
  document.documentElement.appendChild(panel);
1921
1875
  requestAnimationFrame(() => panel.classList.add("wg-in"));
1876
+ } else {
1877
+ panel.classList.add("wg-in");
1922
1878
  }
1923
1879
  if (window.__wgWirePanelChrome) {
1924
1880
  window.__wgWirePanelChrome(panel, "wg-panel-hidden", "waygraph demo", {
@@ -1926,6 +1882,15 @@ async function renderStepError(page, info) {
1926
1882
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
1927
1883
  });
1928
1884
  }
1885
+ if (window.__wgStampModal) {
1886
+ window.__wgStampModal(panel, "panel", {
1887
+ phase: "error",
1888
+ step: info.index + 1,
1889
+ total: info.total,
1890
+ block: info.blockName || "",
1891
+ ready: true,
1892
+ });
1893
+ }
1929
1894
  const runBtn = document.getElementById("wg-run");
1930
1895
  if (runBtn) runBtn.addEventListener("click", () => window.__wgNext({}));
1931
1896
  const retryBtn = document.getElementById("wg-error-retry");
@@ -2041,11 +2006,24 @@ async function markStepRunning(page, _opts) {
2041
2006
  const panel = document.getElementById("wg-panel");
2042
2007
  if (panel) {
2043
2008
  panel.classList.add("wg-collapsed");
2009
+ panel.setAttribute("data-wg-collapsed", "1");
2010
+ panel.setAttribute("data-wg-phase", "running");
2011
+ if (typeof window.__wgStampModal === "function") {
2012
+ window.__wgStampModal(panel, "panel", {
2013
+ phase: "running",
2014
+ ready: true,
2015
+ });
2016
+ }
2044
2017
  const toggle = panel.querySelector("[data-wg-toggle]");
2045
2018
  if (toggle) toggle.textContent = "Show";
2046
2019
  const titleEl = panel.querySelector(".wg-chrome-title");
2047
2020
  const label = panel.dataset.wgStepLabel;
2048
2021
  if (titleEl && label) titleEl.textContent = label;
2022
+ const miniNext = panel.querySelector("[data-wg-mini-next]");
2023
+ if (miniNext) {
2024
+ miniNext.disabled = true;
2025
+ miniNext.classList.remove("wg-mini-next-show");
2026
+ }
2049
2027
  }
2050
2028
  })
2051
2029
  .catch(() => {});
@@ -2054,16 +2032,14 @@ async function markStepRunning(page, _opts) {
2054
2032
  async function showRing(page, box, label, tone, style) {
2055
2033
  const size = normalizeHighlightSize(style && style.size);
2056
2034
  const weight = normalizeHighlightWeight(style && style.weight);
2057
- const focus = !!(style && style.focus);
2058
- const color = (style && (style.color || style.labelColor || style.textColor)) || "";
2059
2035
  await page
2060
2036
  .evaluate(
2061
- ({ box, label, tone, size, weight, focus, color }) => {
2037
+ ({ box, label, tone, size, weight }) => {
2062
2038
  if (window.__wgPositionRing) {
2063
- window.__wgPositionRing(box, label, tone || "planned", { size, weight, focus, color });
2039
+ window.__wgPositionRing(box, label, tone || "planned", { size, weight });
2064
2040
  }
2065
2041
  },
2066
- { box, label, tone: tone || "planned", size, weight, focus, color },
2042
+ { box, label, tone: tone || "planned", size, weight },
2067
2043
  )
2068
2044
  .catch(() => {});
2069
2045
  }
@@ -2153,10 +2129,7 @@ async function presentSlides(page, slides, _gate, opts) {
2153
2129
  if (s.selector) {
2154
2130
  try {
2155
2131
  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
- }
2132
+ await applyHighlightZoom(page, s.selector, s.zoom);
2160
2133
  const box = await page.locator(s.selector).first().boundingBox();
2161
2134
  if (box) await showRing(page, box, caption, slideTone, slideStyle);
2162
2135
  } catch {
@@ -2174,31 +2147,31 @@ async function presentSlides(page, slides, _gate, opts) {
2174
2147
  panel.id = "wg-panel";
2175
2148
  }
2176
2149
  panel.classList.remove("wg-error", "wg-expected");
2177
- // Do not clear wg-collapsed here - WirePanelChrome honors Hide (localStorage)
2150
+ // Do not clear wg-collapsed - WirePanelChrome honors Hide (localStorage)
2178
2151
  // unless forceCollapsed:true (--mini / --video).
2179
2152
  const esc = (t) =>
2180
2153
  String(t).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
2181
2154
  const episodeLine =
2182
2155
  info.episodeNumber !== undefined && info.episodeNumber !== null
2183
- ? "<div class=\\\"wg-episode\\\">Episode " +
2156
+ ? "<div class=\\"wg-episode\\">Episode " +
2184
2157
  info.episodeNumber +
2185
2158
  (info.episodeTitle ? ": " + esc(info.episodeTitle) : "") +
2186
2159
  " · yap slides</div>"
2187
2160
  : "";
2188
2161
  const paceHtml =
2189
2162
  info.paceBadge || info.paceLabel
2190
- ? "<div class=\\\"wg-pace\\\" data-pace-kind=\\\"" +
2163
+ ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
2191
2164
  esc(info.paceKind || "normal") +
2192
- "\\\"><span class=\\\"wg-pace-badge\\\">" +
2165
+ "\\"><span class=\\"wg-pace-badge\\">" +
2193
2166
  esc(info.paceBadge || "1x") +
2194
2167
  "</span><span>" +
2195
2168
  esc(info.paceLabel || "") +
2196
2169
  "</span></div>"
2197
2170
  : "";
2198
2171
  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\\\">" +
2172
+ "<div class=\\"wg-chrome\\"><span class=\\"wg-chrome-title\\">waygraph demo</span>" +
2173
+ "<button type=\\"button\\" class=\\"wg-hide-btn\\" data-wg-toggle=\\"1\\">Hide</button></div>" +
2174
+ "<div class=\\"wg-body\\">" +
2202
2175
  episodeLine +
2203
2176
  paceHtml +
2204
2177
  "<h3>Slide " +
@@ -2208,35 +2181,35 @@ async function presentSlides(page, slides, _gate, opts) {
2208
2181
  (info.blockName ? " · " + esc(info.blockName) : "") +
2209
2182
  "</h3>" +
2210
2183
  (info.tag
2211
- ? "<div style=\\\"border:none;padding:0;margin:0 0 6px;font-size:11px;color:#c9a6ff\\\">" +
2184
+ ? "<div style=\\"border:none;padding:0;margin:0 0 6px;font-size:11px;color:#c9a6ff\\">" +
2212
2185
  esc(info.tag) +
2213
2186
  "</div>"
2214
2187
  : "") +
2215
- "<div class=\\\"wg-narration\\\">" +
2188
+ "<div class=\\"wg-narration\\">" +
2216
2189
  esc(info.caption) +
2217
2190
  "</div>" +
2218
2191
  (info.detail
2219
- ? "<div class=\\\"wg-result-pretty\\\" style=\\\"margin:0 0 12px;color:#f0e8ff\\\">" +
2192
+ ? "<div class=\\"wg-result-pretty\\" style=\\"margin:0 0 12px;color:#f0e8ff\\">" +
2220
2193
  esc(info.detail) +
2221
2194
  "</div>"
2222
2195
  : "") +
2223
2196
  (info.dwellMs != null
2224
- ? "<div class=\\\"wg-auto\\\" id=\\\"wg-dwell-hint\\\" style=\\\"margin:0 0 8px\\\">Hold " +
2197
+ ? "<div class=\\"wg-auto\\" id=\\"wg-dwell-hint\\" style=\\"margin:0 0 8px\\">Hold " +
2225
2198
  (info.dwellMs / 1000).toFixed(1) +
2226
2199
  "s before next...</div>"
2227
2200
  : "") +
2228
- "<div class=\\\"wg-autoplay-row\\\"><label><input type=\\\"checkbox\\\" id=\\\"wg-autoplay-cb\\\"" +
2201
+ "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
2229
2202
  (info.autoNow ? " checked" : "") +
2230
2203
  "> Auto-advance</label></div>" +
2231
- "<div id=\\\"wg-gate-manual\\\"" +
2232
- (info.autoNow ? " style=\\\"display:none\\\"" : "") +
2233
- "><button type=\\\"button\\\" id=\\\"wg-run\\\"" +
2204
+ "<div id=\\"wg-gate-manual\\"" +
2205
+ (info.autoNow ? " style=\\"display:none\\"" : "") +
2206
+ "><button type=\\"button\\" id=\\"wg-run\\"" +
2234
2207
  (info.dwellMs != null ? " disabled" : "") +
2235
2208
  ">" +
2236
2209
  (info.isLast ? "Continue \\u25B6" : "Next slide \\u25B6") +
2237
2210
  "</button></div>" +
2238
- "<div id=\\\"wg-gate-auto\\\" class=\\\"wg-auto\\\"" +
2239
- (info.autoNow ? "" : " style=\\\"display:none\\\"") +
2211
+ "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" +
2212
+ (info.autoNow ? "" : " style=\\"display:none\\"") +
2240
2213
  ">Auto-advancing...</div>" +
2241
2214
  "</div>";
2242
2215
  if (isNew) {
@@ -2254,10 +2227,19 @@ async function presentSlides(page, slides, _gate, opts) {
2254
2227
  " / " +
2255
2228
  info.total +
2256
2229
  (info.blockName ? " \\u00b7 " + info.blockName : ""),
2257
- // Video / --mini stays on the mini stepper (Ep N · step · block pill).
2230
+ // Video / --mini: compact pill. Else omit so Hide sticks.
2258
2231
  forceCollapsed: info.forceCollapsed === true ? true : undefined,
2259
2232
  });
2260
2233
  }
2234
+ if (window.__wgStampModal) {
2235
+ window.__wgStampModal(panel, "panel", {
2236
+ phase: "slide",
2237
+ step: info.index + 1,
2238
+ total: info.total,
2239
+ block: info.blockName || "",
2240
+ ready: true,
2241
+ });
2242
+ }
2261
2243
  let autoNow = false;
2262
2244
  try {
2263
2245
  autoNow = localStorage.getItem("wg-autoplay") === "1";
@@ -2408,8 +2390,7 @@ async function teardownOverlay(page) {
2408
2390
  window.__wgRingTrack = null;
2409
2391
  }
2410
2392
  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"]) {
2393
+ for (const id of ["wg-panel", "wg-ring", "wg-ring-label", "wg-cursor", "wg-click-pulse", "wg-banner"]) {
2413
2394
  const el = document.getElementById(id);
2414
2395
  if (el) el.remove();
2415
2396
  }
@@ -2417,53 +2398,24 @@ async function teardownOverlay(page) {
2417
2398
  .catch(() => {});
2418
2399
  }
2419
2400
 
2420
- async function moveCursorTo(page, box, ms, opts) {
2401
+ async function moveCursorTo(page, box, ms) {
2421
2402
  const x = box.x + box.width / 2;
2422
2403
  const y = box.y + box.height / 2;
2423
2404
  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
2405
  await page
2428
2406
  .evaluate(({ x, y, ms }) => {
2429
- window.__wgCameraLock = true;
2430
- window.__wgIgnoreHostMouse = true;
2431
2407
  if (window.__wgMoveCursorTo) window.__wgMoveCursorTo(x, y, ms);
2432
2408
  }, { x, y, ms: travel })
2433
2409
  .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
- }
2410
+ // CSS transition is async - wait the full travel so the cursor is ON the
2411
+ // target before ring/pulse/real click (NavBlock click nav was skipping
2412
+ // this and looked like a teleport).
2444
2413
  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(() => {});
2414
+ await new Promise((res) => setTimeout(res, travel));
2453
2415
  }
2454
2416
  return { x, y };
2455
2417
  }
2456
2418
 
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
2419
  async function clickPulseAt(page, x, y, tone) {
2468
2420
  await page
2469
2421
  .evaluate(({ x, y, tone }) => {
@@ -2588,47 +2540,17 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2588
2540
  proto.click = async function (options) {
2589
2541
  let clickPoint = null;
2590
2542
  let clickTone = "auto";
2591
- let keptCameraLock = false;
2592
2543
  try {
2593
2544
  await installOverlay(page);
2545
+ // Wait until the target is visible BEFORE measuring - otherwise
2546
+ // defineNavBlock({ click }) (and any slow-to-appear control) skips
2547
+ // the cursor entirely: boundingBox was null, then originalClick
2548
+ // waited and clicked with no demo animation.
2594
2549
  await this.waitFor({
2595
2550
  state: "visible",
2596
2551
  timeout: (options && options.timeout) || 30000,
2597
2552
  }).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();
2553
+ const box = await this.boundingBox();
2632
2554
  const narrated = box ? await wasJustNarrated(page) : false;
2633
2555
  if (box && !narrated) {
2634
2556
  let fallback = "click";
@@ -2636,8 +2558,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2636
2558
  const text = (await this.textContent())?.trim();
2637
2559
  if (text && text.length > 0 && text.length <= 30) fallback = text;
2638
2560
  } catch {
2639
- /* ignore */
2561
+ // element has no simple text (an icon button, say) - generic label is fine
2640
2562
  }
2563
+ // Prefer "nav: …" when this locator is a NavBlock click target
2564
+ // (set on page by runStepMode just before act).
2641
2565
  try {
2642
2566
  const navLabel = await page.evaluate(() => window.__wgPendingNavClickLabel || null);
2643
2567
  if (navLabel) fallback = String(navLabel);
@@ -2647,80 +2571,39 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2647
2571
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
2648
2572
  const cap = await captionForLocator(page, this, stubs, fallback);
2649
2573
  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, {
2574
+ clickPoint = await moveCursorTo(page, box, cursorMs(600));
2575
+ await showRing(page, box, cap.label, cap.tone, {
2662
2576
  size: cap.size,
2663
2577
  weight: cap.weight,
2664
2578
  });
2665
2579
  await dwellMatchedStub(page, this, stubs, pacing);
2580
+ // "pop for a few seconds" - Dan's own phrase, matching the
2581
+ // zsign demo-engine's ring-before-click pattern in
2582
+ // services/help-center-clip-engine's video-pipeline.
2666
2583
  await new Promise((res) => setTimeout(res, clickPrePop()));
2667
2584
  } else if (box) {
2668
- const travel = zoomed ? cursorMs(120) : cursorMs(200);
2669
- clickPoint = await moveCursorTo(page, box, travel, { keepLock: zoomed });
2585
+ // Ring/caption already handled by narrate() - still move the
2586
+ // cursor + pulse the click point, just skip re-showing the ring.
2587
+ clickPoint = await moveCursorTo(page, box, cursorMs(200));
2670
2588
  clickTone = "planned";
2671
2589
  }
2672
2590
  } catch {
2673
- /* best-effort */
2591
+ // best-effort - the real click below still runs either way
2674
2592
  }
2675
2593
  if (clickPoint) {
2676
2594
  await clickPulseAt(page, clickPoint.x, clickPoint.y, clickTone);
2595
+ // clickPulseAt only triggers the CSS animation class - it doesn't
2596
+ // wait for it. Without a pause here, the real click (and any
2597
+ // resulting navigation/DOM change) fires while the ripple is still
2598
+ // mid-animation, sometimes cutting it off before it's even visible.
2599
+ // Wait out the same .5s the "@keyframes wg-pulse" rule uses, so the
2600
+ // mock click visually completes before the real one fires.
2677
2601
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 500));
2678
2602
  }
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
- }
2603
+ const result = await originalClick.call(this, options);
2604
+ await new Promise((res) => setTimeout(res, clickPostPop()));
2605
+ await hideRing(page);
2606
+ return result;
2724
2607
  };
2725
2608
  }
2726
2609
 
@@ -2830,7 +2713,10 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2830
2713
  // installOverlay only seeds localStorage when the key is null (so mid-run
2831
2714
  // checkbox clicks survive navigations). Without this, a prior --autoplay
2832
2715
  // session sticks forever and agents cannot flip back with --no-autoplay.
2833
- if (process.env.WAYGRAPH_AUTOPLAY !== undefined) {
2716
+ // Must run on the *demo origin* (after goto), not about:blank - localStorage
2717
+ // is origin-scoped.
2718
+ const syncAutoplayFromEnv = async () => {
2719
+ if (process.env.WAYGRAPH_AUTOPLAY === undefined) return;
2834
2720
  const on = process.env.WAYGRAPH_AUTOPLAY === "1";
2835
2721
  await page
2836
2722
  .evaluate((want) => {
@@ -2841,7 +2727,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2841
2727
  }
2842
2728
  }, on)
2843
2729
  .catch(() => {});
2844
- }
2730
+ };
2731
+ await syncAutoplayFromEnv();
2845
2732
  // Some real Blocks (e.g. zsign-all's login.block.ts) call
2846
2733
  // page.setViewportSize({ width: 1280, height: 720 }) inside their own
2847
2734
  // act() - a hardcoded override for THEIR OWN testing consistency, with no
@@ -2995,6 +2882,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
2995
2882
  // still rendered on screen.
2996
2883
  if ((clearSession || r.resetSession) && i > 0) {
2997
2884
  await resetPageState(context, page, baseURL);
2885
+ // clear() wiped origin storage - re-apply CLI autoplay default.
2886
+ await syncAutoplayFromEnv();
2998
2887
  }
2999
2888
  // Episode pace: block > flow > CLI --fast (only when no authored pace).
3000
2889
  // Authoring wins: withDemoPace("slow"|2|4500) keeps slow dwell even under --fast.
@@ -3034,9 +2923,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3034
2923
  const stubBeforeTodos = stubBeforePhase.todos || [];
3035
2924
  const isNavBlock = r.block.__waygraphKind === "nav";
3036
2925
  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.
2926
+ // Video / --mini: compact pill. Never pass false - Hide/localStorage wins.
3040
2927
  const forceCollapsed =
3041
2928
  !!process.env.WAYGRAPH_VIDEO ||
3042
2929
  process.env.WAYGRAPH_MINI === "1" ||
@@ -3086,7 +2973,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3086
2973
  todos: stubBeforeTodos,
3087
2974
  ...paceSpeak,
3088
2975
  });
3089
- await setDemoTodos(page, stubBeforeTodos);
3090
2976
  const edits = await gate();
3091
2977
  for (const k of requires) {
3092
2978
  if (edits[k.name] !== undefined) {
@@ -3098,40 +2984,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3098
2984
  }
3099
2985
  }
3100
2986
  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
2987
  await markStepRunning(page, {});
3136
2988
  // NavBlock click-nav: label the upcoming Locator.click demo cursor as
3137
2989
  // "nav: <block>" so pia/demo watchers see cursor+pulse on click nav
@@ -3164,52 +3016,12 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3164
3016
  delete window.__wgPendingNavClickLabel;
3165
3017
  })
3166
3018
  .catch(() => {});
3167
- // stubOnError: ring the broken UI BEFORE the error panel (PIA RFC).
3168
- // stubAfter stays success-only - distinct BUG/FAIL copy on the fail path.
3169
- const fixturesOnError = r.highlightFixtures;
3170
- if (hasAuthoredStubOnError(r.block, fixturesOnError)) {
3171
- await page
3172
- .evaluate(() => {
3173
- if (window.__wgRingTrack) {
3174
- window.removeEventListener("resize", window.__wgRingTrack);
3175
- window.removeEventListener("scroll", window.__wgRingTrack, true);
3176
- window.__wgRingTrack = null;
3177
- }
3178
- })
3179
- .catch(() => {});
3180
- const errPhase = await runStubPhase(r.block, "stubOnError", {
3181
- fixtures: fixturesOnError,
3182
- error: err,
3183
- });
3184
- await setDemoBannerTitle(page, errPhase.title);
3185
- const errHighlights = errPhase.highlights.map((h) => {
3186
- const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
3187
- return {
3188
- selector: styled.selector,
3189
- label: formatHighlightCaption(styled),
3190
- duration: styled.duration,
3191
- fastMode: styled.fastMode,
3192
- tone: styled.tone,
3193
- size: styled.size,
3194
- weight: styled.weight,
3195
- zoom: styled.zoom,
3196
- focus: styled.focus,
3197
- color: styled.color,
3198
- cursor: styled.cursor,
3199
- zoomOut: styled.zoomOut,
3200
- };
3201
- });
3202
- await cycleHighlightRings(page, errHighlights, !!pacing.gatesFast, {
3203
- defaultHoldMs: 2000,
3204
- pace: pacing.demoPace,
3205
- todos: errPhase.todos,
3206
- });
3207
- }
3208
- // A thrown act()/observe()/a failed verify Trait used to just crash
3209
- // the whole Node process with a raw stack trace - the browser closes
3210
- // (main()'s own try/finally) before a human watching ever sees WHY.
3211
- // Show it on-screen and let a real click acknowledge it first.
3212
- await renderStepError(page, {
3019
+ // stubOnError + error/expected panel BEFORE acknowledging (PIA RFC).
3020
+ const errEdits = await presentFailPanel(page, {
3021
+ block: r.block,
3022
+ fixtures: r.highlightFixtures,
3023
+ highlightStyle: r.highlightStyle,
3024
+ error: err,
3213
3025
  index: i,
3214
3026
  total: resolved.length,
3215
3027
  blockName: r.block.name,
@@ -3223,14 +3035,13 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3223
3035
  episodeTitle: r.episodeTitle,
3224
3036
  expectedFailureReason: r.expectedFailureReason,
3225
3037
  stepperMode,
3226
- // Omit forceCollapsed on errors so Hide preference is not stomped.
3038
+ gatesFast: pacing.gatesFast,
3039
+ demoPace: pacing.demoPace,
3040
+ gate,
3227
3041
  });
3228
- const errEdits = await gate();
3229
3042
  if (errEdits && errEdits.__wgRetry) {
3230
- // Same clean slate a normal episode boundary gets - not just
3231
- // re-running the failed block against whatever broken/half-
3232
- // navigated state it left the page in.
3233
3043
  await resetPageState(context, page, baseURL);
3044
+ await syncAutoplayFromEnv();
3234
3045
  i = episodeStartIndex - 1;
3235
3046
  continue;
3236
3047
  }
@@ -3252,6 +3063,50 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3252
3063
  })
3253
3064
  .catch(() => {});
3254
3065
  result = stepOutcome.result;
3066
+ // withExpectedFailure last block that SUCCEEDS on the intentional fail
3067
+ // branch (e.g. submit-login -> LoginPage + error banner). Still show
3068
+ // stubOnError rings + amber "expected outcome" panel - branching no
3069
+ // longer throws, but the demo must not skip the educational overlay.
3070
+ if (r.expectedFailureReason) {
3071
+ const errEdits = await presentFailPanel(page, {
3072
+ block: r.block,
3073
+ fixtures: r.highlightFixtures,
3074
+ highlightStyle: r.highlightStyle,
3075
+ error: undefined,
3076
+ out: result,
3077
+ index: i,
3078
+ total: resolved.length,
3079
+ blockName: r.block.name,
3080
+ message:
3081
+ "Resolved " +
3082
+ (result && result.__state ? result.__state : JSON.stringify(result)) +
3083
+ " (expected failure path)",
3084
+ allNames: moduleNames,
3085
+ allDescriptions: moduleDescriptions,
3086
+ moduleIndex,
3087
+ allEpisodes,
3088
+ title,
3089
+ episodeNumber: r.episodeNumber,
3090
+ episodeTitle: r.episodeTitle,
3091
+ expectedFailureReason: r.expectedFailureReason,
3092
+ stepperMode,
3093
+ gatesFast: pacing.gatesFast,
3094
+ demoPace: pacing.demoPace,
3095
+ gate,
3096
+ });
3097
+ if (errEdits && errEdits.__wgRetry) {
3098
+ await resetPageState(context, page, baseURL);
3099
+ await syncAutoplayFromEnv();
3100
+ i = episodeStartIndex - 1;
3101
+ continue;
3102
+ }
3103
+ if (process.env.WAYGRAPH_JSON !== "1") {
3104
+ console.log(
3105
+ "waygraph: expected failure on " + r.block.name + " - " + r.expectedFailureReason,
3106
+ );
3107
+ }
3108
+ break;
3109
+ }
3255
3110
  const fixturesAfter = r.highlightFixtures;
3256
3111
  const slides = resolveSlides(r.block, { out: result, fixtures: fixturesAfter }).map((s) =>
3257
3112
  applyHighlightStyleDefaults(s, r.highlightStyle),
@@ -3275,8 +3130,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3275
3130
  fixtures: fixturesAfter,
3276
3131
  });
3277
3132
  stubAfterTodos = afterPhase.todos || [];
3278
- await setDemoBannerTitle(page, afterPhase.title);
3279
- await setDemoTodos(page, afterPhase.todos || []);
3280
3133
  highlights = afterPhase.highlights.map((h) => {
3281
3134
  const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
3282
3135
  return {
@@ -3288,10 +3141,6 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
3288
3141
  size: styled.size,
3289
3142
  weight: styled.weight,
3290
3143
  zoom: styled.zoom,
3291
- focus: styled.focus,
3292
- color: styled.color,
3293
- cursor: styled.cursor,
3294
- zoomOut: styled.zoomOut,
3295
3144
  };
3296
3145
  });
3297
3146
  } else {
@@ -4674,7 +4523,7 @@ Primary (less is more):
4674
4523
  --auto-next Auto-advance steps (alias: --autoplay)
4675
4524
  --fast Shorter auto-next / Next gates (keeps smooth cursor)
4676
4525
  --full Classic wrap-all block chips (default: carousel)
4677
- --mini Force compact mini panel (Hide pill); alias --stepper-mini
4526
+ --mini Force compact mini panel (Hide pill + Next); alias --stepper-mini
4678
4527
  --ff-expand Expand fastForwardComposeBlock inners as separate steps
4679
4528
  --ff-disabled Dispute: expand FF (alias --no-ff); blitz kept on those inners
4680
4529
  --auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless by default