waygraph 0.12.36 → 0.12.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +194 -40
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -711,6 +711,8 @@ async function installOverlay(page, title) {
711
711
  const cursor = document.getElementById("wg-cursor");
712
712
  if (!cursor) return;
713
713
  const travel = ms || 600;
714
+ window.__wgCursorPos = { x: Number(x) || 0, y: Number(y) || 0 };
715
+ window.__wgCursorTravelUntil = Date.now() + (instant ? 0 : travel) + 40;
714
716
  cursor.style.setProperty("--wg-cursor-ms", travel + "ms");
715
717
  if (instant) {
716
718
  const prev = cursor.style.transition;
@@ -811,6 +813,54 @@ async function installOverlay(page, title) {
811
813
  let settled = zoom <= 1;
812
814
  const settleAt = Date.now() + camMs;
813
815
  const style = { size, weight, focus, color };
816
+ // Screen Studio / Screenify: dead zone follow (see followTick).
817
+ const deadFrac = 0.5;
818
+ let lastPanAt = 0;
819
+ const readCursorViewport = () => {
820
+ const cursor = document.getElementById("wg-cursor");
821
+ if (cursor && cursor.style.opacity !== "0") {
822
+ const r = cursor.getBoundingClientRect();
823
+ if (r.width || r.height || r.left || r.top) {
824
+ return { x: r.left, y: r.top };
825
+ }
826
+ }
827
+ if (window.__wgCursorPos) return window.__wgCursorPos;
828
+ return null;
829
+ };
830
+ const followTick = () => {
831
+ if (!followMouse || !settled || window.__wgCameraLock) return;
832
+ if (!window.__wgSetCamera || !window.__wgViewportToDoc) return;
833
+ const cam = window.__wgCamera;
834
+ if (!cam || !(cam.scale > 1.001)) return;
835
+ // Skip while the demo cursor is mid CSS travel - reading a
836
+ // mid-lerp rect makes the camera chase noise and teleport.
837
+ if (window.__wgCursorTravelUntil && Date.now() < window.__wgCursorTravelUntil) {
838
+ return;
839
+ }
840
+ const pos = readCursorViewport();
841
+ if (!pos) return;
842
+ const vw = window.innerWidth;
843
+ const vh = window.innerHeight;
844
+ const dx = pos.x - vw / 2;
845
+ const dy = pos.y - vh / 2;
846
+ const deadX = (vw * deadFrac) / 2;
847
+ const deadY = (vh * deadFrac) / 2;
848
+ let ox = 0;
849
+ let oy = 0;
850
+ if (dx > deadX) ox = dx - deadX;
851
+ else if (dx < -deadX) ox = dx + deadX;
852
+ if (dy > deadY) oy = dy - deadY;
853
+ else if (dy < -deadY) oy = dy + deadY;
854
+ if (ox === 0 && oy === 0) return;
855
+ const now = Date.now();
856
+ if (now - lastPanAt < 120) return;
857
+ lastPanAt = now;
858
+ // Partial gain - never yank the full overflow in one pan.
859
+ const gain = 0.35;
860
+ const nx = cam.cx + (ox / cam.scale) * gain;
861
+ const ny = cam.cy + (oy / cam.scale) * gain;
862
+ window.__wgSetCamera(nx, ny, cam.scale, 280);
863
+ };
814
864
  const tick = () => {
815
865
  const el = document.querySelector(sel);
816
866
  if (el && window.__wgPositionRing) {
@@ -823,32 +873,34 @@ async function installOverlay(page, title) {
823
873
  );
824
874
  }
825
875
  if (!settled && Date.now() >= settleAt) settled = true;
876
+ followTick();
826
877
  window.__wgRingRaf = requestAnimationFrame(tick);
827
878
  };
828
879
  if (zoom > 1 && window.__wgSetCamera) {
829
880
  const el0 = document.querySelector(sel);
830
881
  if (el0) {
831
- const r0 = el0.getBoundingClientRect();
832
882
  const already =
833
883
  window.__wgCamera && window.__wgCamera.scale > 1.001;
834
- // Already zoomed (zoomOut:false chain): short pan, not full zoom-in.
835
884
  const dur = already ? Math.min(camMs, 420) : camMs;
836
- window.__wgSetCamera(
837
- r0.left + r0.width / 2,
838
- r0.top + r0.height / 2,
839
- zoom,
840
- dur,
841
- );
885
+ // Focus must be document-space, not raw getBoundingClientRect
886
+ // (viewport) - otherwise already-zoomed pans fight the cursor.
887
+ const pt = window.__wgElDocPoint
888
+ ? window.__wgElDocPoint(el0)
889
+ : {
890
+ x: el0.getBoundingClientRect().left + el0.getBoundingClientRect().width / 2,
891
+ y: el0.getBoundingClientRect().top + el0.getBoundingClientRect().height / 2,
892
+ };
893
+ window.__wgCameraLock = true;
894
+ window.__wgSetCamera(pt.x, pt.y, zoom, dur);
895
+ setTimeout(() => {
896
+ window.__wgCameraLock = false;
897
+ }, dur + 40);
842
898
  }
843
899
  }
844
900
  window.__wgRingRaf = requestAnimationFrame(tick);
901
+ // Real mouse only seeds cursor pos; dead-zone follow runs in RAF.
845
902
  const onMove = (ev) => {
846
- if (!followMouse || !settled) return;
847
- if (!window.__wgSetCamera) return;
848
- const cam = window.__wgCamera;
849
- const scale = (cam && cam.scale > 1 ? cam.scale : zoom) || zoom;
850
- if (!(scale > 1)) return;
851
- window.__wgSetCamera(ev.clientX, ev.clientY, scale, 140);
903
+ window.__wgCursorPos = { x: ev.clientX, y: ev.clientY };
852
904
  };
853
905
  window.addEventListener("mousemove", onMove, { passive: true });
854
906
  window.__wgRingTrack = onMove;
@@ -893,7 +945,34 @@ async function installOverlay(page, title) {
893
945
  // rings; only resetCameraZoom / teardown call __wgClearZoom.
894
946
  };
895
947
  // Screen Studio-style camera: scale <body>, keep overlays on <html> fixed.
896
- window.__wgCamera = { scale: 1, cx: 0, cy: 0 };
948
+ // Focus (cx,cy) is DOCUMENT space (pre-transform). Viewport mouse/rects
949
+ // must go through __wgViewportToDoc before SetCamera.
950
+ window.__wgCamera = {
951
+ scale: 1,
952
+ cx: window.innerWidth / 2,
953
+ cy: window.innerHeight / 2,
954
+ };
955
+ window.__wgCameraLock = false;
956
+ window.__wgViewportToDoc = (vx, vy) => {
957
+ const cam = window.__wgCamera || {
958
+ scale: 1,
959
+ cx: window.innerWidth / 2,
960
+ cy: window.innerHeight / 2,
961
+ };
962
+ const s = cam.scale > 1.001 ? cam.scale : 1;
963
+ if (s <= 1.001) return { x: vx, y: vy };
964
+ return {
965
+ x: (vx - window.innerWidth / 2) / s + cam.cx,
966
+ y: (vy - window.innerHeight / 2) / s + cam.cy,
967
+ };
968
+ };
969
+ window.__wgElDocPoint = (el) => {
970
+ const r = el.getBoundingClientRect();
971
+ return window.__wgViewportToDoc(
972
+ r.left + r.width / 2,
973
+ r.top + r.height / 2,
974
+ );
975
+ };
897
976
  window.__wgSetCamera = (fx, fy, scale, ms) => {
898
977
  const body = document.body;
899
978
  if (!body) return;
@@ -961,7 +1040,7 @@ async function installOverlay(page, title) {
961
1040
  '<div class="wg-todo-head">Checklist</div><ul>' + rows + '</ul>';
962
1041
  modal.classList.add("wg-todo-on");
963
1042
  };
964
- window.__wgResetCamera = () => {
1043
+ window.__wgResetCamera = () => {
965
1044
  window.__wgSetCamera(window.innerWidth / 2, window.innerHeight / 2, 1);
966
1045
  };
967
1046
  window.__wgClearZoom = () => {
@@ -1413,13 +1492,17 @@ async function applyCameraZoom(page, box, zoom, ms) {
1413
1492
  }).catch(() => {});
1414
1493
  return;
1415
1494
  }
1416
- const cx = box.x + box.width / 2;
1417
- const cy = box.y + box.height / 2;
1495
+ const vx = box.x + box.width / 2;
1496
+ const vy = box.y + box.height / 2;
1418
1497
  const dur = ms != null ? ms : 650;
1419
1498
  await page
1420
- .evaluate(({ cx, cy, scale, dur }) => {
1421
- if (window.__wgSetCamera) window.__wgSetCamera(cx, cy, scale, dur);
1422
- }, { cx, cy, scale: z, dur })
1499
+ .evaluate(({ vx, vy, scale, dur }) => {
1500
+ if (!window.__wgSetCamera) return;
1501
+ const pt = window.__wgViewportToDoc
1502
+ ? window.__wgViewportToDoc(vx, vy)
1503
+ : { x: vx, y: vy };
1504
+ window.__wgSetCamera(pt.x, pt.y, scale, dur);
1505
+ }, { vx, vy, scale: z, dur })
1423
1506
  .catch(() => {});
1424
1507
  }
1425
1508
 
@@ -1553,16 +1636,9 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1553
1636
  : h.follow !== undefined
1554
1637
  ? !!h.follow
1555
1638
  : z > 1;
1556
- const cx = box.x + box.width / 2;
1557
- const cy = box.y + box.height / 2;
1558
- if (h.cursor !== false) {
1559
- await page
1560
- .evaluate(({ cx, cy, camMs }) => {
1561
- if (window.__wgMoveCursorTo) window.__wgMoveCursorTo(cx, cy, camMs, false);
1562
- }, { cx, cy, camMs })
1563
- .catch(() => {});
1564
- }
1565
- // Track first: ring rides visual rect through the zoom (no re-adjust).
1639
+ // Track/zoom first, then park the demo cursor on the LIVE rect.
1640
+ // Placing the cursor on a pre-zoom boundingBox leaves it stranded after
1641
+ // body scale (html cursor is outside the transform).
1566
1642
  await page
1567
1643
  .evaluate(
1568
1644
  ({ sel, label, tone, size, weight, focus, color, zoom, followMouse, camMs }) => {
@@ -1600,7 +1676,21 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1600
1676
  .catch(() => false);
1601
1677
  const waitMs = alreadyZoomed ? Math.min(camMs, 420) : camMs;
1602
1678
  await new Promise((res) => setTimeout(res, waitMs));
1603
-
1679
+ if (h.cursor !== false) {
1680
+ await page
1681
+ .evaluate(({ sel, ms }) => {
1682
+ const el = document.querySelector(sel);
1683
+ if (!el || !window.__wgMoveCursorTo) return;
1684
+ window.__wgCameraLock = true;
1685
+ const r = el.getBoundingClientRect();
1686
+ window.__wgMoveCursorTo(r.left + r.width / 2, r.top + r.height / 2, ms, false);
1687
+ setTimeout(() => {
1688
+ window.__wgCameraLock = false;
1689
+ }, ms + 40);
1690
+ }, { sel: h.selector, ms: Math.min(280, camMs) })
1691
+ .catch(() => {});
1692
+ await new Promise((res) => setTimeout(res, Math.min(280, camMs)));
1693
+ }
1604
1694
  // Appear settle
1605
1695
  await new Promise((res) => setTimeout(res, fadeMs));
1606
1696
  const authored = resolveFixtureDwellMs(h, { gatesFast, pace });
@@ -1623,6 +1713,14 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1623
1713
  const last = list.length > 0 ? list[list.length - 1] : null;
1624
1714
  if (!last || last.zoomOut !== false) {
1625
1715
  await resetCameraZoom(page);
1716
+ } else {
1717
+ // Hold lock into the upcoming act click so follow/scroll cannot yank
1718
+ // the camera off the already-centered target (cart teleport bug).
1719
+ await page
1720
+ .evaluate(() => {
1721
+ window.__wgCameraLock = true;
1722
+ })
1723
+ .catch(() => {});
1626
1724
  }
1627
1725
  if (list.length === 0) {
1628
1726
  await hideRing(page);
@@ -2359,12 +2457,16 @@ async function teardownOverlay(page) {
2359
2457
  .catch(() => {});
2360
2458
  }
2361
2459
 
2362
- async function moveCursorTo(page, box, ms) {
2460
+ async function moveCursorTo(page, box, ms, opts) {
2363
2461
  const x = box.x + box.width / 2;
2364
2462
  const y = box.y + box.height / 2;
2365
2463
  const travel = Math.max(0, Number(ms) || 0);
2464
+ const keepLock = !!(opts && opts.keepLock);
2465
+ // Freeze camera follow while the demo cursor travels - otherwise dead-zone
2466
+ // pans fight the click target (esp. right-edge cart chrome).
2366
2467
  await page
2367
2468
  .evaluate(({ x, y, ms }) => {
2469
+ window.__wgCameraLock = true;
2368
2470
  if (window.__wgMoveCursorTo) window.__wgMoveCursorTo(x, y, ms);
2369
2471
  }, { x, y, ms: travel })
2370
2472
  .catch(() => {});
@@ -2374,6 +2476,13 @@ async function moveCursorTo(page, box, ms) {
2374
2476
  if (travel > 0) {
2375
2477
  await new Promise((res) => setTimeout(res, travel));
2376
2478
  }
2479
+ if (!keepLock) {
2480
+ await page
2481
+ .evaluate(() => {
2482
+ window.__wgCameraLock = false;
2483
+ })
2484
+ .catch(() => {});
2485
+ }
2377
2486
  return { x, y };
2378
2487
  }
2379
2488
 
@@ -2501,6 +2610,7 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2501
2610
  proto.click = async function (options) {
2502
2611
  let clickPoint = null;
2503
2612
  let clickTone = "auto";
2613
+ let keptCameraLock = false;
2504
2614
  try {
2505
2615
  await installOverlay(page);
2506
2616
  // Wait until the target is visible BEFORE measuring - otherwise
@@ -2511,6 +2621,20 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2511
2621
  state: "visible",
2512
2622
  timeout: (options && options.timeout) || 30000,
2513
2623
  }).catch(() => {});
2624
+ // Body-scale camera + Playwright scrollIntoView = teleport storm.
2625
+ // Lock the camera for the whole click when zoomed; force-click so
2626
+ // Playwright does not try to scroll the transformed page.
2627
+ const zoomed = await page
2628
+ .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
2629
+ .catch(() => false);
2630
+ if (zoomed) {
2631
+ await page
2632
+ .evaluate(() => {
2633
+ window.__wgCameraLock = true;
2634
+ })
2635
+ .catch(() => {});
2636
+ keptCameraLock = true;
2637
+ }
2514
2638
  const box = await this.boundingBox();
2515
2639
  const narrated = box ? await wasJustNarrated(page) : false;
2516
2640
  if (box && !narrated) {
@@ -2532,8 +2656,18 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2532
2656
  const stubs = (stubBeforeRef && stubBeforeRef.current) || [];
2533
2657
  const cap = await captionForLocator(page, this, stubs, fallback);
2534
2658
  clickTone = cap.tone;
2535
- clickPoint = await moveCursorTo(page, box, cursorMs(600));
2536
- await showRing(page, box, cap.label, cap.tone, {
2659
+ // Already centered from stubBefore zoom: short cursor, keep lock.
2660
+ const travel = zoomed ? cursorMs(160) : cursorMs(600);
2661
+ clickPoint = await moveCursorTo(page, box, travel, {
2662
+ keepLock: zoomed,
2663
+ });
2664
+ // Re-measure after cursor settle - stale pre-travel box teleports the ring.
2665
+ const liveBox = (await this.boundingBox()) || box;
2666
+ clickPoint = {
2667
+ x: liveBox.x + liveBox.width / 2,
2668
+ y: liveBox.y + liveBox.height / 2,
2669
+ };
2670
+ await showRing(page, liveBox, cap.label, cap.tone, {
2537
2671
  size: cap.size,
2538
2672
  weight: cap.weight,
2539
2673
  });
@@ -2545,7 +2679,10 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2545
2679
  } else if (box) {
2546
2680
  // Ring/caption already handled by narrate() - still move the
2547
2681
  // cursor + pulse the click point, just skip re-showing the ring.
2548
- clickPoint = await moveCursorTo(page, box, cursorMs(200));
2682
+ const travel = zoomed ? cursorMs(120) : cursorMs(200);
2683
+ clickPoint = await moveCursorTo(page, box, travel, {
2684
+ keepLock: zoomed,
2685
+ });
2549
2686
  clickTone = "planned";
2550
2687
  }
2551
2688
  } catch {
@@ -2561,10 +2698,27 @@ function instrumentInteractionHighlighting(page, mem, slowMo, pacing, stubBefore
2561
2698
  // mock click visually completes before the real one fires.
2562
2699
  await new Promise((res) => setTimeout(res, skipTheater() ? 0 : 500));
2563
2700
  }
2564
- const result = await originalClick.call(this, options);
2565
- await new Promise((res) => setTimeout(res, clickPostPop()));
2566
- await hideRing(page);
2567
- return result;
2701
+ const zoomedClick = await page
2702
+ .evaluate(() => !!(window.__wgCamera && window.__wgCamera.scale > 1.001))
2703
+ .catch(() => false);
2704
+ const clickOpts =
2705
+ zoomedClick || keptCameraLock
2706
+ ? { ...(options || {}), force: true }
2707
+ : options;
2708
+ try {
2709
+ const result = await originalClick.call(this, clickOpts);
2710
+ await new Promise((res) => setTimeout(res, clickPostPop()));
2711
+ await hideRing(page);
2712
+ return result;
2713
+ } finally {
2714
+ if (keptCameraLock) {
2715
+ await page
2716
+ .evaluate(() => {
2717
+ window.__wgCameraLock = false;
2718
+ })
2719
+ .catch(() => {});
2720
+ }
2721
+ }
2568
2722
  };
2569
2723
  }
2570
2724
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.36",
3
+ "version": "0.12.38",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",