waygraph 0.12.40 → 0.12.42

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