waygraph 0.13.3 → 0.13.5
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 +1776 -124
- package/dist/highlights.d.ts +35 -3
- package/dist/highlights.js +55 -17
- package/dist/index.d.ts +1 -1
- package/examples/saucedemo/.tmp/device-demo/orientation-gestures.webm +0 -0
- package/examples/saucedemo/.tmp/device-demo/page@fe3808e393870b60560762cb58982a73.webm +0 -0
- package/examples/saucedemo/.tmp/device-showcase/page@01821f1be3025a1f33af7aeaa7c31453.webm +0 -0
- package/examples/saucedemo/.tmp/device-showcase/page@112894bf514e729c25426effd0465db0.webm +0 -0
- package/examples/saucedemo/.tmp/device-showcase/page@254d2f8bb34167fb9f128efc2aa9831f.webm +0 -0
- package/examples/saucedemo/.tmp/device-showcase/page@2fee43e36341108e0ee6e6a1360b14f9.webm +0 -0
- package/examples/saucedemo/.tmp/device-showcase/page@648144d90fdf2c65bd7b03faeadc2aeb.webm +0 -0
- package/examples/saucedemo/.tmp/device-showcase/page@a360145174b3bccffd6516db7d084a9c.webm +0 -0
- package/examples/saucedemo/.tmp/device-showcase/page@d045914b97b35d17147a940965a7a93f.webm +0 -0
- package/examples/saucedemo/.tmp/ts/page@6145fa8fc23d19870af85e0e111c0e30.webm +0 -0
- package/examples/saucedemo/.tmp/ts-sample/page@e2995031b7eef8fc0e3c1bdb60f58254.webm +0 -0
- package/examples/saucedemo/.tmp/ts2/page@1a450773bef74c91a68a89093fc41838.webm +0 -0
- package/examples/saucedemo/README.md +2 -0
- package/examples/saucedemo/package.json +2 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/nav-cart.block.ts +21 -13
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +27 -18
- package/examples/saucedemo/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +40 -27
- package/examples/saucedemo/src/flows/device-showcase.flow.ts +28 -0
- package/package.json +1 -1
- package/templates/quickstart/src/blocks/saucedemo-web/cart/nav-cart.block.ts +10 -4
- package/templates/quickstart/src/blocks/saucedemo-web/inventory/methods/add-to-cart.effect.block.ts +7 -5
- package/templates/quickstart/src/blocks/saucedemo-web/methods/submit-login.method.block.ts +6 -3
- package/templates/quickstart/src/flows/device-showcase.flow.ts +28 -0
package/dist/cli.js
CHANGED
|
@@ -479,6 +479,26 @@ const RING_CSS =
|
|
|
479
479
|
"#wg-click-pulse[data-tone=success]{border-color:#22C55E;background:rgba(34,197,94,.28);}" +
|
|
480
480
|
"#wg-click-pulse.wg-pulse{animation:wg-pulse .5s ease-out;}" +
|
|
481
481
|
"@keyframes wg-pulse{0%{opacity:.9;transform:scale(.4);}100%{opacity:0;transform:scale(2.4);}}" +
|
|
482
|
+
// Spotlight: dim everything except the highlight target (focus: true).
|
|
483
|
+
"#wg-focus-veil{position:fixed;z-index:2147483644;pointer-events:none;" +
|
|
484
|
+
"border-radius:12px;box-shadow:0 0 0 9999px rgba(8,4,20,.62);" +
|
|
485
|
+
"opacity:0;transition:opacity .3s ease,left .3s ease,top .3s ease,width .3s ease,height .3s ease;}" +
|
|
486
|
+
"#wg-focus-veil.wg-in{opacity:1;}" +
|
|
487
|
+
// Touch swipe trail (mobile/tablet orientation + device theater).
|
|
488
|
+
"#wg-swipe-layer{position:fixed;inset:0;z-index:2147483646;pointer-events:none;" +
|
|
489
|
+
"overflow:hidden;}" +
|
|
490
|
+
"#wg-swipe-layer .wg-swipe-dot{position:absolute;width:14px;height:14px;margin:-7px 0 0 -7px;" +
|
|
491
|
+
"border-radius:50%;background:rgba(147,197,253,.55);border:2px solid rgba(255,255,255,.85);" +
|
|
492
|
+
"box-shadow:0 0 12px rgba(59,130,246,.55);opacity:0;}" +
|
|
493
|
+
"#wg-swipe-layer .wg-swipe-finger{position:absolute;width:28px;height:28px;margin:-14px 0 0 -14px;" +
|
|
494
|
+
"border-radius:50%;background:rgba(15,23,42,.92);border:2px solid #fff;" +
|
|
495
|
+
"box-shadow:0 4px 16px rgba(0,0,0,.4);opacity:0;" +
|
|
496
|
+
"transition:left .05s linear,top .05s linear,opacity .15s ease;}" +
|
|
497
|
+
"#wg-swipe-layer .wg-swipe-label{position:absolute;left:50%;top:18%;transform:translateX(-50%);" +
|
|
498
|
+
"padding:6px 12px;border-radius:999px;background:rgba(15,23,42,.88);color:#e0f2fe;" +
|
|
499
|
+
"border:1px solid rgba(56,189,248,.55);font:700 12px/1.2 system-ui,sans-serif;" +
|
|
500
|
+
"letter-spacing:.04em;text-transform:uppercase;opacity:0;transition:opacity .25s ease;}" +
|
|
501
|
+
"#wg-swipe-layer.wg-in .wg-swipe-label{opacity:1;}" +
|
|
482
502
|
"#wg-panel{position:fixed;z-index:2147483647;left:50%;bottom:12px;transform:translateX(-50%);" +
|
|
483
503
|
"max-width:min(92vw,640px);max-height:calc(100vh - 24px);overflow-y:auto;box-sizing:border-box;" +
|
|
484
504
|
"background:rgba(20,10,40,.94);color:#fff;border-radius:14px;" +
|
|
@@ -520,18 +540,57 @@ const RING_CSS =
|
|
|
520
540
|
"#wg-panel .wg-pace[data-pace-kind=blitz] .wg-pace-badge{background:#22C55E;color:#052e16;}" +
|
|
521
541
|
"#wg-panel .wg-pace[data-pace-kind=ms] .wg-pace-badge{background:#3B82F6;color:#fff;}" +
|
|
522
542
|
"#wg-panel .wg-narration{margin:0 0 12px;font:italic 14px/1.4 system-ui,sans-serif;color:#f0e8ff;}" +
|
|
523
|
-
// Todo checklist
|
|
524
|
-
//
|
|
525
|
-
// Click dock to slide left <-> right (WAYGRAPH_TODO_POS / --todo-left|right).
|
|
526
|
-
"
|
|
543
|
+
// Todo checklist(s) float outside #wg-panel so --mini / Hide never hide them.
|
|
544
|
+
// Multiple docks (one per todoId) are supported - stacked on the same side.
|
|
545
|
+
// Click a dock to slide left <-> right (WAYGRAPH_TODO_POS / --todo-left|right).
|
|
546
|
+
".wg-todo-dock,#wg-todo-dock{position:fixed;z-index:2147483646;top:72px;left:14px;" +
|
|
527
547
|
"width:min(280px,42vw);max-height:calc(100vh - 100px);overflow:auto;box-sizing:border-box;" +
|
|
528
548
|
"padding:10px 12px;background:rgba(20,10,40,.94);color:#fff;border-radius:12px;" +
|
|
529
549
|
"border:1px solid rgba(124,58,237,.45);box-shadow:0 8px 24px rgba(0,0,0,.35);" +
|
|
530
550
|
"pointer-events:auto;cursor:pointer;" +
|
|
531
|
-
"transition:transform .4s cubic-bezier(.22,1,.36,1);transform:translateX(0);}" +
|
|
532
|
-
"
|
|
533
|
-
"
|
|
534
|
-
"#wg-todo-dock #wg-todos{list-style:none;margin:0;padding:0;background:transparent;border:none;}" +
|
|
551
|
+
"transition:transform .4s cubic-bezier(.22,1,.36,1),top .35s ease;transform:translateX(0);}" +
|
|
552
|
+
".wg-todo-dock[data-pos=right],#wg-todo-dock[data-pos=right]{transform:translateX(calc(100vw - 100% - 28px));}" +
|
|
553
|
+
".wg-todo-dock[data-pos=left],#wg-todo-dock[data-pos=left]{transform:translateX(0);}" +
|
|
554
|
+
".wg-todo-dock .wg-todos-list,#wg-todo-dock #wg-todos,.wg-todo-dock #wg-todos{list-style:none;margin:0;padding:0;background:transparent;border:none;}" +
|
|
555
|
+
// Video device stage: keep recordVideo size fixed; center a device-sized shell.
|
|
556
|
+
"html.wg-video-device-stage{background:#0b1220 !important;}" +
|
|
557
|
+
"html.wg-video-device-stage body{margin:0 !important;min-height:100vh !important;" +
|
|
558
|
+
"display:flex !important;align-items:center !important;justify-content:center !important;" +
|
|
559
|
+
"background:#0b1220 !important;overflow:hidden !important;}" +
|
|
560
|
+
"#wg-device-shell{flex-shrink:0;overflow:auto;background:#fff;" +
|
|
561
|
+
"border-radius:16px;box-shadow:0 24px 80px rgba(0,0,0,.55),0 0 0 1px rgba(255,255,255,.08);" +
|
|
562
|
+
"transform-origin:center center;" +
|
|
563
|
+
/* Force radius clip on all corners (Chrome + transform/scale). */
|
|
564
|
+
"-webkit-mask-image:-webkit-radial-gradient(white,black);" +
|
|
565
|
+
"isolation:isolate;" +
|
|
566
|
+
"transition:border-radius .55s cubic-bezier(.22,1,.36,1),box-shadow .55s ease," +
|
|
567
|
+
"transform .55s cubic-bezier(.22,1,.36,1),width .5s ease,height .5s ease,max-width .5s ease,max-height .5s ease;}" +
|
|
568
|
+
"#wg-device-shell.wg-shell-enter{border-radius:0;box-shadow:none;}" +
|
|
569
|
+
"#wg-device-shell.wg-shell-shutter-out{border-radius:0 !important;" +
|
|
570
|
+
"box-shadow:none !important;outline:none !important;border:none !important;}" +
|
|
571
|
+
"#wg-device-shell.wg-shell-desktop-flat{border-radius:0 !important;box-shadow:none !important;" +
|
|
572
|
+
"outline:none !important;border:none !important;-webkit-mask-image:none;}" +
|
|
573
|
+
// Always-on zoom HUD - bottom-right (top-left is crowded: todos + banners).
|
|
574
|
+
"#wg-zoom-badge{position:fixed;z-index:2147483646;bottom:14px;right:14px;" +
|
|
575
|
+
"display:flex;align-items:center;gap:7px;padding:6px 11px 6px 8px;" +
|
|
576
|
+
"border-radius:999px;background:rgba(20,10,40,.94);color:#f0e8ff;" +
|
|
577
|
+
"border:1px solid rgba(250,204,21,.6);box-shadow:0 6px 18px rgba(0,0,0,.4);" +
|
|
578
|
+
"font:700 12px/1.2 system-ui,sans-serif;pointer-events:none;" +
|
|
579
|
+
"opacity:1;transform:translateY(0);}" +
|
|
580
|
+
"#wg-zoom-badge .wg-zoom-ico{width:18px;height:18px;display:flex;align-items:center;justify-content:center;" +
|
|
581
|
+
"border-radius:6px;background:rgba(250,204,21,.22);}" +
|
|
582
|
+
"#wg-zoom-badge .wg-zoom-ico svg{width:14px;height:14px;display:block;}" +
|
|
583
|
+
"#wg-zoom-badge .wg-zoom-val{color:#fde68a;font-variant-numeric:tabular-nums;font-weight:800;min-width:3.2em;}" +
|
|
584
|
+
"#wg-zoom-badge[data-zoomed=\\"1\\"]{border-color:#fbbf24;}" +
|
|
585
|
+
// Typing chip (method fills: username / password / etc.)
|
|
586
|
+
"#wg-typing-badge{position:fixed;z-index:2147483646;bottom:88px;left:50%;transform:translateX(-50%);" +
|
|
587
|
+
"display:flex;align-items:center;gap:8px;padding:8px 14px;" +
|
|
588
|
+
"border-radius:999px;background:rgba(20,10,40,.95);color:#e0f2fe;" +
|
|
589
|
+
"border:1px solid rgba(56,189,248,.55);box-shadow:0 8px 22px rgba(0,0,0,.4);" +
|
|
590
|
+
"font:700 12px/1.2 system-ui,sans-serif;pointer-events:none;" +
|
|
591
|
+
"opacity:0;transition:opacity .2s ease;}" +
|
|
592
|
+
"#wg-typing-badge.wg-in{opacity:1;}" +
|
|
593
|
+
"#wg-typing-badge .wg-ty-dots{letter-spacing:.15em;color:#7dd3fc;}" +
|
|
535
594
|
// Device toast + chip (0.13.2+) - icon toast on fixture change, then compact chip.
|
|
536
595
|
"#wg-device-toast{position:fixed;z-index:2147483647;top:14px;right:14px;" +
|
|
537
596
|
"display:flex;align-items:center;gap:10px;min-width:200px;max-width:min(92vw,320px);" +
|
|
@@ -570,17 +629,20 @@ const RING_CSS =
|
|
|
570
629
|
"#wg-device-badge .wg-dev-icon svg{width:14px;height:14px;display:block;}" +
|
|
571
630
|
"#wg-device-badge .wg-dev-label{white-space:nowrap;}" +
|
|
572
631
|
"#wg-cursor[data-touch=1]{width:28px;height:28px;}" +
|
|
573
|
-
"#wg-todos{list-style:none;margin:0 0 12px;padding:8px 10px;background:#0f0620;border-radius:8px;" +
|
|
632
|
+
"#wg-todos,.wg-todos-list{list-style:none;margin:0 0 12px;padding:8px 10px;background:#0f0620;border-radius:8px;" +
|
|
574
633
|
"border:1px solid #3a2a60;}" +
|
|
575
|
-
"#wg-todos li{display:flex;gap:8px;align-items:flex-start;margin:0 0 6px;font:600 12.5px/1.35 system-ui,sans-serif;}" +
|
|
576
|
-
"#wg-todos li:last-child{margin-bottom:0;}" +
|
|
577
|
-
"#wg-todos .wg-todo-mark{flex:0 0 auto;width:1.1em;text-align:center;}" +
|
|
578
|
-
"#wg-todos .wg-todo-done{color:#6ee7b7;text-decoration:line-through;opacity:.85;}" +
|
|
579
|
-
"#wg-todos .wg-todo-current{color:#fff;}" +
|
|
580
|
-
"#wg-todos .wg-todo-pending{color:#9a7ad1;}" +
|
|
634
|
+
"#wg-todos li,.wg-todos-list li{display:flex;gap:8px;align-items:flex-start;margin:0 0 6px;font:600 12.5px/1.35 system-ui,sans-serif;}" +
|
|
635
|
+
"#wg-todos li:last-child,.wg-todos-list li:last-child{margin-bottom:0;}" +
|
|
636
|
+
"#wg-todos .wg-todo-mark,.wg-todos-list .wg-todo-mark{flex:0 0 auto;width:1.1em;text-align:center;}" +
|
|
637
|
+
"#wg-todos .wg-todo-done,.wg-todos-list .wg-todo-done{color:#6ee7b7;text-decoration:line-through;opacity:.85;}" +
|
|
638
|
+
"#wg-todos .wg-todo-current,.wg-todos-list .wg-todo-current{color:#fff;}" +
|
|
639
|
+
"#wg-todos .wg-todo-pending,.wg-todos-list .wg-todo-pending{color:#9a7ad1;}" +
|
|
581
640
|
"#wg-todos[data-wg-todo-style=bullets] .wg-todo-pending," +
|
|
582
|
-
"#wg-todos[data-wg-todo-style=bullets] .wg-todo-current
|
|
583
|
-
"
|
|
641
|
+
"#wg-todos[data-wg-todo-style=bullets] .wg-todo-current," +
|
|
642
|
+
".wg-todos-list[data-wg-todo-style=bullets] .wg-todo-pending," +
|
|
643
|
+
".wg-todos-list[data-wg-todo-style=bullets] .wg-todo-current{color:#e8dcff;}" +
|
|
644
|
+
"#wg-todos[data-wg-todo-style=bullets] .wg-todo-mark," +
|
|
645
|
+
".wg-todos-list[data-wg-todo-style=bullets] .wg-todo-mark{color:#c9a6ff;}" +
|
|
584
646
|
"#wg-progress{height:4px;background:#2a1650;border-radius:2px;margin:0 0 12px;overflow:hidden;}" +
|
|
585
647
|
"#wg-progress-bar{height:100%;background:#7C3AED;border-radius:2px;transition:width .3s ease;}" +
|
|
586
648
|
// A real tab bar for episodes - the currently-active episode reads as
|
|
@@ -776,7 +838,7 @@ async function installOverlay(page, title) {
|
|
|
776
838
|
window.__wgPositionRing = (box, label, tone, style) => {
|
|
777
839
|
const ring = document.getElementById("wg-ring");
|
|
778
840
|
const ringLabel = document.getElementById("wg-ring-label");
|
|
779
|
-
if (!ring || !ringLabel) return;
|
|
841
|
+
if (!ring || !ringLabel || !box) return;
|
|
780
842
|
const raw = (tone || "planned") + "";
|
|
781
843
|
const t =
|
|
782
844
|
raw === "auto" || raw === "info" || raw === "warning" || raw === "danger" || raw === "success"
|
|
@@ -797,34 +859,59 @@ async function installOverlay(page, title) {
|
|
|
797
859
|
const margin = 6;
|
|
798
860
|
const vw = window.innerWidth;
|
|
799
861
|
const vh = window.innerHeight;
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if (left + width > vw - margin) left = Math.max(margin, vw - margin - width);
|
|
807
|
-
if (top + height > vh - margin) top = Math.max(margin, vh - margin - height);
|
|
862
|
+
// Pin ring EXACTLY to the element - never clamp the ring away from
|
|
863
|
+
// the target (that looked "strapped on" the viewport).
|
|
864
|
+
const left = box.x - pad;
|
|
865
|
+
const top = box.y - pad;
|
|
866
|
+
const width = Math.max(4, box.width + pad * 2);
|
|
867
|
+
const height = Math.max(4, box.height + pad * 2);
|
|
808
868
|
ring.style.left = left + "px";
|
|
809
869
|
ring.style.top = top + "px";
|
|
810
870
|
ring.style.width = width + "px";
|
|
811
871
|
ring.style.height = height + "px";
|
|
812
872
|
ring.style.opacity = "1";
|
|
813
|
-
ringLabel.textContent = label;
|
|
873
|
+
ringLabel.textContent = label || "";
|
|
814
874
|
ringLabel.style.opacity = "1";
|
|
815
|
-
// Measure the label's own natural width/height (its real
|
|
816
|
-
// rendered size, unchanged) before deciding where it fits.
|
|
817
875
|
const lw = ringLabel.offsetWidth;
|
|
818
876
|
const lh = ringLabel.offsetHeight;
|
|
819
877
|
let labelLeft = left;
|
|
820
878
|
let labelTop = top + height + 8;
|
|
821
|
-
if (labelTop + lh > vh - margin) labelTop = top - lh - 8;
|
|
879
|
+
if (labelTop + lh > vh - margin) labelTop = top - lh - 8;
|
|
822
880
|
if (labelTop < margin) labelTop = margin;
|
|
823
881
|
if (labelLeft + lw > vw - margin) labelLeft = Math.max(margin, vw - margin - lw);
|
|
824
882
|
if (labelLeft < margin) labelLeft = margin;
|
|
825
883
|
ringLabel.style.left = labelLeft + "px";
|
|
826
884
|
ringLabel.style.top = labelTop + "px";
|
|
827
885
|
};
|
|
886
|
+
/** Live-follow a selector with rAF (accurate under device shell scale). */
|
|
887
|
+
window.__wgStopRingFollow = () => {
|
|
888
|
+
if (window.__wgRingFollowRaf) {
|
|
889
|
+
cancelAnimationFrame(window.__wgRingFollowRaf);
|
|
890
|
+
window.__wgRingFollowRaf = 0;
|
|
891
|
+
}
|
|
892
|
+
window.__wgRingFollowSel = "";
|
|
893
|
+
};
|
|
894
|
+
window.__wgFollowRing = (sel, label, tone, style, focus) => {
|
|
895
|
+
window.__wgStopRingFollow();
|
|
896
|
+
if (!sel) return;
|
|
897
|
+
window.__wgRingFollowSel = sel;
|
|
898
|
+
const tick = () => {
|
|
899
|
+
if (window.__wgRingFollowSel !== sel) return;
|
|
900
|
+
const el = document.querySelector(sel);
|
|
901
|
+
if (!el) {
|
|
902
|
+
window.__wgRingFollowRaf = requestAnimationFrame(tick);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const r = el.getBoundingClientRect();
|
|
906
|
+
const box = { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
907
|
+
if (window.__wgPositionRing) {
|
|
908
|
+
window.__wgPositionRing(box, label, tone || "planned", style || {});
|
|
909
|
+
}
|
|
910
|
+
if (focus && window.__wgApplyFocus) window.__wgApplyFocus(box);
|
|
911
|
+
window.__wgRingFollowRaf = requestAnimationFrame(tick);
|
|
912
|
+
};
|
|
913
|
+
tick();
|
|
914
|
+
};
|
|
828
915
|
window.__wgClickPulse = (x, y, tone) => {
|
|
829
916
|
const pulse = document.getElementById("wg-click-pulse");
|
|
830
917
|
if (!pulse) return;
|
|
@@ -839,19 +926,155 @@ async function installOverlay(page, title) {
|
|
|
839
926
|
void pulse.offsetWidth;
|
|
840
927
|
pulse.classList.add("wg-pulse");
|
|
841
928
|
};
|
|
842
|
-
window.__wgHideRing = () => {
|
|
929
|
+
window.__wgHideRing = (opts) => {
|
|
843
930
|
// While a narrate() call owns the ring (mid multi-step action),
|
|
844
931
|
// the auto-highlight click/fill patches' own end-of-step hide is
|
|
845
932
|
// a no-op - narrate() itself clears it once the WHOLE wrapped
|
|
846
933
|
// action finishes, not just its first sub-step.
|
|
847
934
|
if (window.__wgNarrateOwnsRing) return;
|
|
935
|
+
if (window.__wgStopRingFollow) window.__wgStopRingFollow();
|
|
848
936
|
const ring = document.getElementById("wg-ring");
|
|
849
937
|
const ringLabel = document.getElementById("wg-ring-label");
|
|
850
938
|
if (ring) ring.style.opacity = "0";
|
|
851
939
|
if (ringLabel) ringLabel.style.opacity = "0";
|
|
852
|
-
if (window.
|
|
940
|
+
if (window.__wgClearFocus) window.__wgClearFocus();
|
|
941
|
+
// Honor zoomOut:false - keep camera until next zoom / desktop clear.
|
|
942
|
+
const forceClear = opts && opts.clearZoom === true;
|
|
943
|
+
const skipClear = opts && opts.clearZoom === false;
|
|
944
|
+
if (
|
|
945
|
+
!skipClear &&
|
|
946
|
+
(forceClear || window.__wgZoomOutOnHide !== false) &&
|
|
947
|
+
window.__wgClearZoom
|
|
948
|
+
) {
|
|
949
|
+
window.__wgClearZoom();
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
window.__wgClearFocus = () => {
|
|
953
|
+
const veil = document.getElementById("wg-focus-veil");
|
|
954
|
+
if (!veil) return;
|
|
955
|
+
veil.classList.remove("wg-in");
|
|
956
|
+
setTimeout(() => {
|
|
957
|
+
const v = document.getElementById("wg-focus-veil");
|
|
958
|
+
if (v && !v.classList.contains("wg-in")) v.remove();
|
|
959
|
+
}, 320);
|
|
960
|
+
};
|
|
961
|
+
window.__wgApplyFocus = (box) => {
|
|
962
|
+
if (!box) {
|
|
963
|
+
if (window.__wgClearFocus) window.__wgClearFocus();
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
let veil = document.getElementById("wg-focus-veil");
|
|
967
|
+
if (!veil) {
|
|
968
|
+
veil = document.createElement("div");
|
|
969
|
+
veil.id = "wg-focus-veil";
|
|
970
|
+
veil.setAttribute("data-wg-ui", "1");
|
|
971
|
+
document.documentElement.appendChild(veil);
|
|
972
|
+
}
|
|
973
|
+
const pad = 10;
|
|
974
|
+
veil.style.left = Math.max(0, box.x - pad) + "px";
|
|
975
|
+
veil.style.top = Math.max(0, box.y - pad) + "px";
|
|
976
|
+
veil.style.width = Math.max(8, box.width + pad * 2) + "px";
|
|
977
|
+
veil.style.height = Math.max(8, box.height + pad * 2) + "px";
|
|
978
|
+
void veil.offsetWidth;
|
|
979
|
+
veil.classList.add("wg-in");
|
|
980
|
+
};
|
|
981
|
+
/**
|
|
982
|
+
* Finger swipe trail for touch theater (orientation rotate / device change).
|
|
983
|
+
* opts: { dir: 'left'|'right'|'up'|'down', label?, ms? }
|
|
984
|
+
* Returns duration waited (ms) via Promise when called from page.evaluate async.
|
|
985
|
+
*/
|
|
986
|
+
window.__wgSwipeTrail = (opts) => {
|
|
987
|
+
const o = opts || {};
|
|
988
|
+
const dir = o.dir === "right" || o.dir === "up" || o.dir === "down" ? o.dir : "left";
|
|
989
|
+
const ms = Math.max(280, Math.min(1400, Number(o.ms) || 720));
|
|
990
|
+
const label =
|
|
991
|
+
o.label ||
|
|
992
|
+
(dir === "left" || dir === "right" ? "swipe" : "swipe") +
|
|
993
|
+
(o.hint ? " \\u00b7 " + o.hint : "");
|
|
994
|
+
let layer = document.getElementById("wg-swipe-layer");
|
|
995
|
+
if (layer && layer._wgTimer) {
|
|
996
|
+
clearTimeout(layer._wgTimer);
|
|
997
|
+
layer._wgTimer = null;
|
|
998
|
+
}
|
|
999
|
+
if (layer) layer.remove();
|
|
1000
|
+
layer = document.createElement("div");
|
|
1001
|
+
layer.id = "wg-swipe-layer";
|
|
1002
|
+
layer.setAttribute("data-wg-ui", "1");
|
|
1003
|
+
const finger = document.createElement("div");
|
|
1004
|
+
finger.className = "wg-swipe-finger";
|
|
1005
|
+
const lab = document.createElement("div");
|
|
1006
|
+
lab.className = "wg-swipe-label";
|
|
1007
|
+
lab.textContent = label;
|
|
1008
|
+
layer.appendChild(lab);
|
|
1009
|
+
layer.appendChild(finger);
|
|
1010
|
+
document.documentElement.appendChild(layer);
|
|
1011
|
+
const vw = window.innerWidth || 390;
|
|
1012
|
+
const vh = window.innerHeight || 844;
|
|
1013
|
+
const pad = Math.min(vw, vh) * 0.18;
|
|
1014
|
+
let x0;
|
|
1015
|
+
let y0;
|
|
1016
|
+
let x1;
|
|
1017
|
+
let y1;
|
|
1018
|
+
if (dir === "left") {
|
|
1019
|
+
x0 = vw - pad;
|
|
1020
|
+
x1 = pad;
|
|
1021
|
+
y0 = y1 = vh * 0.52;
|
|
1022
|
+
} else if (dir === "right") {
|
|
1023
|
+
x0 = pad;
|
|
1024
|
+
x1 = vw - pad;
|
|
1025
|
+
y0 = y1 = vh * 0.52;
|
|
1026
|
+
} else if (dir === "up") {
|
|
1027
|
+
x0 = x1 = vw * 0.5;
|
|
1028
|
+
y0 = vh - pad;
|
|
1029
|
+
y1 = pad;
|
|
1030
|
+
} else {
|
|
1031
|
+
x0 = x1 = vw * 0.5;
|
|
1032
|
+
y0 = pad;
|
|
1033
|
+
y1 = vh - pad;
|
|
1034
|
+
}
|
|
1035
|
+
finger.style.left = x0 + "px";
|
|
1036
|
+
finger.style.top = y0 + "px";
|
|
1037
|
+
finger.style.opacity = "1";
|
|
1038
|
+
void layer.offsetWidth;
|
|
1039
|
+
layer.classList.add("wg-in");
|
|
1040
|
+
const steps = 12;
|
|
1041
|
+
const stepMs = Math.floor(ms / steps);
|
|
1042
|
+
let i = 0;
|
|
1043
|
+
const tick = () => {
|
|
1044
|
+
i += 1;
|
|
1045
|
+
const t = Math.min(1, i / steps);
|
|
1046
|
+
const e = t * t * (3 - 2 * t);
|
|
1047
|
+
const x = x0 + (x1 - x0) * e;
|
|
1048
|
+
const y = y0 + (y1 - y0) * e;
|
|
1049
|
+
finger.style.left = x + "px";
|
|
1050
|
+
finger.style.top = y + "px";
|
|
1051
|
+
const dot = document.createElement("div");
|
|
1052
|
+
dot.className = "wg-swipe-dot";
|
|
1053
|
+
dot.style.left = x + "px";
|
|
1054
|
+
dot.style.top = y + "px";
|
|
1055
|
+
dot.style.opacity = String(0.85 - t * 0.55);
|
|
1056
|
+
layer.appendChild(dot);
|
|
1057
|
+
requestAnimationFrame(() => {
|
|
1058
|
+
dot.style.transition = "opacity .45s ease, transform .45s ease";
|
|
1059
|
+
dot.style.opacity = "0";
|
|
1060
|
+
dot.style.transform = "scale(1.8)";
|
|
1061
|
+
});
|
|
1062
|
+
if (i < steps) {
|
|
1063
|
+
layer._wgTimer = setTimeout(tick, stepMs);
|
|
1064
|
+
} else {
|
|
1065
|
+
finger.style.opacity = "0";
|
|
1066
|
+
layer.classList.remove("wg-in");
|
|
1067
|
+
layer._wgTimer = setTimeout(() => {
|
|
1068
|
+
const el = document.getElementById("wg-swipe-layer");
|
|
1069
|
+
if (el) el.remove();
|
|
1070
|
+
}, 380);
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
layer._wgTimer = setTimeout(tick, 40);
|
|
1074
|
+
return ms + 120;
|
|
853
1075
|
};
|
|
854
1076
|
window.__wgClearZoom = () => {
|
|
1077
|
+
// Strip leftover element transforms from older zoom path.
|
|
855
1078
|
document.querySelectorAll("[data-wg-zoomed=\\"1\\"]").forEach((el) => {
|
|
856
1079
|
el.style.removeProperty("transform");
|
|
857
1080
|
el.style.removeProperty("transform-origin");
|
|
@@ -860,21 +1083,116 @@ async function installOverlay(page, title) {
|
|
|
860
1083
|
el.style.removeProperty("position");
|
|
861
1084
|
delete el.dataset.wgZoomed;
|
|
862
1085
|
});
|
|
1086
|
+
// Undo any prior shell-camera experiment - restore device fit scale only.
|
|
1087
|
+
const shell = document.getElementById("wg-device-shell");
|
|
1088
|
+
if (shell && shell.dataset.wgCamBase != null) {
|
|
1089
|
+
shell.style.transform = shell.dataset.wgCamBase;
|
|
1090
|
+
if (shell.dataset.wgCamOrigin) {
|
|
1091
|
+
shell.style.transformOrigin = shell.dataset.wgCamOrigin;
|
|
1092
|
+
} else {
|
|
1093
|
+
shell.style.transformOrigin = "center center";
|
|
1094
|
+
}
|
|
1095
|
+
delete shell.dataset.wgCamBase;
|
|
1096
|
+
delete shell.dataset.wgCamOrigin;
|
|
1097
|
+
delete shell.dataset.wgCamZoom;
|
|
1098
|
+
}
|
|
1099
|
+
window.__wgZoomLevel = 1;
|
|
1100
|
+
window.__wgZoomSel = "";
|
|
1101
|
+
if (window.__wgSetZoomBadge) window.__wgSetZoomBadge(1, "");
|
|
1102
|
+
};
|
|
1103
|
+
const ZOOM_ICO =
|
|
1104
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="#fde68a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
1105
|
+
'<circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>';
|
|
1106
|
+
window.__wgSetZoomBadge = (scale, sel) => {
|
|
1107
|
+
const n = Number(scale);
|
|
1108
|
+
const level = Number.isFinite(n) && n > 0 ? n : 1;
|
|
1109
|
+
let badge = document.getElementById("wg-zoom-badge");
|
|
1110
|
+
if (!badge) {
|
|
1111
|
+
badge = document.createElement("div");
|
|
1112
|
+
badge.id = "wg-zoom-badge";
|
|
1113
|
+
badge.setAttribute("data-wg-ui", "1");
|
|
1114
|
+
document.documentElement.appendChild(badge);
|
|
1115
|
+
}
|
|
1116
|
+
const zoomed = level > 1.001 || level < 0.999;
|
|
1117
|
+
badge.dataset.zoomed = zoomed ? "1" : "0";
|
|
1118
|
+
const txt = level.toFixed(2) + "\\u00d7";
|
|
1119
|
+
badge.innerHTML =
|
|
1120
|
+
'<span class="wg-zoom-ico">' +
|
|
1121
|
+
ZOOM_ICO +
|
|
1122
|
+
'</span><span class="wg-zoom-val">' +
|
|
1123
|
+
txt +
|
|
1124
|
+
"</span>";
|
|
1125
|
+
badge.classList.add("wg-in");
|
|
1126
|
+
window.__wgZoomLevel = level;
|
|
1127
|
+
window.__wgZoomSel = sel || "";
|
|
1128
|
+
};
|
|
1129
|
+
window.__wgEnsureZoomBadge = () => {
|
|
1130
|
+
if (window.__wgSetZoomBadge) {
|
|
1131
|
+
window.__wgSetZoomBadge(window.__wgZoomLevel || 1, window.__wgZoomSel || "");
|
|
1132
|
+
}
|
|
863
1133
|
};
|
|
1134
|
+
window.__wgSetTypingBadge = (on, label) => {
|
|
1135
|
+
let badge = document.getElementById("wg-typing-badge");
|
|
1136
|
+
if (!on) {
|
|
1137
|
+
if (badge) {
|
|
1138
|
+
badge.classList.remove("wg-in");
|
|
1139
|
+
setTimeout(() => {
|
|
1140
|
+
const b = document.getElementById("wg-typing-badge");
|
|
1141
|
+
if (b && !b.classList.contains("wg-in")) b.remove();
|
|
1142
|
+
}, 220);
|
|
1143
|
+
}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (!badge) {
|
|
1147
|
+
badge = document.createElement("div");
|
|
1148
|
+
badge.id = "wg-typing-badge";
|
|
1149
|
+
badge.setAttribute("data-wg-ui", "1");
|
|
1150
|
+
document.documentElement.appendChild(badge);
|
|
1151
|
+
}
|
|
1152
|
+
const text = label ? String(label).slice(0, 40) : "typing";
|
|
1153
|
+
badge.innerHTML =
|
|
1154
|
+
'<span class="wg-ty-dots">\\u25cf\\u25cf\\u25cf</span><span>typing \\u00b7 ' +
|
|
1155
|
+
text.replace(/</g, "<") +
|
|
1156
|
+
"</span>";
|
|
1157
|
+
void badge.offsetWidth;
|
|
1158
|
+
badge.classList.add("wg-in");
|
|
1159
|
+
};
|
|
1160
|
+
/**
|
|
1161
|
+
* Zoom HUD only - never CSS-scale the target or the device shell.
|
|
1162
|
+
* Shell camera zoom fought device-theater fit-scale (broke ring follow
|
|
1163
|
+
* + uneven border-radius). Element scale mangled cart rows. Emphasis
|
|
1164
|
+
* is ring + focus veil; badge still shows authored zoom level.
|
|
1165
|
+
*/
|
|
864
1166
|
window.__wgApplyZoom = (sel, scale) => {
|
|
865
|
-
|
|
1167
|
+
document.querySelectorAll("[data-wg-zoomed=\\"1\\"]").forEach((el) => {
|
|
1168
|
+
el.style.removeProperty("transform");
|
|
1169
|
+
el.style.removeProperty("transform-origin");
|
|
1170
|
+
el.style.removeProperty("transition");
|
|
1171
|
+
el.style.removeProperty("z-index");
|
|
1172
|
+
el.style.removeProperty("position");
|
|
1173
|
+
delete el.dataset.wgZoomed;
|
|
1174
|
+
});
|
|
1175
|
+
const shell = document.getElementById("wg-device-shell");
|
|
1176
|
+
if (shell && shell.dataset.wgCamBase != null) {
|
|
1177
|
+
shell.style.transform = shell.dataset.wgCamBase;
|
|
1178
|
+
shell.style.transformOrigin = shell.dataset.wgCamOrigin || "center center";
|
|
1179
|
+
delete shell.dataset.wgCamBase;
|
|
1180
|
+
delete shell.dataset.wgCamOrigin;
|
|
1181
|
+
delete shell.dataset.wgCamZoom;
|
|
1182
|
+
}
|
|
866
1183
|
const n = Number(scale);
|
|
867
|
-
if (!Number.isFinite(n) || n <= 1.001)
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
el.dataset.wgZoomed = "1";
|
|
1184
|
+
if (!Number.isFinite(n) || n <= 1.001) {
|
|
1185
|
+
window.__wgZoomLevel = 1;
|
|
1186
|
+
window.__wgZoomSel = "";
|
|
1187
|
+
if (window.__wgSetZoomBadge) window.__wgSetZoomBadge(1, "");
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
window.__wgZoomLevel = n;
|
|
1191
|
+
window.__wgZoomSel = sel || "";
|
|
1192
|
+
if (window.__wgSetZoomBadge) window.__wgSetZoomBadge(n, sel || "");
|
|
877
1193
|
};
|
|
1194
|
+
// Mount zoom HUD immediately so demos always show 1.00x.
|
|
1195
|
+
if (window.__wgEnsureZoomBadge) window.__wgEnsureZoomBadge();
|
|
878
1196
|
const POSITIONS = ["left", "center", "right"];
|
|
879
1197
|
const applyPos = (el, pos) => {
|
|
880
1198
|
el.dataset.pos = pos;
|
|
@@ -1160,25 +1478,85 @@ async function installOverlay(page, title) {
|
|
|
1160
1478
|
sync = "clear";
|
|
1161
1479
|
}
|
|
1162
1480
|
|
|
1163
|
-
let
|
|
1481
|
+
let dockKey =
|
|
1482
|
+
(dockState && dockState.id && String(dockState.id).trim()) ||
|
|
1483
|
+
(payload && !Array.isArray(payload) && payload.todoId && String(payload.todoId).trim()) ||
|
|
1484
|
+
"_default";
|
|
1485
|
+
|
|
1164
1486
|
// Drop any in-panel leftover (old builds put #wg-todos in .wg-body).
|
|
1165
1487
|
document.querySelectorAll("#wg-panel #wg-todos").forEach((el) => el.remove());
|
|
1166
1488
|
|
|
1167
|
-
|
|
1489
|
+
const allDocks = () =>
|
|
1490
|
+
[...document.querySelectorAll(".wg-todo-dock, #wg-todo-dock")].filter(
|
|
1491
|
+
(el, i, arr) => arr.indexOf(el) === i,
|
|
1492
|
+
);
|
|
1493
|
+
const relayoutTodoDocks = () => {
|
|
1494
|
+
const docks = allDocks();
|
|
1495
|
+
let top = 72;
|
|
1496
|
+
for (const el of docks) {
|
|
1497
|
+
el.style.top = top + "px";
|
|
1498
|
+
top += Math.max(48, el.getBoundingClientRect().height) + 10;
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
const findDock = (key) => {
|
|
1502
|
+
if (key === "_default") {
|
|
1503
|
+
return (
|
|
1504
|
+
document.querySelector('.wg-todo-dock[data-wg-todo-key="_default"]') ||
|
|
1505
|
+
document.getElementById("wg-todo-dock")
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
const all = document.querySelectorAll(".wg-todo-dock");
|
|
1509
|
+
for (let i = 0; i < all.length; i++) {
|
|
1510
|
+
if (all[i].getAttribute("data-wg-todo-key") === String(key)) return all[i];
|
|
1511
|
+
}
|
|
1512
|
+
return null;
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1515
|
+
// keep + no new payload: do not remove existing docks (PIA #10).
|
|
1168
1516
|
if (sync === "keep") {
|
|
1169
1517
|
if (!dockState && !list.length) return;
|
|
1170
1518
|
// After navigation the DOM may be gone - recreate from carried dock.
|
|
1171
1519
|
sync = "set";
|
|
1172
1520
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1521
|
+
// Default replace=true: one checklist. parallel docks only when replace:false.
|
|
1522
|
+
const replace =
|
|
1523
|
+
!(payload && !Array.isArray(payload) && payload.replace === false) &&
|
|
1524
|
+
!(payload && !Array.isArray(payload) && payload.parallel === true);
|
|
1525
|
+
const hasDock =
|
|
1526
|
+
!!(dockState && Array.isArray(dockState.groups) && dockState.groups.length);
|
|
1527
|
+
// Explicit clear only - never treat "set + dock + empty list" as clear
|
|
1528
|
+
// (that thrash was marks -> blank in a few ms).
|
|
1529
|
+
if (sync === "clear" || (sync === "set" && !hasDock && !list.length)) {
|
|
1530
|
+
const clearId =
|
|
1531
|
+
(payload && !Array.isArray(payload) && payload.todoId && String(payload.todoId).trim()) ||
|
|
1532
|
+
(dockState && dockState.id && String(dockState.id).trim()) ||
|
|
1533
|
+
"";
|
|
1534
|
+
if (clearId && !replace) {
|
|
1535
|
+
const el = findDock(clearId);
|
|
1536
|
+
if (el) el.remove();
|
|
1537
|
+
} else {
|
|
1538
|
+
allDocks().forEach((el) => el.remove());
|
|
1539
|
+
}
|
|
1540
|
+
relayoutTodoDocks();
|
|
1175
1541
|
return;
|
|
1176
1542
|
}
|
|
1177
1543
|
|
|
1544
|
+
if (sync === "set" && replace) {
|
|
1545
|
+
// Drop sibling docks so we never stack duplicates.
|
|
1546
|
+
for (const el of allDocks()) {
|
|
1547
|
+
const key = el.getAttribute("data-wg-todo-key") || "_default";
|
|
1548
|
+
if (key !== dockKey) el.remove();
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1178
1552
|
const POS = ["left", "right"];
|
|
1553
|
+
let dock = findDock(dockKey);
|
|
1179
1554
|
if (!dock) {
|
|
1180
1555
|
dock = document.createElement("div");
|
|
1181
|
-
dock.
|
|
1556
|
+
dock.className = "wg-todo-dock";
|
|
1557
|
+
dock.setAttribute("data-wg-todo-key", dockKey);
|
|
1558
|
+
if (dockKey === "_default") dock.id = "wg-todo-dock";
|
|
1559
|
+
else dock.id = "wg-todo-dock--" + dockKey.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
1182
1560
|
dock.setAttribute("data-wg-ui", "1");
|
|
1183
1561
|
dock.setAttribute("data-wg-modal", "todos");
|
|
1184
1562
|
dock.setAttribute("data-wg-ready", "1");
|
|
@@ -1238,7 +1616,7 @@ async function installOverlay(page, title) {
|
|
|
1238
1616
|
? "bullets"
|
|
1239
1617
|
: "sequential";
|
|
1240
1618
|
return (
|
|
1241
|
-
'<ul
|
|
1619
|
+
'<ul class="wg-todos-list" data-wg-todo-style="' +
|
|
1242
1620
|
st +
|
|
1243
1621
|
'">' +
|
|
1244
1622
|
(items || [])
|
|
@@ -1279,11 +1657,7 @@ async function installOverlay(page, title) {
|
|
|
1279
1657
|
);
|
|
1280
1658
|
};
|
|
1281
1659
|
|
|
1282
|
-
|
|
1283
|
-
(dockState && dockState.id) ||
|
|
1284
|
-
(payload && !Array.isArray(payload) && payload.todoId) ||
|
|
1285
|
-
"";
|
|
1286
|
-
if (dockId) dock.setAttribute("data-wg-todo-id", String(dockId));
|
|
1660
|
+
if (dockKey !== "_default") dock.setAttribute("data-wg-todo-id", dockKey);
|
|
1287
1661
|
else dock.removeAttribute("data-wg-todo-id");
|
|
1288
1662
|
|
|
1289
1663
|
let html = "";
|
|
@@ -1309,6 +1683,7 @@ async function installOverlay(page, title) {
|
|
|
1309
1683
|
html = renderItems(list, "sequential");
|
|
1310
1684
|
}
|
|
1311
1685
|
dock.innerHTML = html;
|
|
1686
|
+
relayoutTodoDocks();
|
|
1312
1687
|
if (window.__wgStampModal) {
|
|
1313
1688
|
window.__wgStampModal(dock, "todos", { ready: true });
|
|
1314
1689
|
}
|
|
@@ -1464,6 +1839,9 @@ async function installOverlay(page, title) {
|
|
|
1464
1839
|
}
|
|
1465
1840
|
window.__wgDevicePreset = "desktop";
|
|
1466
1841
|
window.__wgDeviceOrientation = "landscape";
|
|
1842
|
+
window.__wgDeviceTouch = false;
|
|
1843
|
+
document.documentElement.dataset.wgTouch = "0";
|
|
1844
|
+
if (window.__wgEnsureZoomBadge) window.__wgEnsureZoomBadge();
|
|
1467
1845
|
return;
|
|
1468
1846
|
}
|
|
1469
1847
|
|
|
@@ -1486,6 +1864,9 @@ async function installOverlay(page, title) {
|
|
|
1486
1864
|
setCursor(touch);
|
|
1487
1865
|
window.__wgDevicePreset = preset;
|
|
1488
1866
|
window.__wgDeviceOrientation = orient;
|
|
1867
|
+
window.__wgDeviceTouch = touch;
|
|
1868
|
+
document.documentElement.dataset.wgTouch = touch ? "1" : "0";
|
|
1869
|
+
if (window.__wgEnsureZoomBadge) window.__wgEnsureZoomBadge();
|
|
1489
1870
|
|
|
1490
1871
|
const announce =
|
|
1491
1872
|
payload.announce === true ||
|
|
@@ -1517,6 +1898,20 @@ async function installOverlay(page, title) {
|
|
|
1517
1898
|
{ title, favicon: WAYGRAPH_FAVICON, bannerPos, todoPos, envAutoplay },
|
|
1518
1899
|
)
|
|
1519
1900
|
.catch(() => {});
|
|
1901
|
+
// Navigations wipe #wg-device-shell - rebuild when video stage + device are live.
|
|
1902
|
+
// If we already shuttered to desktop-flat, re-apply FLAT (never re-add bezel).
|
|
1903
|
+
if (
|
|
1904
|
+
page.__wgVideoViewport &&
|
|
1905
|
+
page.__wgDeviceShell &&
|
|
1906
|
+
page.__wgDeviceShell.width &&
|
|
1907
|
+
page.__wgDeviceShell.height
|
|
1908
|
+
) {
|
|
1909
|
+
await applyVideoDeviceStage(page, page.__wgDeviceShell, page.__wgVideoViewport, {
|
|
1910
|
+
clear: false,
|
|
1911
|
+
shutterOut: !!page.__wgDesktopFlat,
|
|
1912
|
+
desktopFlat: !!page.__wgDesktopFlat,
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1520
1915
|
}
|
|
1521
1916
|
|
|
1522
1917
|
async function renderBeforeStep(page, info) {
|
|
@@ -1653,13 +2048,22 @@ async function renderBeforeStep(page, info) {
|
|
|
1653
2048
|
}
|
|
1654
2049
|
if (window.__wgSyncTodos) {
|
|
1655
2050
|
const sync = info.todoSync || (info.todos && info.todos.length ? "set" : "keep");
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
2051
|
+
// Prefer dock-only payload. Passing list:[] with a dock used to clear
|
|
2052
|
+
// marks a frame later (empty-list clear path).
|
|
2053
|
+
if (sync === "keep" && !info.todoDock) {
|
|
2054
|
+
/* leave floating dock alone */
|
|
2055
|
+
} else {
|
|
2056
|
+
const payload = {
|
|
2057
|
+
sync: sync,
|
|
2058
|
+
dock: info.todoDock || null,
|
|
2059
|
+
pos: info.todoPos || null,
|
|
2060
|
+
todoId: info.todoId || (info.todoDock && info.todoDock.id) || null,
|
|
2061
|
+
};
|
|
2062
|
+
if (!info.todoDock && info.todos && info.todos.length) {
|
|
2063
|
+
payload.list = info.todos;
|
|
2064
|
+
}
|
|
2065
|
+
window.__wgSyncTodos(payload);
|
|
2066
|
+
}
|
|
1663
2067
|
}
|
|
1664
2068
|
if (window.__wgStampModal) {
|
|
1665
2069
|
window.__wgStampModal(panel, "panel", {
|
|
@@ -1775,21 +2179,73 @@ async function renderBeforeStep(page, info) {
|
|
|
1775
2179
|
}
|
|
1776
2180
|
}
|
|
1777
2181
|
|
|
1778
|
-
|
|
2182
|
+
/**
|
|
2183
|
+
* Scroll a locator into view for demo theater.
|
|
2184
|
+
* Prefer CSS smooth scroll + scrollend wait. Do NOT call Playwright's
|
|
2185
|
+
* scrollIntoViewIfNeeded first - that jumps instantly and makes the
|
|
2186
|
+
* following smooth scroll a no-op (looks like a teleport), especially
|
|
2187
|
+
* noticeable under --fast when dwell is shorter.
|
|
2188
|
+
* Pass opts.instant=true only for skipTheater / blitz.
|
|
2189
|
+
*/
|
|
2190
|
+
async function ensureLocatorInView(locator, opts) {
|
|
2191
|
+
const instant = !!(opts && opts.instant);
|
|
1779
2192
|
try {
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
2193
|
+
if (instant) {
|
|
2194
|
+
await locator.scrollIntoViewIfNeeded().catch(() => {});
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
// Touch mode: if the target is off-screen, show a swipe trail in the
|
|
2198
|
+
// finger direction that would scroll it into view (not device morph).
|
|
2199
|
+
const scrollPlan = await locator
|
|
2200
|
+
.evaluate((el) => {
|
|
2201
|
+
if (!el || typeof el.getBoundingClientRect !== "function") return null;
|
|
2202
|
+
const touch =
|
|
2203
|
+
window.__wgDeviceTouch === true ||
|
|
2204
|
+
document.documentElement.dataset.wgTouch === "1" ||
|
|
2205
|
+
window.__wgDevicePreset === "mobile" ||
|
|
2206
|
+
window.__wgDevicePreset === "tablet";
|
|
2207
|
+
if (!touch) return null;
|
|
2208
|
+
const r = el.getBoundingClientRect();
|
|
2209
|
+
const vw = window.innerWidth || 390;
|
|
2210
|
+
const vh = window.innerHeight || 844;
|
|
2211
|
+
const margin = 48;
|
|
2212
|
+
const below = r.top > vh - margin;
|
|
2213
|
+
const above = r.bottom < margin;
|
|
2214
|
+
const right = r.left > vw - margin;
|
|
2215
|
+
const left = r.right < margin;
|
|
2216
|
+
if (!below && !above && !left && !right) return null;
|
|
2217
|
+
// Finger direction to reveal the target (swipe up => content moves up).
|
|
2218
|
+
let dir = "up";
|
|
2219
|
+
if (below) dir = "up";
|
|
2220
|
+
else if (above) dir = "down";
|
|
2221
|
+
else if (right) dir = "left";
|
|
2222
|
+
else if (left) dir = "right";
|
|
2223
|
+
return { dir, hint: "scroll" };
|
|
2224
|
+
})
|
|
2225
|
+
.catch(() => null);
|
|
2226
|
+
if (scrollPlan && scrollPlan.dir) {
|
|
2227
|
+
demoLog(" swipe trail dir=" + scrollPlan.dir + " hint=scroll");
|
|
2228
|
+
await playSwipeTrail(locator.page(), {
|
|
2229
|
+
dir: scrollPlan.dir,
|
|
2230
|
+
label: "swipe \\u00b7 scroll",
|
|
2231
|
+
hint: "scroll",
|
|
2232
|
+
ms: Number(process.env.WAYGRAPH_SWIPE_MS || 650),
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
await locator
|
|
2236
|
+
.evaluate(async (el) => {
|
|
2237
|
+
if (!el || typeof el.scrollIntoView !== "function") return;
|
|
1786
2238
|
const waitScroll = (node, ms) =>
|
|
1787
2239
|
new Promise((resolve) => {
|
|
1788
2240
|
let done = false;
|
|
1789
2241
|
const finish = () => {
|
|
1790
2242
|
if (done) return;
|
|
1791
2243
|
done = true;
|
|
1792
|
-
|
|
2244
|
+
try {
|
|
2245
|
+
node.removeEventListener("scrollend", finish);
|
|
2246
|
+
} catch {
|
|
2247
|
+
/* ignore */
|
|
2248
|
+
}
|
|
1793
2249
|
clearTimeout(t);
|
|
1794
2250
|
resolve();
|
|
1795
2251
|
};
|
|
@@ -1813,7 +2269,7 @@ async function ensureSelectorInView(page, selector) {
|
|
|
1813
2269
|
}
|
|
1814
2270
|
}
|
|
1815
2271
|
}
|
|
1816
|
-
await waitScroll(document.scrollingElement || document.documentElement,
|
|
2272
|
+
await waitScroll(document.scrollingElement || document.documentElement, 650);
|
|
1817
2273
|
let p = el.parentElement;
|
|
1818
2274
|
while (p && p !== document.documentElement && p !== document.body) {
|
|
1819
2275
|
const st = getComputedStyle(p);
|
|
@@ -1831,36 +2287,66 @@ async function ensureSelectorInView(page, selector) {
|
|
|
1831
2287
|
p.scrollLeft = left;
|
|
1832
2288
|
p.scrollTop = top;
|
|
1833
2289
|
}
|
|
1834
|
-
await waitScroll(p,
|
|
2290
|
+
await waitScroll(p, 650);
|
|
1835
2291
|
}
|
|
1836
2292
|
}
|
|
1837
2293
|
p = p.parentElement;
|
|
1838
2294
|
}
|
|
1839
|
-
}
|
|
2295
|
+
})
|
|
1840
2296
|
.catch(() => {});
|
|
1841
2297
|
} catch {
|
|
1842
2298
|
/* best-effort */
|
|
1843
2299
|
}
|
|
1844
2300
|
}
|
|
1845
2301
|
|
|
1846
|
-
async function
|
|
2302
|
+
async function ensureSelectorInView(page, selector, opts) {
|
|
2303
|
+
await ensureLocatorInView(page.locator(selector).first(), opts);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
async function applyHighlightZoom(page, selector, zoom, zoomOut) {
|
|
1847
2307
|
const z = Number(zoom);
|
|
1848
2308
|
if (!Number.isFinite(z) || z <= 1.001) {
|
|
1849
2309
|
await page.evaluate(() => {
|
|
2310
|
+
window.__wgZoomOutOnHide = true;
|
|
1850
2311
|
if (window.__wgClearZoom) window.__wgClearZoom();
|
|
1851
2312
|
}).catch(() => {});
|
|
1852
2313
|
return;
|
|
1853
2314
|
}
|
|
2315
|
+
demoFixture("zoom", "ctx/ring zoom(" + z + ") zoomOut=" + (zoomOut !== false), selector);
|
|
1854
2316
|
await page
|
|
1855
2317
|
.evaluate(
|
|
1856
|
-
({ sel, scale }) => {
|
|
2318
|
+
({ sel, scale, zoomOut }) => {
|
|
2319
|
+
// false = keep camera after ring hide (ctx.zoomOut(false) / ring.zoomOut).
|
|
2320
|
+
window.__wgZoomOutOnHide = zoomOut !== false;
|
|
1857
2321
|
if (window.__wgApplyZoom) window.__wgApplyZoom(sel, scale);
|
|
1858
2322
|
},
|
|
1859
|
-
{ sel: selector, scale: z },
|
|
2323
|
+
{ sel: selector, scale: z, zoomOut: zoomOut !== false },
|
|
1860
2324
|
)
|
|
1861
2325
|
.catch(() => {});
|
|
1862
2326
|
}
|
|
1863
2327
|
|
|
2328
|
+
async function applyHighlightFocus(page, box, focus) {
|
|
2329
|
+
if (!focus || !box) {
|
|
2330
|
+
await page
|
|
2331
|
+
.evaluate(() => {
|
|
2332
|
+
if (window.__wgClearFocus) window.__wgClearFocus();
|
|
2333
|
+
})
|
|
2334
|
+
.catch(() => {});
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
demoFixture("focus", "ctx/ring focus(true)", {
|
|
2338
|
+
x: Math.round(box.x),
|
|
2339
|
+
y: Math.round(box.y),
|
|
2340
|
+
w: Math.round(box.width),
|
|
2341
|
+
h: Math.round(box.height),
|
|
2342
|
+
});
|
|
2343
|
+
await page
|
|
2344
|
+
.evaluate((b) => {
|
|
2345
|
+
if (window.__wgApplyFocus) window.__wgApplyFocus(b);
|
|
2346
|
+
}, box)
|
|
2347
|
+
.catch(() => {});
|
|
2348
|
+
}
|
|
2349
|
+
|
|
1864
2350
|
function todosHtmlFromInfo(info, escFn) {
|
|
1865
2351
|
const list = info.todos || [];
|
|
1866
2352
|
if (!list.length) return "";
|
|
@@ -1953,6 +2439,11 @@ async function presentFailPanel(page, opts) {
|
|
|
1953
2439
|
size: styled.size,
|
|
1954
2440
|
weight: styled.weight,
|
|
1955
2441
|
zoom: styled.zoom,
|
|
2442
|
+
zoomOut: styled.zoomOut,
|
|
2443
|
+
focus: !!styled.focus,
|
|
2444
|
+
tone: styled.tone,
|
|
2445
|
+
color: styled.color,
|
|
2446
|
+
detail: styled.detail,
|
|
1956
2447
|
};
|
|
1957
2448
|
});
|
|
1958
2449
|
await cycleHighlightRings(page, errHighlights, !!gatesFast, {
|
|
@@ -2002,17 +2493,30 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
|
|
|
2002
2493
|
})
|
|
2003
2494
|
.catch(() => {});
|
|
2004
2495
|
}
|
|
2496
|
+
let lastRingEndedAt = null;
|
|
2005
2497
|
for (let i = 0; i < list.length; i++) {
|
|
2006
2498
|
const h = list[i];
|
|
2499
|
+
const ringStarted = Date.now();
|
|
2007
2500
|
try {
|
|
2008
2501
|
await ensureSelectorInView(page, h.selector);
|
|
2009
|
-
await applyHighlightZoom(page, h.selector, h.zoom);
|
|
2010
|
-
|
|
2011
|
-
|
|
2502
|
+
await applyHighlightZoom(page, h.selector, h.zoom, h.zoomOut);
|
|
2503
|
+
// Prefer live in-page rect (honors device-shell scale) over Playwright box.
|
|
2504
|
+
const box = await page
|
|
2505
|
+
.evaluate((sel) => {
|
|
2506
|
+
const el = document.querySelector(sel);
|
|
2507
|
+
if (!el) return null;
|
|
2508
|
+
const r = el.getBoundingClientRect();
|
|
2509
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
2510
|
+
}, h.selector)
|
|
2511
|
+
.catch(() => null);
|
|
2512
|
+
if (box && box.width > 0 && box.height > 0) {
|
|
2012
2513
|
await showRing(page, box, h.label, h.tone || "planned", {
|
|
2013
2514
|
size: h.size,
|
|
2014
2515
|
weight: h.weight,
|
|
2516
|
+
selector: h.selector,
|
|
2517
|
+
focus: !!h.focus,
|
|
2015
2518
|
});
|
|
2519
|
+
// focus is applied by FollowRing / showRing - no second apply needed
|
|
2016
2520
|
// Do NOT advanceTodoDock by ring index after stubAfter already set
|
|
2017
2521
|
// progress - that reset current back to 0 and the next block looked
|
|
2018
2522
|
// blank. Mid-act Method advance still uses syncTodoDockAdvance.
|
|
@@ -2069,27 +2573,76 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
|
|
|
2069
2573
|
const legacyMs = i === list.length - 1 ? 200 : 900;
|
|
2070
2574
|
const holdMs =
|
|
2071
2575
|
authored != null ? authored : defaultHoldMs != null ? defaultHoldMs : legacyMs;
|
|
2576
|
+
const gap = lastRingEndedAt != null ? ringStarted - lastRingEndedAt : null;
|
|
2577
|
+
const holdExtra =
|
|
2578
|
+
"hold=" +
|
|
2579
|
+
holdMs +
|
|
2580
|
+
"ms" +
|
|
2581
|
+
(authored != null ? " authored" : " default") +
|
|
2582
|
+
" box=" +
|
|
2583
|
+
Math.round(box.x) +
|
|
2584
|
+
"," +
|
|
2585
|
+
Math.round(box.y) +
|
|
2586
|
+
" " +
|
|
2587
|
+
Math.round(box.width) +
|
|
2588
|
+
"x" +
|
|
2589
|
+
Math.round(box.height) +
|
|
2590
|
+
(gap != null ? " gap=" + gap + "ms" : "");
|
|
2591
|
+
demoHighlight(h, holdExtra);
|
|
2592
|
+
demoLog(
|
|
2593
|
+
" highlight " +
|
|
2594
|
+
(i + 1) +
|
|
2595
|
+
"/" +
|
|
2596
|
+
list.length +
|
|
2597
|
+
(gap != null && gap < 120 ? ansiPaint(ANSI.yellow, " WARN gap<120ms") : "") +
|
|
2598
|
+
(holdMs < 250 && list.length > 1 ? ansiPaint(ANSI.yellow, " WARN short hold") : ""),
|
|
2599
|
+
);
|
|
2072
2600
|
await new Promise((res) => setTimeout(res, holdMs));
|
|
2601
|
+
lastRingEndedAt = Date.now();
|
|
2602
|
+
} else {
|
|
2603
|
+
demoHighlight(h, "SKIP no boundingBox");
|
|
2073
2604
|
}
|
|
2074
|
-
} catch {
|
|
2075
|
-
|
|
2605
|
+
} catch (err) {
|
|
2606
|
+
demoHighlight(
|
|
2607
|
+
h || { selector: h && h.selector, label: "(error)" },
|
|
2608
|
+
"SKIP " + String(err && err.message ? err.message : err).slice(0, 80),
|
|
2609
|
+
);
|
|
2076
2610
|
}
|
|
2077
2611
|
}
|
|
2078
2612
|
if (list.length > 0) {
|
|
2079
2613
|
const last = list[list.length - 1];
|
|
2080
2614
|
await page
|
|
2081
2615
|
.evaluate((h) => {
|
|
2082
|
-
if (!window.__wgPositionRing) return;
|
|
2616
|
+
if (!window.__wgFollowRing && !window.__wgPositionRing) return;
|
|
2617
|
+
if (window.__wgFollowRing) {
|
|
2618
|
+
window.__wgFollowRing(
|
|
2619
|
+
h.selector,
|
|
2620
|
+
h.label,
|
|
2621
|
+
h.tone || "planned",
|
|
2622
|
+
{ size: h.size || "md", weight: h.weight || "normal" },
|
|
2623
|
+
!!h.focus,
|
|
2624
|
+
);
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2083
2627
|
const reposition = () => {
|
|
2084
2628
|
const el = document.querySelector(h.selector);
|
|
2085
2629
|
if (!el) {
|
|
2086
2630
|
if (window.__wgHideRing) window.__wgHideRing();
|
|
2087
2631
|
return;
|
|
2088
2632
|
}
|
|
2089
|
-
|
|
2633
|
+
const box = el.getBoundingClientRect();
|
|
2634
|
+
window.__wgPositionRing(box, h.label, h.tone || "planned", {
|
|
2090
2635
|
size: h.size || "md",
|
|
2091
2636
|
weight: h.weight || "normal",
|
|
2092
2637
|
});
|
|
2638
|
+
if (h.focus && window.__wgApplyFocus) {
|
|
2639
|
+
window.__wgApplyFocus({
|
|
2640
|
+
x: box.x,
|
|
2641
|
+
y: box.y,
|
|
2642
|
+
width: box.width,
|
|
2643
|
+
height: box.height,
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2093
2646
|
};
|
|
2094
2647
|
reposition();
|
|
2095
2648
|
window.__wgRingTrack = reposition;
|
|
@@ -2238,13 +2791,22 @@ async function renderAfterStep(page, info) {
|
|
|
2238
2791
|
}
|
|
2239
2792
|
if (window.__wgSyncTodos) {
|
|
2240
2793
|
const sync = info.todoSync || (info.todos && info.todos.length ? "set" : "keep");
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2794
|
+
// Prefer dock-only payload. Passing list:[] with a dock used to clear
|
|
2795
|
+
// marks a frame later (empty-list clear path).
|
|
2796
|
+
if (sync === "keep" && !info.todoDock) {
|
|
2797
|
+
/* leave floating dock alone */
|
|
2798
|
+
} else {
|
|
2799
|
+
const payload = {
|
|
2800
|
+
sync: sync,
|
|
2801
|
+
dock: info.todoDock || null,
|
|
2802
|
+
pos: info.todoPos || null,
|
|
2803
|
+
todoId: info.todoId || (info.todoDock && info.todoDock.id) || null,
|
|
2804
|
+
};
|
|
2805
|
+
if (!info.todoDock && info.todos && info.todos.length) {
|
|
2806
|
+
payload.list = info.todos;
|
|
2807
|
+
}
|
|
2808
|
+
window.__wgSyncTodos(payload);
|
|
2809
|
+
}
|
|
2248
2810
|
}
|
|
2249
2811
|
if (window.__wgStampModal) {
|
|
2250
2812
|
window.__wgStampModal(panel, "panel", {
|
|
@@ -2510,14 +3072,23 @@ async function markStepRunning(page, _opts) {
|
|
|
2510
3072
|
async function showRing(page, box, label, tone, style) {
|
|
2511
3073
|
const size = normalizeHighlightSize(style && style.size);
|
|
2512
3074
|
const weight = normalizeHighlightWeight(style && style.weight);
|
|
3075
|
+
const selector = style && style.selector ? String(style.selector) : "";
|
|
3076
|
+
const focus = !!(style && style.focus);
|
|
2513
3077
|
await page
|
|
2514
3078
|
.evaluate(
|
|
2515
|
-
({ box, label, tone, size, weight }) => {
|
|
3079
|
+
({ box, label, tone, size, weight, selector, focus }) => {
|
|
3080
|
+
if (selector && window.__wgFollowRing) {
|
|
3081
|
+
window.__wgFollowRing(selector, label, tone || "planned", { size, weight }, focus);
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
if (window.__wgStopRingFollow) window.__wgStopRingFollow();
|
|
2516
3085
|
if (window.__wgPositionRing) {
|
|
2517
3086
|
window.__wgPositionRing(box, label, tone || "planned", { size, weight });
|
|
2518
3087
|
}
|
|
3088
|
+
if (focus && box && window.__wgApplyFocus) window.__wgApplyFocus(box);
|
|
3089
|
+
else if (!focus && window.__wgClearFocus) window.__wgClearFocus();
|
|
2519
3090
|
},
|
|
2520
|
-
{ box, label, tone: tone || "planned", size, weight },
|
|
3091
|
+
{ box, label, tone: tone || "planned", size, weight, selector, focus },
|
|
2521
3092
|
)
|
|
2522
3093
|
.catch(() => {});
|
|
2523
3094
|
}
|
|
@@ -2569,13 +3140,78 @@ async function matchStubIndexForLocator(page, locator, stubs) {
|
|
|
2569
3140
|
return -1;
|
|
2570
3141
|
}
|
|
2571
3142
|
|
|
3143
|
+
/** Flatten dock items in order (sequential groups). */
|
|
3144
|
+
function flattenTodoDockItems(dock) {
|
|
3145
|
+
const items = [];
|
|
3146
|
+
if (!dock || !dock.groups) return items;
|
|
3147
|
+
for (const g of dock.groups) {
|
|
3148
|
+
for (const t of g.items || []) items.push(t);
|
|
3149
|
+
}
|
|
3150
|
+
return items;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
/**
|
|
3154
|
+
* Resolve which todo index a matched stub should advance to.
|
|
3155
|
+
* Prefer stub.todo / stub.slotId / stub.id against item.id; never jump backward.
|
|
3156
|
+
*/
|
|
3157
|
+
function resolveTodoAdvanceIndex(dock, stub, stubIndex) {
|
|
3158
|
+
const items = flattenTodoDockItems(dock);
|
|
3159
|
+
if (!items.length) return stubIndex;
|
|
3160
|
+
const curIdx = items.findIndex((t) => t.current);
|
|
3161
|
+
let idx = stubIndex;
|
|
3162
|
+
const key = stub && (stub.todo || stub.slotId || stub.id);
|
|
3163
|
+
if (key) {
|
|
3164
|
+
const k = String(key).trim().toLowerCase();
|
|
3165
|
+
let found = items.findIndex((t) => t.id && String(t.id).toLowerCase() === k);
|
|
3166
|
+
if (found < 0) {
|
|
3167
|
+
// username -> login-user, password -> login-pass, submit -> login-submit
|
|
3168
|
+
const aliases = {
|
|
3169
|
+
username: ["login-user", "user", "email"],
|
|
3170
|
+
password: ["login-pass", "pass"],
|
|
3171
|
+
submit: ["login-submit", "login", "tap-login"],
|
|
3172
|
+
user: ["login-user", "username"],
|
|
3173
|
+
pass: ["login-pass", "password"],
|
|
3174
|
+
};
|
|
3175
|
+
const alts = aliases[k] || [];
|
|
3176
|
+
for (const a of alts) {
|
|
3177
|
+
found = items.findIndex((t) => t.id && String(t.id).toLowerCase() === a);
|
|
3178
|
+
if (found >= 0) break;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
if (found < 0) {
|
|
3182
|
+
found = items.findIndex(
|
|
3183
|
+
(t) =>
|
|
3184
|
+
(t.id && String(t.id).toLowerCase().includes(k)) ||
|
|
3185
|
+
(t.text && String(t.text).toLowerCase().includes(k)),
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
if (found >= 0) idx = found;
|
|
3189
|
+
}
|
|
3190
|
+
// Monotonic: never retreat the walkthrough.
|
|
3191
|
+
if (curIdx >= 0 && idx < curIdx) idx = curIdx;
|
|
3192
|
+
return idx;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
2572
3195
|
/** Advance sequential todo dock to stubIndex and push to the page (Method act). */
|
|
2573
|
-
async function syncTodoDockAdvance(page, todoDockRef, stubIndex) {
|
|
3196
|
+
async function syncTodoDockAdvance(page, todoDockRef, stubIndex, stub) {
|
|
2574
3197
|
if (!todoDockRef || stubIndex < 0) return;
|
|
2575
3198
|
const prev = todoDockRef.current;
|
|
2576
3199
|
if (!prev) return;
|
|
2577
|
-
const
|
|
3200
|
+
const idx = resolveTodoAdvanceIndex(prev, stub || null, stubIndex);
|
|
3201
|
+
const advanced = advanceTodoDock(prev, idx);
|
|
2578
3202
|
todoDockRef.current = advanced;
|
|
3203
|
+
demoLog(
|
|
3204
|
+
" todo advance -> idx=" +
|
|
3205
|
+
idx +
|
|
3206
|
+
(stub && (stub.slotId || stub.todo)
|
|
3207
|
+
? " slot=" + (stub.todo || stub.slotId)
|
|
3208
|
+
: " stub#" + stubIndex) +
|
|
3209
|
+
' current="' +
|
|
3210
|
+
String(
|
|
3211
|
+
(flattenTodoDockItems(advanced).find((t) => t.current) || {}).text || "",
|
|
3212
|
+
).slice(0, 40) +
|
|
3213
|
+
'"',
|
|
3214
|
+
);
|
|
2579
3215
|
await page
|
|
2580
3216
|
.evaluate((dock) => {
|
|
2581
3217
|
if (window.__wgSyncTodos) {
|
|
@@ -2647,9 +3283,22 @@ async function presentSlides(page, slides, _gate, opts) {
|
|
|
2647
3283
|
if (s.selector) {
|
|
2648
3284
|
try {
|
|
2649
3285
|
await ensureSelectorInView(page, s.selector);
|
|
2650
|
-
await applyHighlightZoom(page, s.selector, s.zoom);
|
|
2651
|
-
const box = await page
|
|
2652
|
-
|
|
3286
|
+
await applyHighlightZoom(page, s.selector, s.zoom, s.zoomOut);
|
|
3287
|
+
const box = await page
|
|
3288
|
+
.evaluate((sel) => {
|
|
3289
|
+
const el = document.querySelector(sel);
|
|
3290
|
+
if (!el) return null;
|
|
3291
|
+
const r = el.getBoundingClientRect();
|
|
3292
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
3293
|
+
}, s.selector)
|
|
3294
|
+
.catch(() => null);
|
|
3295
|
+
if (box && box.width > 0 && box.height > 0) {
|
|
3296
|
+
await showRing(page, box, caption, slideTone, {
|
|
3297
|
+
...slideStyle,
|
|
3298
|
+
selector: s.selector,
|
|
3299
|
+
focus: !!s.focus,
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
2653
3302
|
} catch {
|
|
2654
3303
|
await hideRing(page);
|
|
2655
3304
|
}
|
|
@@ -2882,6 +3531,307 @@ function paceSpeakFields(pace, baseAutoplayMs) {
|
|
|
2882
3531
|
};
|
|
2883
3532
|
}
|
|
2884
3533
|
|
|
3534
|
+
/** Demo CLI chatter (agents use this to ballpark timing / todo layout). Off for WAYGRAPH_JSON. */
|
|
3535
|
+
function demoQuiet() {
|
|
3536
|
+
return process.env.WAYGRAPH_JSON === "1";
|
|
3537
|
+
}
|
|
3538
|
+
function demoColorEnabled() {
|
|
3539
|
+
// FORCE_COLOR wins (agents/CI often set NO_COLOR=1).
|
|
3540
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
|
|
3541
|
+
if (process.env.NO_COLOR === "1" || process.env.NO_COLOR === "true") return false;
|
|
3542
|
+
if (process.env.FORCE_COLOR === "0") return false;
|
|
3543
|
+
return !!(
|
|
3544
|
+
(process.stderr && process.stderr.isTTY) ||
|
|
3545
|
+
(process.stdout && process.stdout.isTTY)
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
const ESC = String.fromCharCode(27);
|
|
3549
|
+
const ANSI = {
|
|
3550
|
+
reset: ESC + "[0m",
|
|
3551
|
+
bold: ESC + "[1m",
|
|
3552
|
+
dim: ESC + "[2m",
|
|
3553
|
+
purple: ESC + "[38;5;141m",
|
|
3554
|
+
blue: ESC + "[38;5;75m",
|
|
3555
|
+
yellow: ESC + "[38;5;220m",
|
|
3556
|
+
red: ESC + "[38;5;203m",
|
|
3557
|
+
green: ESC + "[38;5;114m",
|
|
3558
|
+
gray: ESC + "[38;5;246m",
|
|
3559
|
+
cyan: ESC + "[38;5;87m",
|
|
3560
|
+
magenta: ESC + "[38;5;213m",
|
|
3561
|
+
white: ESC + "[37m",
|
|
3562
|
+
};
|
|
3563
|
+
function ansiPaint(code, text) {
|
|
3564
|
+
if (!demoColorEnabled() || text == null || text === "") return String(text ?? "");
|
|
3565
|
+
return code + String(text) + ANSI.reset;
|
|
3566
|
+
}
|
|
3567
|
+
function toneAnsi(tone) {
|
|
3568
|
+
const t = normalizeHighlightTone(tone);
|
|
3569
|
+
if (t === "info") return ANSI.blue;
|
|
3570
|
+
if (t === "warning") return ANSI.yellow;
|
|
3571
|
+
if (t === "danger") return ANSI.red;
|
|
3572
|
+
if (t === "success") return ANSI.green;
|
|
3573
|
+
if (t === "auto") return ANSI.gray;
|
|
3574
|
+
return ANSI.purple;
|
|
3575
|
+
}
|
|
3576
|
+
function toneName(tone) {
|
|
3577
|
+
const t = normalizeHighlightTone(tone);
|
|
3578
|
+
if (t === "info") return "BLUE";
|
|
3579
|
+
if (t === "warning") return "YELLOW";
|
|
3580
|
+
if (t === "danger") return "RED";
|
|
3581
|
+
if (t === "success") return "GREEN";
|
|
3582
|
+
if (t === "auto") return "GRAY";
|
|
3583
|
+
return "PURPLE";
|
|
3584
|
+
}
|
|
3585
|
+
/** Optional authored CSS color (#rrggbb) -> truecolor ANSI when TTY. */
|
|
3586
|
+
function hexAnsi(hex) {
|
|
3587
|
+
if (!hex || typeof hex !== "string") return null;
|
|
3588
|
+
const m = String(hex).trim().match(/^#?([0-9a-fA-F]{6})$/);
|
|
3589
|
+
if (!m) return null;
|
|
3590
|
+
const n = parseInt(m[1], 16);
|
|
3591
|
+
const r = (n >> 16) & 255;
|
|
3592
|
+
const g = (n >> 8) & 255;
|
|
3593
|
+
const b = n & 255;
|
|
3594
|
+
return ESC + "[38;2;" + r + ";" + g + ";" + b + "m";
|
|
3595
|
+
}
|
|
3596
|
+
function demoTs() {
|
|
3597
|
+
const d = new Date();
|
|
3598
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
3599
|
+
const ms = String(d.getMilliseconds()).padStart(3, "0");
|
|
3600
|
+
return (
|
|
3601
|
+
p(d.getHours()) +
|
|
3602
|
+
":" +
|
|
3603
|
+
p(d.getMinutes()) +
|
|
3604
|
+
":" +
|
|
3605
|
+
p(d.getSeconds()) +
|
|
3606
|
+
"." +
|
|
3607
|
+
ms
|
|
3608
|
+
);
|
|
3609
|
+
}
|
|
3610
|
+
function demoPrefix() {
|
|
3611
|
+
return ansiPaint(ANSI.dim, "[" + demoTs() + "]") + " waygraph demo: ";
|
|
3612
|
+
}
|
|
3613
|
+
function demoLog(msg) {
|
|
3614
|
+
if (demoQuiet()) return;
|
|
3615
|
+
console.log(demoPrefix() + msg);
|
|
3616
|
+
}
|
|
3617
|
+
/** Fixture catch-all: how it was called + value. */
|
|
3618
|
+
function demoFixture(kind, call, value) {
|
|
3619
|
+
if (demoQuiet()) return;
|
|
3620
|
+
const head = ansiPaint(ANSI.cyan, "FIXTURE") + " " + ansiPaint(ANSI.bold, kind);
|
|
3621
|
+
const callPart = call ? " " + ansiPaint(ANSI.dim, call) : "";
|
|
3622
|
+
let val = "";
|
|
3623
|
+
if (value !== undefined) {
|
|
3624
|
+
if (typeof value === "string") val = value;
|
|
3625
|
+
else {
|
|
3626
|
+
try {
|
|
3627
|
+
val = JSON.stringify(value);
|
|
3628
|
+
} catch {
|
|
3629
|
+
val = String(value);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
if (val.length > 220) val = val.slice(0, 217) + "...";
|
|
3633
|
+
val = " = " + ansiPaint(ANSI.white, val);
|
|
3634
|
+
}
|
|
3635
|
+
console.log(demoPrefix() + head + callPart + val);
|
|
3636
|
+
}
|
|
3637
|
+
/** Highlight trigger line: PURPLE · Cart items - Product landed in cart */
|
|
3638
|
+
function demoHighlight(h, extra) {
|
|
3639
|
+
if (demoQuiet()) return;
|
|
3640
|
+
const tone = h && h.tone != null ? h.tone : "planned";
|
|
3641
|
+
const paint = hexAnsi(h && h.color) || toneAnsi(tone);
|
|
3642
|
+
const name = toneName(tone);
|
|
3643
|
+
const label = String((h && h.label) || "").trim() || "(no label)";
|
|
3644
|
+
const sel = h && h.selector ? String(h.selector) : "";
|
|
3645
|
+
const bits = [];
|
|
3646
|
+
if (h && h.focus) bits.push("focus");
|
|
3647
|
+
if (h && h.zoom != null && Number(h.zoom) > 1) bits.push("zoom=" + h.zoom);
|
|
3648
|
+
if (h && h.zoomOut === false) bits.push("zoomOut=false");
|
|
3649
|
+
if (h && h.gesture) bits.push("gesture=" + h.gesture);
|
|
3650
|
+
if (h && h.weight && h.weight !== "normal") bits.push("weight=" + h.weight);
|
|
3651
|
+
if (h && h.size && h.size !== "md") bits.push("size=" + h.size);
|
|
3652
|
+
if (h && h.color) bits.push("color=" + h.color);
|
|
3653
|
+
if (extra) bits.push(extra);
|
|
3654
|
+
const line =
|
|
3655
|
+
ansiPaint(paint, name) +
|
|
3656
|
+
" · " +
|
|
3657
|
+
ansiPaint(paint, label) +
|
|
3658
|
+
(sel ? ansiPaint(ANSI.dim, " sel=" + sel) : "") +
|
|
3659
|
+
(bits.length ? ansiPaint(ANSI.dim, " [" + bits.join(" ") + "]") : "");
|
|
3660
|
+
console.log(demoPrefix() + ansiPaint(ANSI.magenta, "HIGHLIGHT") + " " + line);
|
|
3661
|
+
}
|
|
3662
|
+
function logStubPhaseFixtures(phaseName, phase) {
|
|
3663
|
+
if (demoQuiet() || !phase) return;
|
|
3664
|
+
const tag = "stub." + phaseName;
|
|
3665
|
+
if (phase.title) demoFixture(tag, "ctx.title(...)", JSON.stringify(phase.title));
|
|
3666
|
+
if (phase.banner) demoFixture(tag, "ctx.banner(...)", JSON.stringify(phase.banner));
|
|
3667
|
+
if (phase.deviceSync && phase.deviceSync !== "keep") {
|
|
3668
|
+
demoFixture(
|
|
3669
|
+
tag,
|
|
3670
|
+
phase.deviceSync === "clear" ? "ctx.clearDevice()" : "ctx.device(...)",
|
|
3671
|
+
phase.device
|
|
3672
|
+
? {
|
|
3673
|
+
preset: phase.device.preset,
|
|
3674
|
+
w: phase.device.viewport && phase.device.viewport.width,
|
|
3675
|
+
h: phase.device.viewport && phase.device.viewport.height,
|
|
3676
|
+
orient: phase.device.orientation,
|
|
3677
|
+
touch: !!phase.device.touchMode,
|
|
3678
|
+
}
|
|
3679
|
+
: phase.deviceSync,
|
|
3680
|
+
);
|
|
3681
|
+
}
|
|
3682
|
+
if (phase.zoom != null) demoFixture(tag, "ctx.zoom(" + phase.zoom + ")", phase.zoom + "x");
|
|
3683
|
+
if (phase.zoomOut !== undefined) demoFixture(tag, "ctx.zoomOut(" + !!phase.zoomOut + ")", !!phase.zoomOut);
|
|
3684
|
+
if (phase.todoSync && phase.todoSync !== "keep") {
|
|
3685
|
+
demoFixture(
|
|
3686
|
+
tag,
|
|
3687
|
+
phase.todoSync === "clear" ? "ctx.hideTodos() / clear" : "ctx.todos(...)",
|
|
3688
|
+
phase.todoDock
|
|
3689
|
+
? {
|
|
3690
|
+
id: phase.todoDock.id,
|
|
3691
|
+
pos: phase.todoDock.pos,
|
|
3692
|
+
title: phase.todoDock.title,
|
|
3693
|
+
n: (phase.todos && phase.todos.length) || 0,
|
|
3694
|
+
}
|
|
3695
|
+
: phase.todoSync,
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
const rings = phase.highlights || [];
|
|
3699
|
+
for (let i = 0; i < rings.length; i++) {
|
|
3700
|
+
const h = rings[i];
|
|
3701
|
+
demoFixture(
|
|
3702
|
+
tag,
|
|
3703
|
+
"ctx.ring/highlights[" + i + "]",
|
|
3704
|
+
{
|
|
3705
|
+
selector: h.selector,
|
|
3706
|
+
label: h.label || h.caption,
|
|
3707
|
+
detail: h.detail,
|
|
3708
|
+
tone: h.tone || "planned",
|
|
3709
|
+
color: h.color,
|
|
3710
|
+
focus: !!h.focus,
|
|
3711
|
+
zoom: h.zoom,
|
|
3712
|
+
zoomOut: h.zoomOut,
|
|
3713
|
+
gesture: h.gesture,
|
|
3714
|
+
duration: h.duration,
|
|
3715
|
+
},
|
|
3716
|
+
);
|
|
3717
|
+
}
|
|
3718
|
+
if (phase.slides && phase.slides.length) {
|
|
3719
|
+
demoFixture(tag, "ctx.slides(...)", { n: phase.slides.length });
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
function summarizeTodoDock(dock) {
|
|
3723
|
+
if (!dock) return "todos=none";
|
|
3724
|
+
const id = dock.id || "_default";
|
|
3725
|
+
const pos = dock.pos || "left";
|
|
3726
|
+
const style = dock.style || "sequential";
|
|
3727
|
+
const title = dock.title ? String(dock.title) : "";
|
|
3728
|
+
const items = [];
|
|
3729
|
+
for (const g of dock.groups || []) {
|
|
3730
|
+
for (const t of g.items || []) items.push(t);
|
|
3731
|
+
}
|
|
3732
|
+
const cur = items.find((t) => t.current);
|
|
3733
|
+
const done = items.filter((t) => t.done).length;
|
|
3734
|
+
return (
|
|
3735
|
+
"todos id=" +
|
|
3736
|
+
id +
|
|
3737
|
+
" pos=" +
|
|
3738
|
+
pos +
|
|
3739
|
+
" style=" +
|
|
3740
|
+
style +
|
|
3741
|
+
(title ? ' title="' + title + '"' : "") +
|
|
3742
|
+
" n=" +
|
|
3743
|
+
items.length +
|
|
3744
|
+
" done=" +
|
|
3745
|
+
done +
|
|
3746
|
+
(cur && cur.text ? ' current="' + String(cur.text).slice(0, 48) + '"' : "")
|
|
3747
|
+
);
|
|
3748
|
+
}
|
|
3749
|
+
/** Full todo dump for agents (every group/item id + flags). */
|
|
3750
|
+
function logTodoDockFull(dock, tag) {
|
|
3751
|
+
const prefix = tag ? tag + " " : "";
|
|
3752
|
+
if (!dock) {
|
|
3753
|
+
demoLog(prefix + "todos=none");
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
demoLog(prefix + summarizeTodoDock(dock));
|
|
3757
|
+
const groups = dock.groups || [];
|
|
3758
|
+
if (!groups.length) {
|
|
3759
|
+
demoLog(prefix + " (no groups)");
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
3763
|
+
const g = groups[gi];
|
|
3764
|
+
const gLabel =
|
|
3765
|
+
"group[" +
|
|
3766
|
+
gi +
|
|
3767
|
+
"]" +
|
|
3768
|
+
(g.id ? " id=" + g.id : "") +
|
|
3769
|
+
(g.title ? ' title="' + String(g.title).slice(0, 40) + '"' : "") +
|
|
3770
|
+
(g.style ? " style=" + g.style : "");
|
|
3771
|
+
demoLog(prefix + " " + gLabel);
|
|
3772
|
+
const items = g.items || [];
|
|
3773
|
+
for (let ti = 0; ti < items.length; ti++) {
|
|
3774
|
+
const t = items[ti];
|
|
3775
|
+
const mark = t.done ? "done" : t.current ? "CURRENT" : "pending";
|
|
3776
|
+
demoLog(
|
|
3777
|
+
prefix +
|
|
3778
|
+
" [" +
|
|
3779
|
+
ti +
|
|
3780
|
+
"] " +
|
|
3781
|
+
mark +
|
|
3782
|
+
(t.id ? " id=" + t.id : "") +
|
|
3783
|
+
' text="' +
|
|
3784
|
+
String(t.text || "").slice(0, 80) +
|
|
3785
|
+
'"' +
|
|
3786
|
+
(t.detail ? ' detail="' + String(t.detail).slice(0, 40) + '"' : ""),
|
|
3787
|
+
);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
function summarizeDevice(d) {
|
|
3792
|
+
if (!d) return "device=none";
|
|
3793
|
+
const vp = d.viewport || {};
|
|
3794
|
+
return (
|
|
3795
|
+
"device=" +
|
|
3796
|
+
(d.preset || "?") +
|
|
3797
|
+
" " +
|
|
3798
|
+
(vp.width || "?") +
|
|
3799
|
+
"x" +
|
|
3800
|
+
(vp.height || "?") +
|
|
3801
|
+
" " +
|
|
3802
|
+
(d.orientation || "?") +
|
|
3803
|
+
(d.touchMode ? " touch" : "")
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3806
|
+
async function probeTodoDocksOnPage(page) {
|
|
3807
|
+
return page
|
|
3808
|
+
.evaluate(() => {
|
|
3809
|
+
const docks = [...document.querySelectorAll(".wg-todo-dock, #wg-todo-dock")];
|
|
3810
|
+
return docks.map((el) => {
|
|
3811
|
+
const r = el.getBoundingClientRect();
|
|
3812
|
+
const items = el.querySelectorAll("li");
|
|
3813
|
+
let current = "";
|
|
3814
|
+
items.forEach((li) => {
|
|
3815
|
+
if (li.classList.contains("wg-todo-current")) {
|
|
3816
|
+
current = (li.textContent || "").replace(/\s+/g, " ").trim().slice(0, 48);
|
|
3817
|
+
}
|
|
3818
|
+
});
|
|
3819
|
+
return {
|
|
3820
|
+
key: el.getAttribute("data-wg-todo-key") || el.id || "?",
|
|
3821
|
+
pos: el.dataset.pos || "?",
|
|
3822
|
+
top: Math.round(r.top),
|
|
3823
|
+
left: Math.round(r.left),
|
|
3824
|
+
w: Math.round(r.width),
|
|
3825
|
+
h: Math.round(r.height),
|
|
3826
|
+
onScreen: r.bottom > 0 && r.right > 0 && r.top < innerHeight && r.left < innerWidth,
|
|
3827
|
+
n: items.length,
|
|
3828
|
+
current,
|
|
3829
|
+
};
|
|
3830
|
+
});
|
|
3831
|
+
})
|
|
3832
|
+
.catch(() => []);
|
|
3833
|
+
}
|
|
3834
|
+
|
|
2885
3835
|
async function hideRing(page) {
|
|
2886
3836
|
await page
|
|
2887
3837
|
.evaluate(() => {
|
|
@@ -2908,10 +3858,23 @@ async function teardownOverlay(page) {
|
|
|
2908
3858
|
window.__wgRingTrack = null;
|
|
2909
3859
|
}
|
|
2910
3860
|
window.__wgNarrateOwnsRing = false;
|
|
2911
|
-
for (const id of [
|
|
3861
|
+
for (const id of [
|
|
3862
|
+
"wg-panel",
|
|
3863
|
+
"wg-ring",
|
|
3864
|
+
"wg-ring-label",
|
|
3865
|
+
"wg-cursor",
|
|
3866
|
+
"wg-click-pulse",
|
|
3867
|
+
"wg-banner",
|
|
3868
|
+
"wg-todo-dock",
|
|
3869
|
+
"wg-device-shell",
|
|
3870
|
+
"wg-device-toast",
|
|
3871
|
+
"wg-device-badge",
|
|
3872
|
+
]) {
|
|
2912
3873
|
const el = document.getElementById(id);
|
|
2913
3874
|
if (el) el.remove();
|
|
2914
3875
|
}
|
|
3876
|
+
document.querySelectorAll(".wg-todo-dock").forEach((el) => el.remove());
|
|
3877
|
+
document.documentElement.classList.remove("wg-video-device-stage");
|
|
2915
3878
|
})
|
|
2916
3879
|
.catch(() => {});
|
|
2917
3880
|
}
|
|
@@ -2942,9 +3905,318 @@ async function clickPulseAt(page, x, y, tone) {
|
|
|
2942
3905
|
.catch(() => {});
|
|
2943
3906
|
}
|
|
2944
3907
|
|
|
3908
|
+
/**
|
|
3909
|
+
* Touch swipe trail (mobile/tablet). dir: left|right|up|down.
|
|
3910
|
+
* Returns after the trail animation completes.
|
|
3911
|
+
* Self-contained when overlay helper is not installed yet (device runs before panel).
|
|
3912
|
+
*/
|
|
3913
|
+
async function playSwipeTrail(page, opts) {
|
|
3914
|
+
const o = opts || {};
|
|
3915
|
+
const waitMs = await page
|
|
3916
|
+
.evaluate((payload) => {
|
|
3917
|
+
if (typeof window.__wgSwipeTrail === "function") {
|
|
3918
|
+
return window.__wgSwipeTrail(payload);
|
|
3919
|
+
}
|
|
3920
|
+
// Fallback: inject a one-shot trail without full overlay install.
|
|
3921
|
+
const dir =
|
|
3922
|
+
payload.dir === "right" || payload.dir === "up" || payload.dir === "down"
|
|
3923
|
+
? payload.dir
|
|
3924
|
+
: "left";
|
|
3925
|
+
const ms = Math.max(280, Math.min(1400, Number(payload.ms) || 720));
|
|
3926
|
+
const label = payload.label || "swipe";
|
|
3927
|
+
let layer = document.getElementById("wg-swipe-layer");
|
|
3928
|
+
if (layer) layer.remove();
|
|
3929
|
+
layer = document.createElement("div");
|
|
3930
|
+
layer.id = "wg-swipe-layer";
|
|
3931
|
+
layer.setAttribute("data-wg-ui", "1");
|
|
3932
|
+
layer.style.cssText =
|
|
3933
|
+
"position:fixed;inset:0;z-index:2147483646;pointer-events:none;overflow:hidden;";
|
|
3934
|
+
const finger = document.createElement("div");
|
|
3935
|
+
finger.style.cssText =
|
|
3936
|
+
"position:absolute;width:28px;height:28px;margin:-14px 0 0 -14px;border-radius:50%;" +
|
|
3937
|
+
"background:rgba(15,23,42,.92);border:2px solid #fff;box-shadow:0 4px 16px rgba(0,0,0,.4);";
|
|
3938
|
+
const lab = document.createElement("div");
|
|
3939
|
+
lab.textContent = label;
|
|
3940
|
+
lab.style.cssText =
|
|
3941
|
+
"position:absolute;left:50%;top:18%;transform:translateX(-50%);padding:6px 12px;" +
|
|
3942
|
+
"border-radius:999px;background:rgba(15,23,42,.88);color:#e0f2fe;" +
|
|
3943
|
+
"border:1px solid rgba(56,189,248,.55);font:700 12px/1.2 system-ui,sans-serif;";
|
|
3944
|
+
layer.appendChild(lab);
|
|
3945
|
+
layer.appendChild(finger);
|
|
3946
|
+
document.documentElement.appendChild(layer);
|
|
3947
|
+
const vw = window.innerWidth || 390;
|
|
3948
|
+
const vh = window.innerHeight || 844;
|
|
3949
|
+
const pad = Math.min(vw, vh) * 0.18;
|
|
3950
|
+
let x0;
|
|
3951
|
+
let y0;
|
|
3952
|
+
let x1;
|
|
3953
|
+
let y1;
|
|
3954
|
+
if (dir === "left") {
|
|
3955
|
+
x0 = vw - pad;
|
|
3956
|
+
x1 = pad;
|
|
3957
|
+
y0 = y1 = vh * 0.52;
|
|
3958
|
+
} else if (dir === "right") {
|
|
3959
|
+
x0 = pad;
|
|
3960
|
+
x1 = vw - pad;
|
|
3961
|
+
y0 = y1 = vh * 0.52;
|
|
3962
|
+
} else if (dir === "up") {
|
|
3963
|
+
x0 = x1 = vw * 0.5;
|
|
3964
|
+
y0 = vh - pad;
|
|
3965
|
+
y1 = pad;
|
|
3966
|
+
} else {
|
|
3967
|
+
x0 = x1 = vw * 0.5;
|
|
3968
|
+
y0 = pad;
|
|
3969
|
+
y1 = vh - pad;
|
|
3970
|
+
}
|
|
3971
|
+
finger.style.left = x0 + "px";
|
|
3972
|
+
finger.style.top = y0 + "px";
|
|
3973
|
+
const steps = 12;
|
|
3974
|
+
const stepMs = Math.floor(ms / steps);
|
|
3975
|
+
for (let i = 1; i <= steps; i++) {
|
|
3976
|
+
const t = i / steps;
|
|
3977
|
+
const e = t * t * (3 - 2 * t);
|
|
3978
|
+
const x = x0 + (x1 - x0) * e;
|
|
3979
|
+
const y = y0 + (y1 - y0) * e;
|
|
3980
|
+
setTimeout(() => {
|
|
3981
|
+
finger.style.left = x + "px";
|
|
3982
|
+
finger.style.top = y + "px";
|
|
3983
|
+
const dot = document.createElement("div");
|
|
3984
|
+
dot.style.cssText =
|
|
3985
|
+
"position:absolute;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;" +
|
|
3986
|
+
"background:rgba(147,197,253,.55);border:2px solid rgba(255,255,255,.85);left:" +
|
|
3987
|
+
x +
|
|
3988
|
+
"px;top:" +
|
|
3989
|
+
y +
|
|
3990
|
+
"px;opacity:" +
|
|
3991
|
+
(0.85 - t * 0.55) +
|
|
3992
|
+
";transition:opacity .45s ease,transform .45s ease;";
|
|
3993
|
+
layer.appendChild(dot);
|
|
3994
|
+
requestAnimationFrame(() => {
|
|
3995
|
+
dot.style.opacity = "0";
|
|
3996
|
+
dot.style.transform = "scale(1.8)";
|
|
3997
|
+
});
|
|
3998
|
+
if (i === steps) {
|
|
3999
|
+
setTimeout(() => {
|
|
4000
|
+
const el = document.getElementById("wg-swipe-layer");
|
|
4001
|
+
if (el) el.remove();
|
|
4002
|
+
}, 400);
|
|
4003
|
+
}
|
|
4004
|
+
}, i * stepMs);
|
|
4005
|
+
}
|
|
4006
|
+
return ms + 120;
|
|
4007
|
+
}, {
|
|
4008
|
+
dir: o.dir || "left",
|
|
4009
|
+
label: o.label || "swipe",
|
|
4010
|
+
hint: o.hint || "",
|
|
4011
|
+
ms: o.ms || 720,
|
|
4012
|
+
})
|
|
4013
|
+
.catch(() => 0);
|
|
4014
|
+
const n = Number(waitMs) || 0;
|
|
4015
|
+
if (n > 0) await new Promise((r) => setTimeout(r, n));
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
/**
|
|
4019
|
+
* Fit + center the OS browser window on the device viewport so mobile/tablet
|
|
4020
|
+
* is not stuck top-left inside a maximized frame (Dan 0.13.4).
|
|
4021
|
+
* Desktop clear restores maximized.
|
|
4022
|
+
*/
|
|
4023
|
+
async function fitWindowToDeviceViewport(page, viewport, opts) {
|
|
4024
|
+
const maximize = !!(opts && opts.maximize);
|
|
4025
|
+
let client;
|
|
4026
|
+
try {
|
|
4027
|
+
client = await page.context().newCDPSession(page);
|
|
4028
|
+
const { windowId } = await client.send("Browser.getWindowForTarget");
|
|
4029
|
+
if (maximize) {
|
|
4030
|
+
await client.send("Browser.setWindowBounds", {
|
|
4031
|
+
windowId,
|
|
4032
|
+
bounds: { windowState: "maximized" },
|
|
4033
|
+
});
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4036
|
+
if (!viewport || !viewport.width || !viewport.height) return;
|
|
4037
|
+
const metrics = await page
|
|
4038
|
+
.evaluate(() => {
|
|
4039
|
+
const chromeW = Math.max(0, (window.outerWidth || 0) - (window.innerWidth || 0));
|
|
4040
|
+
const chromeH = Math.max(0, (window.outerHeight || 0) - (window.innerHeight || 0));
|
|
4041
|
+
return {
|
|
4042
|
+
chromeW: Number.isFinite(chromeW) ? chromeW : 0,
|
|
4043
|
+
// First paint after maximize often reports 0 chrome - use a floor.
|
|
4044
|
+
chromeH: chromeH > 20 ? chromeH : 88,
|
|
4045
|
+
screenW: window.screen.availWidth || 1920,
|
|
4046
|
+
screenH: window.screen.availHeight || 1080,
|
|
4047
|
+
};
|
|
4048
|
+
})
|
|
4049
|
+
.catch(() => ({ chromeW: 0, chromeH: 88, screenW: 1920, screenH: 1080 }));
|
|
4050
|
+
// Leave maximized before setting pixel bounds (Chromium ignores size while max).
|
|
4051
|
+
await client.send("Browser.setWindowBounds", {
|
|
4052
|
+
windowId,
|
|
4053
|
+
bounds: { windowState: "normal" },
|
|
4054
|
+
});
|
|
4055
|
+
const winW = Math.min(
|
|
4056
|
+
metrics.screenW,
|
|
4057
|
+
Math.max(320, Math.floor(viewport.width + metrics.chromeW)),
|
|
4058
|
+
);
|
|
4059
|
+
const winH = Math.min(
|
|
4060
|
+
metrics.screenH,
|
|
4061
|
+
Math.max(320, Math.floor(viewport.height + metrics.chromeH)),
|
|
4062
|
+
);
|
|
4063
|
+
const left = Math.max(0, Math.floor((metrics.screenW - winW) / 2));
|
|
4064
|
+
const top = Math.max(0, Math.floor((metrics.screenH - winH) / 2));
|
|
4065
|
+
await client.send("Browser.setWindowBounds", {
|
|
4066
|
+
windowId,
|
|
4067
|
+
bounds: {
|
|
4068
|
+
windowState: "normal",
|
|
4069
|
+
left,
|
|
4070
|
+
top,
|
|
4071
|
+
width: winW,
|
|
4072
|
+
height: winH,
|
|
4073
|
+
},
|
|
4074
|
+
});
|
|
4075
|
+
} catch {
|
|
4076
|
+
/* headless / no window - ignore */
|
|
4077
|
+
} finally {
|
|
4078
|
+
if (client) await client.detach().catch(() => {});
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
/**
|
|
4083
|
+
* Video recording locks Playwright viewport to recordVideo.size. Shrinking
|
|
4084
|
+
* setViewportSize to mobile/tablet letterboxes the .webm top-left. Instead keep
|
|
4085
|
+
* the stage size and center a device-sized #wg-device-shell inside the frame.
|
|
4086
|
+
*/
|
|
4087
|
+
async function applyVideoDeviceStage(page, target, stage, opts) {
|
|
4088
|
+
const clear = !!(opts && opts.clear);
|
|
4089
|
+
const shutterOut = !!(opts && opts.shutterOut);
|
|
4090
|
+
const enterIn = !!(opts && opts.enterIn);
|
|
4091
|
+
const desktopFlat = !!(opts && (opts.desktopFlat || opts.shutterOut));
|
|
4092
|
+
const result = await page
|
|
4093
|
+
.evaluate(
|
|
4094
|
+
({ tw, th, sw, sh, clear, shutterOut, enterIn, desktopFlat }) => {
|
|
4095
|
+
const MATTE = "#0b1220";
|
|
4096
|
+
const unwrap = () => {
|
|
4097
|
+
const shell = document.getElementById("wg-device-shell");
|
|
4098
|
+
if (shell) {
|
|
4099
|
+
const parent = shell.parentNode;
|
|
4100
|
+
if (parent) {
|
|
4101
|
+
while (shell.firstChild) parent.insertBefore(shell.firstChild, shell);
|
|
4102
|
+
}
|
|
4103
|
+
shell.remove();
|
|
4104
|
+
}
|
|
4105
|
+
document.documentElement.classList.remove("wg-video-device-stage");
|
|
4106
|
+
document.documentElement.style.removeProperty("background");
|
|
4107
|
+
const b = document.body;
|
|
4108
|
+
if (b) {
|
|
4109
|
+
b.style.margin = "";
|
|
4110
|
+
b.style.minHeight = "";
|
|
4111
|
+
b.style.display = "";
|
|
4112
|
+
b.style.alignItems = "";
|
|
4113
|
+
b.style.justifyContent = "";
|
|
4114
|
+
b.style.background = "";
|
|
4115
|
+
b.style.overflow = "";
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4118
|
+
if (clear) {
|
|
4119
|
+
unwrap();
|
|
4120
|
+
return { waitMs: 0 };
|
|
4121
|
+
}
|
|
4122
|
+
if (!document.body) return { waitMs: 0 };
|
|
4123
|
+
document.documentElement.classList.add("wg-video-device-stage");
|
|
4124
|
+
document.documentElement.style.background = MATTE;
|
|
4125
|
+
document.body.style.margin = "0";
|
|
4126
|
+
document.body.style.minHeight = "100vh";
|
|
4127
|
+
document.body.style.display = "flex";
|
|
4128
|
+
document.body.style.alignItems = "center";
|
|
4129
|
+
document.body.style.justifyContent = "center";
|
|
4130
|
+
document.body.style.background = MATTE;
|
|
4131
|
+
document.body.style.overflow = "hidden";
|
|
4132
|
+
|
|
4133
|
+
let shell = document.getElementById("wg-device-shell");
|
|
4134
|
+
const created = !shell;
|
|
4135
|
+
if (!shell) {
|
|
4136
|
+
shell = document.createElement("div");
|
|
4137
|
+
shell.id = "wg-device-shell";
|
|
4138
|
+
const move = [];
|
|
4139
|
+
for (const child of [...document.body.childNodes]) {
|
|
4140
|
+
if (
|
|
4141
|
+
child.nodeType === 1 &&
|
|
4142
|
+
child.getAttribute &&
|
|
4143
|
+
child.getAttribute("data-wg-ui") === "1"
|
|
4144
|
+
) {
|
|
4145
|
+
continue;
|
|
4146
|
+
}
|
|
4147
|
+
if (child.nodeType === 1 && child.id === "wg-device-shell") continue;
|
|
4148
|
+
move.push(child);
|
|
4149
|
+
}
|
|
4150
|
+
for (const n of move) shell.appendChild(n);
|
|
4151
|
+
document.body.appendChild(shell);
|
|
4152
|
+
}
|
|
4153
|
+
// Desktop flat: always full-bleed, never reintroduce bezel on overlay rebuild.
|
|
4154
|
+
if (desktopFlat) {
|
|
4155
|
+
shell.classList.remove("wg-shell-enter");
|
|
4156
|
+
shell.classList.add("wg-shell-shutter-out");
|
|
4157
|
+
shell.classList.add("wg-shell-desktop-flat");
|
|
4158
|
+
shell.style.width = sw + "px";
|
|
4159
|
+
shell.style.height = sh + "px";
|
|
4160
|
+
shell.style.maxWidth = sw + "px";
|
|
4161
|
+
shell.style.maxHeight = sh + "px";
|
|
4162
|
+
shell.style.transform = "scale(1)";
|
|
4163
|
+
shell.style.transformOrigin = "center center";
|
|
4164
|
+
shell.style.borderRadius = "0";
|
|
4165
|
+
shell.style.boxShadow = "none";
|
|
4166
|
+
shell.style.border = "none";
|
|
4167
|
+
shell.style.outline = "none";
|
|
4168
|
+
return { waitMs: shutterOut ? 480 : 0 };
|
|
4169
|
+
}
|
|
4170
|
+
// Leaving desktop-flat: clear INLINE radius/shadow overrides so CSS
|
|
4171
|
+
// border-radius:16px applies evenly on all corners again.
|
|
4172
|
+
shell.style.removeProperty("border-radius");
|
|
4173
|
+
shell.style.removeProperty("box-shadow");
|
|
4174
|
+
shell.style.removeProperty("border");
|
|
4175
|
+
shell.style.removeProperty("outline");
|
|
4176
|
+
if (enterIn && (created || shell.classList.contains("wg-shell-shutter-out"))) {
|
|
4177
|
+
shell.classList.remove("wg-shell-shutter-out");
|
|
4178
|
+
shell.classList.remove("wg-shell-desktop-flat");
|
|
4179
|
+
shell.classList.add("wg-shell-enter");
|
|
4180
|
+
void shell.offsetWidth;
|
|
4181
|
+
}
|
|
4182
|
+
const scale = Math.min(sw / tw, sh / th) * 0.92;
|
|
4183
|
+
shell.style.width = tw + "px";
|
|
4184
|
+
shell.style.height = th + "px";
|
|
4185
|
+
shell.style.maxWidth = tw + "px";
|
|
4186
|
+
shell.style.maxHeight = th + "px";
|
|
4187
|
+
shell.style.transform = "scale(" + scale + ")";
|
|
4188
|
+
shell.style.transformOrigin = "center center";
|
|
4189
|
+
shell.classList.remove("wg-shell-shutter-out");
|
|
4190
|
+
shell.classList.remove("wg-shell-desktop-flat");
|
|
4191
|
+
if (enterIn && shell.classList.contains("wg-shell-enter")) {
|
|
4192
|
+
requestAnimationFrame(() => {
|
|
4193
|
+
shell.classList.remove("wg-shell-enter");
|
|
4194
|
+
});
|
|
4195
|
+
return { waitMs: 520 };
|
|
4196
|
+
}
|
|
4197
|
+
shell.classList.remove("wg-shell-enter");
|
|
4198
|
+
return { waitMs: 0 };
|
|
4199
|
+
},
|
|
4200
|
+
{
|
|
4201
|
+
tw: target.width,
|
|
4202
|
+
th: target.height,
|
|
4203
|
+
sw: stage.width,
|
|
4204
|
+
sh: stage.height,
|
|
4205
|
+
clear,
|
|
4206
|
+
shutterOut,
|
|
4207
|
+
enterIn,
|
|
4208
|
+
desktopFlat,
|
|
4209
|
+
},
|
|
4210
|
+
)
|
|
4211
|
+
.catch(() => ({ waitMs: 0 }));
|
|
4212
|
+
const waitMs = result && result.waitMs ? Number(result.waitMs) : 0;
|
|
4213
|
+
if (waitMs > 0) await new Promise((r) => setTimeout(r, waitMs));
|
|
4214
|
+
}
|
|
4215
|
+
|
|
2945
4216
|
/**
|
|
2946
4217
|
* Apply authored device fixture to the live page (0.13+).
|
|
2947
|
-
* Seamless viewport lerp + toast on set/clear
|
|
4218
|
+
* Seamless viewport lerp + centered window + toast on set/clear.
|
|
4219
|
+
* With --video: keep recordVideo stage size and center a device shell in-frame.
|
|
2948
4220
|
*/
|
|
2949
4221
|
async function applyDeviceToPage(page, device, sync) {
|
|
2950
4222
|
const mode = sync || (device ? "set" : "keep");
|
|
@@ -2966,6 +4238,15 @@ async function applyDeviceToPage(page, device, sync) {
|
|
|
2966
4238
|
width: Math.max(200, Math.floor(d.viewport.width)),
|
|
2967
4239
|
height: Math.max(200, Math.floor(d.viewport.height)),
|
|
2968
4240
|
};
|
|
4241
|
+
const videoStage =
|
|
4242
|
+
page.__wgVideoViewport &&
|
|
4243
|
+
page.__wgVideoViewport.width > 0 &&
|
|
4244
|
+
page.__wgVideoViewport.height > 0
|
|
4245
|
+
? {
|
|
4246
|
+
width: Math.floor(page.__wgVideoViewport.width),
|
|
4247
|
+
height: Math.floor(page.__wgVideoViewport.height),
|
|
4248
|
+
}
|
|
4249
|
+
: null;
|
|
2969
4250
|
let from = null;
|
|
2970
4251
|
try {
|
|
2971
4252
|
from = page.viewportSize();
|
|
@@ -2973,6 +4254,8 @@ async function applyDeviceToPage(page, device, sync) {
|
|
|
2973
4254
|
from = null;
|
|
2974
4255
|
}
|
|
2975
4256
|
const announce = mode === "set" || mode === "clear";
|
|
4257
|
+
const toDesktop =
|
|
4258
|
+
mode === "clear" || (d.preset === "desktop" && !d.touchMode);
|
|
2976
4259
|
const shouldAnimate =
|
|
2977
4260
|
announce &&
|
|
2978
4261
|
from &&
|
|
@@ -2980,25 +4263,135 @@ async function applyDeviceToPage(page, device, sync) {
|
|
|
2980
4263
|
from.height > 0 &&
|
|
2981
4264
|
(from.width !== target.width || from.height !== target.height);
|
|
2982
4265
|
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
4266
|
+
// Always clear camera zoom when returning to desktop (video + headed).
|
|
4267
|
+
if (toDesktop) {
|
|
4268
|
+
page.__wgDesktopFlat = true;
|
|
4269
|
+
await page
|
|
4270
|
+
.evaluate(() => {
|
|
4271
|
+
window.__wgZoomOutOnHide = true;
|
|
4272
|
+
if (window.__wgClearZoom) window.__wgClearZoom();
|
|
4273
|
+
})
|
|
4274
|
+
.catch(() => {});
|
|
4275
|
+
} else if (announce) {
|
|
4276
|
+
page.__wgDesktopFlat = false;
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
// Swipe trail is for scrolling (see ensureLocatorInView), not device morph.
|
|
4280
|
+
const nextOrient =
|
|
4281
|
+
d.orientation === "landscape" || d.orientation === "portrait"
|
|
4282
|
+
? d.orientation
|
|
4283
|
+
: target.height > target.width
|
|
4284
|
+
? "portrait"
|
|
4285
|
+
: "landscape";
|
|
4286
|
+
page.__wgDeviceOrient = nextOrient;
|
|
4287
|
+
page.__wgDevicePreset = toDesktop ? "desktop" : d.preset || "";
|
|
4288
|
+
|
|
4289
|
+
if (videoStage) {
|
|
4290
|
+
// Lock Playwright viewport to the recordVideo size so frames fill the
|
|
4291
|
+
// .webm; center the device shell inside (OS window centering alone does not).
|
|
4292
|
+
// Do NOT fitWindow to the device size - that shrinks the viewport below the
|
|
4293
|
+
// recordVideo canvas and Playwright pads the .webm with grey (top-left bias).
|
|
4294
|
+
try {
|
|
4295
|
+
await realSet(videoStage);
|
|
4296
|
+
} catch {
|
|
4297
|
+
/* ignore */
|
|
4298
|
+
}
|
|
4299
|
+
const shellFrom = page.__wgDeviceShell || target;
|
|
4300
|
+
if (toDesktop) {
|
|
4301
|
+
// Zoom already cleared above. Full-bleed desktop shell (no matte frame).
|
|
4302
|
+
// Shutter-out: soft rounded -> radius 0, then flat edge-to-edge.
|
|
4303
|
+
const desk = {
|
|
4304
|
+
width: videoStage.width,
|
|
4305
|
+
height: videoStage.height,
|
|
4306
|
+
};
|
|
4307
|
+
page.__wgDeviceShell = { ...desk };
|
|
4308
|
+
await applyVideoDeviceStage(page, desk, videoStage, {
|
|
4309
|
+
clear: false,
|
|
4310
|
+
shutterOut: true,
|
|
4311
|
+
desktopFlat: true,
|
|
4312
|
+
});
|
|
4313
|
+
try {
|
|
4314
|
+
await realSet(videoStage);
|
|
4315
|
+
} catch {
|
|
4316
|
+
/* ignore */
|
|
4317
|
+
}
|
|
4318
|
+
} else {
|
|
4319
|
+
// Lerp shell size when switching mobile <-> tablet mid-video.
|
|
4320
|
+
const firstDevice = !page.__wgDeviceShell;
|
|
4321
|
+
if (
|
|
4322
|
+
announce &&
|
|
4323
|
+
shellFrom &&
|
|
4324
|
+
(shellFrom.width !== target.width || shellFrom.height !== target.height)
|
|
4325
|
+
) {
|
|
4326
|
+
const steps = 14;
|
|
4327
|
+
const ms = Number(process.env.WAYGRAPH_DEVICE_TRANSITION_MS || 520);
|
|
4328
|
+
const stepMs = Math.max(12, Math.floor(ms / steps));
|
|
4329
|
+
for (let i = 1; i <= steps; i++) {
|
|
4330
|
+
const t = i / steps;
|
|
4331
|
+
const e = t * t * (3 - 2 * t);
|
|
4332
|
+
const mid = {
|
|
4333
|
+
width: Math.round(shellFrom.width + (target.width - shellFrom.width) * e),
|
|
4334
|
+
height: Math.round(shellFrom.height + (target.height - shellFrom.height) * e),
|
|
4335
|
+
};
|
|
4336
|
+
await applyVideoDeviceStage(page, mid, videoStage, {
|
|
4337
|
+
clear: false,
|
|
4338
|
+
enterIn: i === 1 && firstDevice,
|
|
4339
|
+
});
|
|
4340
|
+
if (i < steps) await new Promise((r) => setTimeout(r, stepMs));
|
|
4341
|
+
}
|
|
4342
|
+
} else {
|
|
4343
|
+
await applyVideoDeviceStage(page, target, videoStage, {
|
|
4344
|
+
clear: false,
|
|
4345
|
+
enterIn: firstDevice || announce,
|
|
2994
4346
|
});
|
|
2995
|
-
if (i < steps) await new Promise((r) => setTimeout(r, stepMs));
|
|
2996
4347
|
}
|
|
4348
|
+
page.__wgDeviceShell = { ...target };
|
|
4349
|
+
// Re-assert stage size after any prior non-video fitWindow.
|
|
4350
|
+
try {
|
|
4351
|
+
await realSet(videoStage);
|
|
4352
|
+
} catch {
|
|
4353
|
+
/* ignore */
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
} else {
|
|
4357
|
+
// Unmaximize + center early so the lerp does not sit in the corner of a
|
|
4358
|
+
// maximized frame. Desktop clear maximizes at the end instead.
|
|
4359
|
+
if (!toDesktop) {
|
|
4360
|
+
await fitWindowToDeviceViewport(page, from && from.width ? from : target, {
|
|
4361
|
+
maximize: false,
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
try {
|
|
4366
|
+
if (shouldAnimate) {
|
|
4367
|
+
const steps = 14;
|
|
4368
|
+
const ms = Number(process.env.WAYGRAPH_DEVICE_TRANSITION_MS || 520);
|
|
4369
|
+
const stepMs = Math.max(12, Math.floor(ms / steps));
|
|
4370
|
+
for (let i = 1; i <= steps; i++) {
|
|
4371
|
+
const t = i / steps;
|
|
4372
|
+
const e = t * t * (3 - 2 * t); // smoothstep
|
|
4373
|
+
const mid = {
|
|
4374
|
+
width: Math.round(from.width + (target.width - from.width) * e),
|
|
4375
|
+
height: Math.round(from.height + (target.height - from.height) * e),
|
|
4376
|
+
};
|
|
4377
|
+
await realSet(mid);
|
|
4378
|
+
if (!toDesktop && (i === 1 || i === steps || i % 3 === 0)) {
|
|
4379
|
+
await fitWindowToDeviceViewport(page, mid, { maximize: false });
|
|
4380
|
+
}
|
|
4381
|
+
if (i < steps) await new Promise((r) => setTimeout(r, stepMs));
|
|
4382
|
+
}
|
|
4383
|
+
} else {
|
|
4384
|
+
await realSet(target);
|
|
4385
|
+
}
|
|
4386
|
+
} catch {
|
|
4387
|
+
/* maximized / null viewport hosts may reject - toast still updates */
|
|
4388
|
+
}
|
|
4389
|
+
|
|
4390
|
+
if (toDesktop) {
|
|
4391
|
+
await fitWindowToDeviceViewport(page, target, { maximize: true });
|
|
2997
4392
|
} else {
|
|
2998
|
-
await
|
|
4393
|
+
await fitWindowToDeviceViewport(page, target, { maximize: false });
|
|
2999
4394
|
}
|
|
3000
|
-
} catch {
|
|
3001
|
-
/* maximized / null viewport hosts may reject - toast still updates */
|
|
3002
4395
|
}
|
|
3003
4396
|
|
|
3004
4397
|
// Fire toast near the end of the lerp so it reads as one seamless beat.
|
|
@@ -3079,7 +4472,30 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
3079
4472
|
proto.fill = async function (value, options) {
|
|
3080
4473
|
try {
|
|
3081
4474
|
await installOverlay(page);
|
|
4475
|
+
await this.waitFor({
|
|
4476
|
+
state: "visible",
|
|
4477
|
+
timeout: (options && options.timeout) || 30000,
|
|
4478
|
+
}).catch(() => {});
|
|
4479
|
+
// Smooth scroll before measuring - otherwise off-screen fields
|
|
4480
|
+
// teleport when Playwright's fill actionability scrolls.
|
|
4481
|
+
await ensureLocatorInView(this, { instant: skipTheater() });
|
|
3082
4482
|
const box = await this.boundingBox();
|
|
4483
|
+
let typeLabel = "input";
|
|
4484
|
+
try {
|
|
4485
|
+
const meta = await this.evaluate((el) => {
|
|
4486
|
+
const id = (el.id || "").toLowerCase();
|
|
4487
|
+
const name = (el.getAttribute("name") || "").toLowerCase();
|
|
4488
|
+
const ph = (el.getAttribute("placeholder") || "").toLowerCase();
|
|
4489
|
+
const ty = (el.getAttribute("type") || "text").toLowerCase();
|
|
4490
|
+
if (ty === "password" || /pass/.test(id + name + ph)) return "password";
|
|
4491
|
+
if (/user|email|login/.test(id + name + ph)) return "username";
|
|
4492
|
+
if (ty && ty !== "text") return ty;
|
|
4493
|
+
return id || name || ph || "input";
|
|
4494
|
+
});
|
|
4495
|
+
if (meta) typeLabel = String(meta);
|
|
4496
|
+
} catch {
|
|
4497
|
+
/* ignore */
|
|
4498
|
+
}
|
|
3083
4499
|
if (box && !(await wasJustNarrated(page))) {
|
|
3084
4500
|
// Only trust the "last mem.get()" as THIS fill's source if it
|
|
3085
4501
|
// happened recently - a stale read from several actions ago is
|
|
@@ -3090,19 +4506,46 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
3090
4506
|
const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
|
|
3091
4507
|
const cap = await captionForLocator(page, this, stubs, fallback);
|
|
3092
4508
|
const stubIdx = await matchStubIndexForLocator(page, this, stubs);
|
|
3093
|
-
|
|
4509
|
+
const stub = stubIdx >= 0 ? stubs[stubIdx] : null;
|
|
4510
|
+
await syncTodoDockAdvance(page, todoDockRef, stubIdx, stub);
|
|
3094
4511
|
await moveCursorTo(page, box, cursorMs(500));
|
|
4512
|
+
if (stub) {
|
|
4513
|
+
await applyHighlightZoom(page, stub.selector, stub.zoom, stub.zoomOut);
|
|
4514
|
+
}
|
|
4515
|
+
demoHighlight(
|
|
4516
|
+
{
|
|
4517
|
+
selector: stub && stub.selector,
|
|
4518
|
+
label: cap.label,
|
|
4519
|
+
tone: cap.tone || (stub && stub.tone) || "planned",
|
|
4520
|
+
color: stub && stub.color,
|
|
4521
|
+
focus: !!(stub && stub.focus),
|
|
4522
|
+
zoom: stub && stub.zoom,
|
|
4523
|
+
zoomOut: stub && stub.zoomOut,
|
|
4524
|
+
gesture: stub && stub.gesture,
|
|
4525
|
+
weight: cap.weight,
|
|
4526
|
+
size: cap.size,
|
|
4527
|
+
},
|
|
4528
|
+
"fill",
|
|
4529
|
+
);
|
|
3095
4530
|
await showRing(page, box, cap.label, cap.tone, {
|
|
3096
4531
|
size: cap.size,
|
|
3097
4532
|
weight: cap.weight,
|
|
4533
|
+
selector: stub && stub.selector ? stub.selector : undefined,
|
|
4534
|
+
focus: !!(stub && stub.focus),
|
|
3098
4535
|
});
|
|
3099
4536
|
await dwellMatchedStub(page, this, stubs, pacing);
|
|
3100
4537
|
await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 200));
|
|
4538
|
+
if (cap && cap.label) typeLabel = String(cap.label).slice(0, 40);
|
|
3101
4539
|
} else if (box) {
|
|
3102
4540
|
// Ring/caption already handled by narrate() - still move the
|
|
3103
4541
|
// cursor there, just skip re-showing the ring with a generic label.
|
|
3104
4542
|
await moveCursorTo(page, box, cursorMs(200));
|
|
3105
4543
|
}
|
|
4544
|
+
await page
|
|
4545
|
+
.evaluate((label) => {
|
|
4546
|
+
if (window.__wgSetTypingBadge) window.__wgSetTypingBadge(true, label);
|
|
4547
|
+
}, typeLabel)
|
|
4548
|
+
.catch(() => {});
|
|
3106
4549
|
} catch {
|
|
3107
4550
|
// best-effort - element not visible/attached yet is not this
|
|
3108
4551
|
// instrumentation's problem, the real fill below still runs
|
|
@@ -3121,6 +4564,11 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
3121
4564
|
// handle specially) - fall back to the real, unpatched fill.
|
|
3122
4565
|
result = await originalFill.call(this, value, options);
|
|
3123
4566
|
}
|
|
4567
|
+
await page
|
|
4568
|
+
.evaluate(() => {
|
|
4569
|
+
if (window.__wgSetTypingBadge) window.__wgSetTypingBadge(false);
|
|
4570
|
+
})
|
|
4571
|
+
.catch(() => {});
|
|
3124
4572
|
// Fade the ring back out once this field is actually done, instead
|
|
3125
4573
|
// of leaving it lit until the next Block's own before-panel clears
|
|
3126
4574
|
// it - it was sticking around through the whole rest of the step.
|
|
@@ -3145,6 +4593,9 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
3145
4593
|
state: "visible",
|
|
3146
4594
|
timeout: (options && options.timeout) || 30000,
|
|
3147
4595
|
}).catch(() => {});
|
|
4596
|
+
// Smooth-scroll off-screen targets before cursor/ring. Playwright's
|
|
4597
|
+
// own click scroll is instant and reads as a teleport under --fast.
|
|
4598
|
+
await ensureLocatorInView(this, { instant: skipTheater() });
|
|
3148
4599
|
const box = await this.boundingBox();
|
|
3149
4600
|
const narrated = box ? await wasJustNarrated(page) : false;
|
|
3150
4601
|
if (box && !narrated) {
|
|
@@ -3166,12 +4617,33 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
|
|
|
3166
4617
|
const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
|
|
3167
4618
|
const cap = await captionForLocator(page, this, stubs, fallback);
|
|
3168
4619
|
const stubIdx = await matchStubIndexForLocator(page, this, stubs);
|
|
3169
|
-
|
|
4620
|
+
const stub = stubIdx >= 0 ? stubs[stubIdx] : null;
|
|
4621
|
+
await syncTodoDockAdvance(page, todoDockRef, stubIdx, stub);
|
|
3170
4622
|
clickTone = cap.tone;
|
|
3171
4623
|
clickPoint = await moveCursorTo(page, box, cursorMs(600));
|
|
4624
|
+
if (stub) {
|
|
4625
|
+
await applyHighlightZoom(page, stub.selector, stub.zoom, stub.zoomOut);
|
|
4626
|
+
}
|
|
4627
|
+
demoHighlight(
|
|
4628
|
+
{
|
|
4629
|
+
selector: stub && stub.selector,
|
|
4630
|
+
label: cap.label,
|
|
4631
|
+
tone: cap.tone || (stub && stub.tone) || "planned",
|
|
4632
|
+
color: stub && stub.color,
|
|
4633
|
+
focus: !!(stub && stub.focus),
|
|
4634
|
+
zoom: stub && stub.zoom,
|
|
4635
|
+
zoomOut: stub && stub.zoomOut,
|
|
4636
|
+
gesture: stub && stub.gesture,
|
|
4637
|
+
weight: cap.weight,
|
|
4638
|
+
size: cap.size,
|
|
4639
|
+
},
|
|
4640
|
+
"click",
|
|
4641
|
+
);
|
|
3172
4642
|
await showRing(page, box, cap.label, cap.tone, {
|
|
3173
4643
|
size: cap.size,
|
|
3174
4644
|
weight: cap.weight,
|
|
4645
|
+
selector: stub && stub.selector ? stub.selector : undefined,
|
|
4646
|
+
focus: !!(stub && stub.focus),
|
|
3175
4647
|
});
|
|
3176
4648
|
await dwellMatchedStub(page, this, stubs, pacing);
|
|
3177
4649
|
// "pop for a few seconds" - Dan's own phrase, matching the
|
|
@@ -3468,6 +4940,52 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3468
4940
|
let result;
|
|
3469
4941
|
/** Carry floating todo dock across steps (empty stubBefore must not wipe). */
|
|
3470
4942
|
let lastTodoDock = undefined;
|
|
4943
|
+
/** Multi-todo: every dock keyed by todoId (or "_default"), survives navigation. */
|
|
4944
|
+
const todoDockById = new Map();
|
|
4945
|
+
const dockRegistryKey = (dock) =>
|
|
4946
|
+
dock && dock.id && String(dock.id).trim() ? String(dock.id).trim() : "_default";
|
|
4947
|
+
const rememberTodoDock = (dock, sync, parallel) => {
|
|
4948
|
+
if (sync === "clear") {
|
|
4949
|
+
todoDockById.clear();
|
|
4950
|
+
return;
|
|
4951
|
+
}
|
|
4952
|
+
if (sync === "set" && dock) {
|
|
4953
|
+
if (!parallel) todoDockById.clear();
|
|
4954
|
+
todoDockById.set(dockRegistryKey(dock), dock);
|
|
4955
|
+
}
|
|
4956
|
+
};
|
|
4957
|
+
const syncAllTodoDocks = async (opts) => {
|
|
4958
|
+
const docks = [...todoDockById.values()];
|
|
4959
|
+
const parallel = !!(opts && opts.parallel);
|
|
4960
|
+
if (!docks.length) {
|
|
4961
|
+
await page
|
|
4962
|
+
.evaluate(() => {
|
|
4963
|
+
if (window.__wgSyncTodos) window.__wgSyncTodos({ sync: "clear" });
|
|
4964
|
+
})
|
|
4965
|
+
.catch(() => {});
|
|
4966
|
+
return;
|
|
4967
|
+
}
|
|
4968
|
+
await page
|
|
4969
|
+
.evaluate(
|
|
4970
|
+
({ list, parallel }) => {
|
|
4971
|
+
if (!window.__wgSyncTodos) return;
|
|
4972
|
+
for (let i = 0; i < list.length; i++) {
|
|
4973
|
+
const d = list[i];
|
|
4974
|
+
window.__wgSyncTodos({
|
|
4975
|
+
sync: "set",
|
|
4976
|
+
dock: d,
|
|
4977
|
+
todoId: (d && d.id) || null,
|
|
4978
|
+
pos: (d && d.pos) || null,
|
|
4979
|
+
// First dock replaces; later ones keep siblings when parallel.
|
|
4980
|
+
replace: !(parallel && i > 0),
|
|
4981
|
+
parallel: parallel && i > 0,
|
|
4982
|
+
});
|
|
4983
|
+
}
|
|
4984
|
+
},
|
|
4985
|
+
{ list: docks, parallel },
|
|
4986
|
+
)
|
|
4987
|
+
.catch(() => {});
|
|
4988
|
+
};
|
|
3471
4989
|
/** Carry device / touch fixture across steps (omit = keep, like todos). */
|
|
3472
4990
|
let lastDevice = undefined;
|
|
3473
4991
|
for (let i = 0; i < resolved.length; i++) {
|
|
@@ -3555,6 +5073,13 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3555
5073
|
stubBeforeRef.current = stubBeforePhase.highlights.map((h) =>
|
|
3556
5074
|
applyHighlightStyleDefaults(h, flowStyle),
|
|
3557
5075
|
);
|
|
5076
|
+
logStubPhaseFixtures("before", {
|
|
5077
|
+
...stubBeforePhase,
|
|
5078
|
+
highlights: stubBeforeRef.current.map((h) => ({
|
|
5079
|
+
...h,
|
|
5080
|
+
label: formatHighlightCaption(h),
|
|
5081
|
+
})),
|
|
5082
|
+
});
|
|
3558
5083
|
const appliedBefore = applyTodoPhase(lastTodoDock, {
|
|
3559
5084
|
todoSync: stubBeforePhase.todoSync,
|
|
3560
5085
|
todoDock: stubBeforePhase.todoDock,
|
|
@@ -3563,6 +5088,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3563
5088
|
});
|
|
3564
5089
|
lastTodoDock = appliedBefore.dock;
|
|
3565
5090
|
todoDockRef.current = lastTodoDock;
|
|
5091
|
+
rememberTodoDock(appliedBefore.dock, appliedBefore.sync, !!stubBeforePhase.todoParallel);
|
|
3566
5092
|
const appliedDeviceBefore = applyDevicePhase(lastDevice, {
|
|
3567
5093
|
deviceSync: stubBeforePhase.deviceSync,
|
|
3568
5094
|
device: stubBeforePhase.device,
|
|
@@ -3604,8 +5130,8 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3604
5130
|
});
|
|
3605
5131
|
const paceSpeak = paceSpeakFields(pacing.demoPace, autoplayMs);
|
|
3606
5132
|
if (process.env.WAYGRAPH_JSON !== "1") {
|
|
3607
|
-
|
|
3608
|
-
"
|
|
5133
|
+
demoLog(
|
|
5134
|
+
"step " +
|
|
3609
5135
|
(i + 1) +
|
|
3610
5136
|
"/" +
|
|
3611
5137
|
resolved.length +
|
|
@@ -3615,6 +5141,29 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3615
5141
|
paceSpeak.paceLabel +
|
|
3616
5142
|
(r.episodeNumber ? " · episode " + r.episodeNumber : ""),
|
|
3617
5143
|
);
|
|
5144
|
+
demoLog(
|
|
5145
|
+
" gate~" +
|
|
5146
|
+
autoplayMs +
|
|
5147
|
+
"ms skipTheater=" +
|
|
5148
|
+
!!pacing.skipTheater +
|
|
5149
|
+
" gatesFast=" +
|
|
5150
|
+
!!pacing.gatesFast,
|
|
5151
|
+
);
|
|
5152
|
+
demoLog(" " + summarizeDevice(lastDevice));
|
|
5153
|
+
const phaseZoom =
|
|
5154
|
+
stubBeforePhase.zoom != null && Number(stubBeforePhase.zoom) > 0
|
|
5155
|
+
? Number(stubBeforePhase.zoom)
|
|
5156
|
+
: null;
|
|
5157
|
+
const phaseZoomOut =
|
|
5158
|
+
stubBeforePhase.zoomOut !== undefined ? !!stubBeforePhase.zoomOut : null;
|
|
5159
|
+
demoLog(
|
|
5160
|
+
" zoom=" +
|
|
5161
|
+
(phaseZoom != null ? phaseZoom + "x" : "default") +
|
|
5162
|
+
(phaseZoomOut != null ? " zoomOut=" + phaseZoomOut : "") +
|
|
5163
|
+
" (StubCtx / API; live chip top-left when ring zoom >1)",
|
|
5164
|
+
);
|
|
5165
|
+
logTodoDockFull(lastTodoDock, " before");
|
|
5166
|
+
demoLog(" todoSync=" + appliedBefore.sync);
|
|
3618
5167
|
}
|
|
3619
5168
|
await renderBeforeStep(page, {
|
|
3620
5169
|
index: i,
|
|
@@ -3639,6 +5188,38 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3639
5188
|
todoId: lastTodoDock && lastTodoDock.id,
|
|
3640
5189
|
...paceSpeak,
|
|
3641
5190
|
});
|
|
5191
|
+
// Re-paint dock(s) - renderBeforeStep syncs the active one; this enforces replace.
|
|
5192
|
+
await syncAllTodoDocks({ parallel: !!stubBeforePhase.todoParallel });
|
|
5193
|
+
if (process.env.WAYGRAPH_JSON !== "1") {
|
|
5194
|
+
const docks = await probeTodoDocksOnPage(page);
|
|
5195
|
+
if (!docks.length && lastTodoDock) {
|
|
5196
|
+
demoLog(" WARN todos authored but dock-dom empty (not painted?)");
|
|
5197
|
+
} else if (docks.length) {
|
|
5198
|
+
demoLog(
|
|
5199
|
+
" dock-dom " +
|
|
5200
|
+
docks
|
|
5201
|
+
.map(
|
|
5202
|
+
(d) =>
|
|
5203
|
+
d.key +
|
|
5204
|
+
"@" +
|
|
5205
|
+
d.pos +
|
|
5206
|
+
" " +
|
|
5207
|
+
d.left +
|
|
5208
|
+
"," +
|
|
5209
|
+
d.top +
|
|
5210
|
+
" " +
|
|
5211
|
+
d.w +
|
|
5212
|
+
"x" +
|
|
5213
|
+
d.h +
|
|
5214
|
+
(d.onScreen ? "" : " OFFSCREEN") +
|
|
5215
|
+
" n=" +
|
|
5216
|
+
d.n +
|
|
5217
|
+
(d.current ? ' cur="' + d.current + '"' : ""),
|
|
5218
|
+
)
|
|
5219
|
+
.join(" | "),
|
|
5220
|
+
);
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
3642
5223
|
const edits = await gate();
|
|
3643
5224
|
for (const k of requires) {
|
|
3644
5225
|
if (edits[k.name] !== undefined) {
|
|
@@ -3798,11 +5379,20 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3798
5379
|
let afterTodoPos = overlayTodoPos;
|
|
3799
5380
|
let appliedAfter = { dock: lastTodoDock, sync: "keep" };
|
|
3800
5381
|
let appliedDeviceAfter = { device: lastDevice, sync: "keep" };
|
|
5382
|
+
let afterTodoParallel = false;
|
|
3801
5383
|
if (hasAuthoredStubAfter(r.block, result, fixturesAfter)) {
|
|
3802
5384
|
const afterPhase = await runStubPhase(r.block, "stubAfter", {
|
|
3803
5385
|
out: result,
|
|
3804
5386
|
fixtures: fixturesAfter,
|
|
3805
5387
|
});
|
|
5388
|
+
logStubPhaseFixtures("after", {
|
|
5389
|
+
...afterPhase,
|
|
5390
|
+
highlights: (afterPhase.highlights || []).map((h) => {
|
|
5391
|
+
const styled = applyHighlightStyleDefaults(h, r.highlightStyle);
|
|
5392
|
+
return { ...styled, label: formatHighlightCaption(styled) };
|
|
5393
|
+
}),
|
|
5394
|
+
});
|
|
5395
|
+
afterTodoParallel = !!afterPhase.todoParallel;
|
|
3806
5396
|
appliedAfter = applyTodoPhase(lastTodoDock, {
|
|
3807
5397
|
todoSync: afterPhase.todoSync,
|
|
3808
5398
|
todoDock: afterPhase.todoDock,
|
|
@@ -3811,6 +5401,7 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3811
5401
|
});
|
|
3812
5402
|
lastTodoDock = appliedAfter.dock;
|
|
3813
5403
|
todoDockRef.current = lastTodoDock;
|
|
5404
|
+
rememberTodoDock(appliedAfter.dock, appliedAfter.sync, afterTodoParallel);
|
|
3814
5405
|
appliedDeviceAfter = applyDevicePhase(lastDevice, {
|
|
3815
5406
|
deviceSync: afterPhase.deviceSync,
|
|
3816
5407
|
device: afterPhase.device,
|
|
@@ -3840,6 +5431,11 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3840
5431
|
size: styled.size,
|
|
3841
5432
|
weight: styled.weight,
|
|
3842
5433
|
zoom: styled.zoom,
|
|
5434
|
+
zoomOut: styled.zoomOut,
|
|
5435
|
+
focus: !!styled.focus,
|
|
5436
|
+
tone: styled.tone,
|
|
5437
|
+
color: styled.color,
|
|
5438
|
+
detail: styled.detail,
|
|
3843
5439
|
};
|
|
3844
5440
|
});
|
|
3845
5441
|
} else {
|
|
@@ -3879,8 +5475,52 @@ async function runStepMode(engine, start, end, context, page, mem, resolved, slo
|
|
|
3879
5475
|
? true
|
|
3880
5476
|
: undefined,
|
|
3881
5477
|
});
|
|
5478
|
+
await syncAllTodoDocks({ parallel: afterTodoParallel });
|
|
3882
5479
|
// Ring cycle / stubAfter may have advanced the dock - persist for next block.
|
|
3883
|
-
if (todoDockRef.current)
|
|
5480
|
+
if (todoDockRef.current) {
|
|
5481
|
+
lastTodoDock = todoDockRef.current;
|
|
5482
|
+
rememberTodoDock(todoDockRef.current, "set", afterTodoParallel);
|
|
5483
|
+
}
|
|
5484
|
+
if (process.env.WAYGRAPH_JSON !== "1") {
|
|
5485
|
+
demoLog(
|
|
5486
|
+
" after " +
|
|
5487
|
+
summarizeDevice(lastDevice) +
|
|
5488
|
+
" deviceSync=" +
|
|
5489
|
+
appliedDeviceAfter.sync,
|
|
5490
|
+
);
|
|
5491
|
+
logTodoDockFull(lastTodoDock, " after");
|
|
5492
|
+
demoLog(" after todoSync=" + appliedAfter.sync);
|
|
5493
|
+
const docksAfter = await probeTodoDocksOnPage(page);
|
|
5494
|
+
if (!docksAfter.length && lastTodoDock && appliedAfter.sync !== "clear") {
|
|
5495
|
+
demoLog(" WARN after: todos authored but dock-dom empty");
|
|
5496
|
+
} else if (docksAfter.length) {
|
|
5497
|
+
demoLog(
|
|
5498
|
+
" after dock-dom " +
|
|
5499
|
+
docksAfter
|
|
5500
|
+
.map(
|
|
5501
|
+
(d) =>
|
|
5502
|
+
d.key +
|
|
5503
|
+
"@" +
|
|
5504
|
+
d.pos +
|
|
5505
|
+
" " +
|
|
5506
|
+
d.left +
|
|
5507
|
+
"," +
|
|
5508
|
+
d.top +
|
|
5509
|
+
" " +
|
|
5510
|
+
d.w +
|
|
5511
|
+
"x" +
|
|
5512
|
+
d.h +
|
|
5513
|
+
(d.onScreen ? "" : " OFFSCREEN") +
|
|
5514
|
+
" n=" +
|
|
5515
|
+
d.n +
|
|
5516
|
+
(d.current ? ' cur="' + d.current + '"' : ""),
|
|
5517
|
+
)
|
|
5518
|
+
.join(" | "),
|
|
5519
|
+
);
|
|
5520
|
+
} else if (appliedAfter.sync === "clear") {
|
|
5521
|
+
demoLog(" after dock-dom cleared (expected)");
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
3884
5524
|
await gate();
|
|
3885
5525
|
}
|
|
3886
5526
|
await teardownOverlay(page);
|
|
@@ -4254,12 +5894,12 @@ async function main() {
|
|
|
4254
5894
|
// Without this, STEP mode always launched a different-looking browser than
|
|
4255
5895
|
// headed Engine runs / waygraph-demo.mjs (which already resolve Flatpak).
|
|
4256
5896
|
const executablePath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH || undefined;
|
|
5897
|
+
// Maximized for interactive step, and for fixed video viewport so the
|
|
5898
|
+
// locked record size can actually fit (otherwise .webm gets grey pad).
|
|
4257
5899
|
const browser = await chromium.launch({
|
|
4258
5900
|
headless: !headed,
|
|
4259
5901
|
slowMo,
|
|
4260
|
-
|
|
4261
|
-
// recording uses a locked 16:9 viewport so .webm has no gray letterboxing.
|
|
4262
|
-
args: step && !fixedVideoViewport ? ["--start-maximized"] : [],
|
|
5902
|
+
args: step || fixedVideoViewport ? ["--start-maximized"] : [],
|
|
4263
5903
|
...(executablePath ? { executablePath } : {}),
|
|
4264
5904
|
});
|
|
4265
5905
|
const contextOpts = fixedVideoViewport
|
|
@@ -4285,6 +5925,7 @@ async function main() {
|
|
|
4285
5925
|
if (step) {
|
|
4286
5926
|
const page = await context.newPage();
|
|
4287
5927
|
pageForVideo = page;
|
|
5928
|
+
if (fixedVideoViewport) page.__wgVideoViewport = fixedVideoViewport;
|
|
4288
5929
|
// Step 1's "before" panel used to sit over a blank about:blank page
|
|
4289
5930
|
// until the human clicked Run - show the real destination first.
|
|
4290
5931
|
if (baseURL) {
|
|
@@ -4294,6 +5935,7 @@ async function main() {
|
|
|
4294
5935
|
} else if (jsonReport) {
|
|
4295
5936
|
const page = await context.newPage();
|
|
4296
5937
|
pageForVideo = page;
|
|
5938
|
+
if (fixedVideoViewport) page.__wgVideoViewport = fixedVideoViewport;
|
|
4297
5939
|
if (baseURL) {
|
|
4298
5940
|
await page.goto(baseURL).catch(() => {});
|
|
4299
5941
|
}
|
|
@@ -4304,6 +5946,7 @@ async function main() {
|
|
|
4304
5946
|
} else {
|
|
4305
5947
|
const page = await context.newPage();
|
|
4306
5948
|
pageForVideo = page;
|
|
5949
|
+
if (fixedVideoViewport) page.__wgVideoViewport = fixedVideoViewport;
|
|
4307
5950
|
if (baseURL) {
|
|
4308
5951
|
await page.goto(baseURL).catch(() => {});
|
|
4309
5952
|
}
|
|
@@ -5044,6 +6687,10 @@ function applyRunFlags(flags, opts) {
|
|
|
5044
6687
|
if (flags.nonHeadless) {
|
|
5045
6688
|
process.env.WAYGRAPH_HEADED = "1";
|
|
5046
6689
|
}
|
|
6690
|
+
// Any --video on demo/run: headless unless --non-headless already won.
|
|
6691
|
+
if (flags.video !== undefined && !flags.nonHeadless && process.env.WAYGRAPH_HEADED === undefined) {
|
|
6692
|
+
process.env.WAYGRAPH_HEADED = "0";
|
|
6693
|
+
}
|
|
5047
6694
|
if (flags.baseUrl !== undefined) {
|
|
5048
6695
|
process.env.WAYGRAPH_BASE_URL = flags.baseUrl;
|
|
5049
6696
|
}
|
|
@@ -5115,14 +6762,17 @@ function resolveBaseUrl(projectDir) {
|
|
|
5115
6762
|
return undefined;
|
|
5116
6763
|
}
|
|
5117
6764
|
/**
|
|
5118
|
-
* Friendly demo defaults: STEP
|
|
6765
|
+
* Friendly demo defaults: STEP on; headed only for live watch (no --video).
|
|
6766
|
+
* --video / --auto-play-video => headless unless --non-headless.
|
|
5119
6767
|
* Call after applyRunFlags so explicit flags already won.
|
|
5120
6768
|
*/
|
|
5121
6769
|
function applyDemoDefaults(projectDir) {
|
|
5122
6770
|
if (process.env.WAYGRAPH_STEP === undefined)
|
|
5123
6771
|
process.env.WAYGRAPH_STEP = "1";
|
|
5124
6772
|
if (process.env.WAYGRAPH_STEP === "1" && process.env.WAYGRAPH_HEADED === undefined) {
|
|
5125
|
-
|
|
6773
|
+
// Recording locks headless - a live window fights fixed video viewport /
|
|
6774
|
+
// device shell centering. Watch live only with --non-headless.
|
|
6775
|
+
process.env.WAYGRAPH_HEADED = process.env.WAYGRAPH_VIDEO ? "0" : "1";
|
|
5126
6776
|
}
|
|
5127
6777
|
// Manual Next by default. --auto-next / --auto-play-video flip this.
|
|
5128
6778
|
if (process.env.WAYGRAPH_AUTOPLAY === undefined) {
|
|
@@ -5243,8 +6893,9 @@ Primary (less is more):
|
|
|
5243
6893
|
--todo-left|--todo-right Floating checklist dock side (also ctx.todoPos / WAYGRAPH_TODO_POS)
|
|
5244
6894
|
--ff-expand Expand fastForwardComposeBlock inners as separate steps
|
|
5245
6895
|
--ff-disabled Dispute: expand FF (alias --no-ff); blitz kept on those inners
|
|
5246
|
-
--auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless
|
|
6896
|
+
--auto-play-video Unattended + recorded: --auto-next + --video (+ step); headless
|
|
5247
6897
|
--auto-play-video-head Same, but keep the browser visible (--non-headless)
|
|
6898
|
+
--video [dir] Record .webm (demo: headless unless --non-headless)
|
|
5248
6899
|
--video-viewport WxH Recording size (default demo: 1920x1080)
|
|
5249
6900
|
--title / --base-url
|
|
5250
6901
|
waygraph run [--blocks <flow|file|spec>] Execute (no overlay unless --step)
|
|
@@ -5651,6 +7302,7 @@ Agents shipped: waygraph-planner, waygraph-author, waygraph-healer.
|
|
|
5651
7302
|
applyDemoDefaults(proj);
|
|
5652
7303
|
console.error(`waygraph demo: STEP=${process.env.WAYGRAPH_STEP === "1" ? "on" : "off"}` +
|
|
5653
7304
|
` AUTO_NEXT=${process.env.WAYGRAPH_AUTOPLAY === "1" ? "on" : "off"}` +
|
|
7305
|
+
` HEADED=${process.env.WAYGRAPH_HEADED === "0" ? "off" : "on"}` +
|
|
5654
7306
|
` BASE_URL=${process.env.WAYGRAPH_BASE_URL ?? "(unset)"}` +
|
|
5655
7307
|
(process.env.WAYGRAPH_VIDEO ? ` VIDEO=${process.env.WAYGRAPH_VIDEO}` : "") +
|
|
5656
7308
|
(process.env.WAYGRAPH_TITLE ? ` TITLE=${JSON.stringify(process.env.WAYGRAPH_TITLE)}` : ""));
|