waygraph 0.12.35 → 0.12.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -711,6 +711,7 @@ 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 };
714
715
  cursor.style.setProperty("--wg-cursor-ms", travel + "ms");
715
716
  if (instant) {
716
717
  const prev = cursor.style.transition;
@@ -811,6 +812,50 @@ async function installOverlay(page, title) {
811
812
  let settled = zoom <= 1;
812
813
  const settleAt = Date.now() + camMs;
813
814
  const style = { size, weight, focus, color };
815
+ // Screen Studio / Screenify: dead zone = center ~40% of viewport.
816
+ // Pan only when the demo cursor leaves that zone - never hard-center
817
+ // on every mousemove (that fights like same-pole magnets).
818
+ const deadFrac = 0.4;
819
+ let lastPanAt = 0;
820
+ const readCursorViewport = () => {
821
+ const cursor = document.getElementById("wg-cursor");
822
+ if (cursor && cursor.style.opacity !== "0") {
823
+ const r = cursor.getBoundingClientRect();
824
+ if (r.width || r.height || r.left || r.top) {
825
+ return { x: r.left, y: r.top };
826
+ }
827
+ }
828
+ if (window.__wgCursorPos) return window.__wgCursorPos;
829
+ return null;
830
+ };
831
+ const followTick = () => {
832
+ if (!followMouse || !settled || window.__wgCameraLock) return;
833
+ if (!window.__wgSetCamera || !window.__wgViewportToDoc) return;
834
+ const cam = window.__wgCamera;
835
+ if (!cam || !(cam.scale > 1.001)) return;
836
+ const pos = readCursorViewport();
837
+ if (!pos) return;
838
+ const vw = window.innerWidth;
839
+ const vh = window.innerHeight;
840
+ const dx = pos.x - vw / 2;
841
+ const dy = pos.y - vh / 2;
842
+ const deadX = (vw * deadFrac) / 2;
843
+ const deadY = (vh * deadFrac) / 2;
844
+ let ox = 0;
845
+ let oy = 0;
846
+ if (dx > deadX) ox = dx - deadX;
847
+ else if (dx < -deadX) ox = dx + deadX;
848
+ if (dy > deadY) oy = dy - deadY;
849
+ else if (dy < -deadY) oy = dy + deadY;
850
+ if (ox === 0 && oy === 0) return;
851
+ const now = Date.now();
852
+ if (now - lastPanAt < 80) return;
853
+ lastPanAt = now;
854
+ // Viewport overflow -> document delta (divide by scale).
855
+ const nx = cam.cx + ox / cam.scale;
856
+ const ny = cam.cy + oy / cam.scale;
857
+ window.__wgSetCamera(nx, ny, cam.scale, 220);
858
+ };
814
859
  const tick = () => {
815
860
  const el = document.querySelector(sel);
816
861
  if (el && window.__wgPositionRing) {
@@ -823,32 +868,34 @@ async function installOverlay(page, title) {
823
868
  );
824
869
  }
825
870
  if (!settled && Date.now() >= settleAt) settled = true;
871
+ followTick();
826
872
  window.__wgRingRaf = requestAnimationFrame(tick);
827
873
  };
828
874
  if (zoom > 1 && window.__wgSetCamera) {
829
875
  const el0 = document.querySelector(sel);
830
876
  if (el0) {
831
- const r0 = el0.getBoundingClientRect();
832
877
  const already =
833
878
  window.__wgCamera && window.__wgCamera.scale > 1.001;
834
- // Already zoomed (zoomOut:false chain): short pan, not full zoom-in.
835
879
  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
- );
880
+ // Focus must be document-space, not raw getBoundingClientRect
881
+ // (viewport) - otherwise already-zoomed pans fight the cursor.
882
+ const pt = window.__wgElDocPoint
883
+ ? window.__wgElDocPoint(el0)
884
+ : {
885
+ x: el0.getBoundingClientRect().left + el0.getBoundingClientRect().width / 2,
886
+ y: el0.getBoundingClientRect().top + el0.getBoundingClientRect().height / 2,
887
+ };
888
+ window.__wgCameraLock = true;
889
+ window.__wgSetCamera(pt.x, pt.y, zoom, dur);
890
+ setTimeout(() => {
891
+ window.__wgCameraLock = false;
892
+ }, dur + 40);
842
893
  }
843
894
  }
844
895
  window.__wgRingRaf = requestAnimationFrame(tick);
896
+ // Real mouse only seeds cursor pos; dead-zone follow runs in RAF.
845
897
  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);
898
+ window.__wgCursorPos = { x: ev.clientX, y: ev.clientY };
852
899
  };
853
900
  window.addEventListener("mousemove", onMove, { passive: true });
854
901
  window.__wgRingTrack = onMove;
@@ -889,10 +936,38 @@ async function installOverlay(page, title) {
889
936
  if (ring) ring.style.opacity = "0";
890
937
  if (ringLabel) ringLabel.style.opacity = "0";
891
938
  if (window.__wgHideFocusVeil) window.__wgHideFocusVeil();
892
- if (window.__wgClearZoom) window.__wgClearZoom();
939
+ // Do NOT clear camera zoom here - zoomOut:false keeps scale between
940
+ // rings; only resetCameraZoom / teardown call __wgClearZoom.
893
941
  };
894
942
  // Screen Studio-style camera: scale <body>, keep overlays on <html> fixed.
895
- window.__wgCamera = { scale: 1, cx: 0, cy: 0 };
943
+ // Focus (cx,cy) is DOCUMENT space (pre-transform). Viewport mouse/rects
944
+ // must go through __wgViewportToDoc before SetCamera.
945
+ window.__wgCamera = {
946
+ scale: 1,
947
+ cx: window.innerWidth / 2,
948
+ cy: window.innerHeight / 2,
949
+ };
950
+ window.__wgCameraLock = false;
951
+ window.__wgViewportToDoc = (vx, vy) => {
952
+ const cam = window.__wgCamera || {
953
+ scale: 1,
954
+ cx: window.innerWidth / 2,
955
+ cy: window.innerHeight / 2,
956
+ };
957
+ const s = cam.scale > 1.001 ? cam.scale : 1;
958
+ if (s <= 1.001) return { x: vx, y: vy };
959
+ return {
960
+ x: (vx - window.innerWidth / 2) / s + cam.cx,
961
+ y: (vy - window.innerHeight / 2) / s + cam.cy,
962
+ };
963
+ };
964
+ window.__wgElDocPoint = (el) => {
965
+ const r = el.getBoundingClientRect();
966
+ return window.__wgViewportToDoc(
967
+ r.left + r.width / 2,
968
+ r.top + r.height / 2,
969
+ );
970
+ };
896
971
  window.__wgSetCamera = (fx, fy, scale, ms) => {
897
972
  const body = document.body;
898
973
  if (!body) return;
@@ -960,7 +1035,7 @@ async function installOverlay(page, title) {
960
1035
  '<div class="wg-todo-head">Checklist</div><ul>' + rows + '</ul>';
961
1036
  modal.classList.add("wg-todo-on");
962
1037
  };
963
- window.__wgResetCamera = () => {
1038
+ window.__wgResetCamera = () => {
964
1039
  window.__wgSetCamera(window.innerWidth / 2, window.innerHeight / 2, 1);
965
1040
  };
966
1041
  window.__wgClearZoom = () => {
@@ -1412,13 +1487,17 @@ async function applyCameraZoom(page, box, zoom, ms) {
1412
1487
  }).catch(() => {});
1413
1488
  return;
1414
1489
  }
1415
- const cx = box.x + box.width / 2;
1416
- const cy = box.y + box.height / 2;
1490
+ const vx = box.x + box.width / 2;
1491
+ const vy = box.y + box.height / 2;
1417
1492
  const dur = ms != null ? ms : 650;
1418
1493
  await page
1419
- .evaluate(({ cx, cy, scale, dur }) => {
1420
- if (window.__wgSetCamera) window.__wgSetCamera(cx, cy, scale, dur);
1421
- }, { cx, cy, scale: z, dur })
1494
+ .evaluate(({ vx, vy, scale, dur }) => {
1495
+ if (!window.__wgSetCamera) return;
1496
+ const pt = window.__wgViewportToDoc
1497
+ ? window.__wgViewportToDoc(vx, vy)
1498
+ : { x: vx, y: vy };
1499
+ window.__wgSetCamera(pt.x, pt.y, scale, dur);
1500
+ }, { vx, vy, scale: z, dur })
1422
1501
  .catch(() => {});
1423
1502
  }
1424
1503
 
@@ -1552,16 +1631,9 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1552
1631
  : h.follow !== undefined
1553
1632
  ? !!h.follow
1554
1633
  : z > 1;
1555
- const cx = box.x + box.width / 2;
1556
- const cy = box.y + box.height / 2;
1557
- if (h.cursor !== false) {
1558
- await page
1559
- .evaluate(({ cx, cy, camMs }) => {
1560
- if (window.__wgMoveCursorTo) window.__wgMoveCursorTo(cx, cy, camMs, false);
1561
- }, { cx, cy, camMs })
1562
- .catch(() => {});
1563
- }
1564
- // Track first: ring rides visual rect through the zoom (no re-adjust).
1634
+ // Track/zoom first, then park the demo cursor on the LIVE rect.
1635
+ // Placing the cursor on a pre-zoom boundingBox leaves it stranded after
1636
+ // body scale (html cursor is outside the transform).
1565
1637
  await page
1566
1638
  .evaluate(
1567
1639
  ({ sel, label, tone, size, weight, focus, color, zoom, followMouse, camMs }) => {
@@ -1599,7 +1671,21 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1599
1671
  .catch(() => false);
1600
1672
  const waitMs = alreadyZoomed ? Math.min(camMs, 420) : camMs;
1601
1673
  await new Promise((res) => setTimeout(res, waitMs));
1602
-
1674
+ if (h.cursor !== false) {
1675
+ await page
1676
+ .evaluate(({ sel, ms }) => {
1677
+ const el = document.querySelector(sel);
1678
+ if (!el || !window.__wgMoveCursorTo) return;
1679
+ window.__wgCameraLock = true;
1680
+ const r = el.getBoundingClientRect();
1681
+ window.__wgMoveCursorTo(r.left + r.width / 2, r.top + r.height / 2, ms, false);
1682
+ setTimeout(() => {
1683
+ window.__wgCameraLock = false;
1684
+ }, ms + 40);
1685
+ }, { sel: h.selector, ms: Math.min(280, camMs) })
1686
+ .catch(() => {});
1687
+ await new Promise((res) => setTimeout(res, Math.min(280, camMs)));
1688
+ }
1603
1689
  // Appear settle
1604
1690
  await new Promise((res) => setTimeout(res, fadeMs));
1605
1691
  const authored = resolveFixtureDwellMs(h, { gatesFast, pace });
@@ -1618,7 +1704,11 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1618
1704
  }
1619
1705
  }
1620
1706
  await stopLiveFollow();
1621
- await resetCameraZoom(page);
1707
+ // Keep camera if the last ring (or phase default) asked zoomOut:false.
1708
+ const last = list.length > 0 ? list[list.length - 1] : null;
1709
+ if (!last || last.zoomOut !== false) {
1710
+ await resetCameraZoom(page);
1711
+ }
1622
1712
  if (list.length === 0) {
1623
1713
  await hideRing(page);
1624
1714
  }
@@ -2345,6 +2435,7 @@ async function teardownOverlay(page) {
2345
2435
  window.__wgRingTrack = null;
2346
2436
  }
2347
2437
  window.__wgNarrateOwnsRing = false;
2438
+ if (window.__wgClearZoom) window.__wgClearZoom();
2348
2439
  for (const id of ["wg-panel", "wg-ring", "wg-ring-label", "wg-cursor", "wg-click-pulse", "wg-banner", "wg-todo-modal", "wg-focus-veil"]) {
2349
2440
  const el = document.getElementById(id);
2350
2441
  if (el) el.remove();
@@ -2357,8 +2448,11 @@ async function moveCursorTo(page, box, ms) {
2357
2448
  const x = box.x + box.width / 2;
2358
2449
  const y = box.y + box.height / 2;
2359
2450
  const travel = Math.max(0, Number(ms) || 0);
2451
+ // Freeze camera follow while the demo cursor travels - otherwise dead-zone
2452
+ // pans fight the click target (esp. right-edge cart chrome).
2360
2453
  await page
2361
2454
  .evaluate(({ x, y, ms }) => {
2455
+ window.__wgCameraLock = true;
2362
2456
  if (window.__wgMoveCursorTo) window.__wgMoveCursorTo(x, y, ms);
2363
2457
  }, { x, y, ms: travel })
2364
2458
  .catch(() => {});
@@ -2368,6 +2462,11 @@ async function moveCursorTo(page, box, ms) {
2368
2462
  if (travel > 0) {
2369
2463
  await new Promise((res) => setTimeout(res, travel));
2370
2464
  }
2465
+ await page
2466
+ .evaluate(() => {
2467
+ window.__wgCameraLock = false;
2468
+ })
2469
+ .catch(() => {});
2371
2470
  return { x, y };
2372
2471
  }
2373
2472
 
@@ -20,6 +20,7 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
20
20
  ctx.todos(["Find the product", "Add to cart", "Open cart"]);
21
21
  ctx.todoIndex(2);
22
22
  ctx.zoom(1.4);
23
+ ctx.zoomOut(false);
23
24
  ctx.highlights({
24
25
  cart: {
25
26
  selector: ".shopping_cart_link",
@@ -66,6 +66,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
66
66
  ctx.banner("In the cart");
67
67
  ctx.todos(["Find the product", "Add to cart", "Open cart"]);
68
68
  ctx.todoIndex(1);
69
+ ctx.zoomOut(false);
69
70
  ctx.ring("badge", {
70
71
  selector: InventorySel.cartBadge,
71
72
  label: "Cart badge",
@@ -73,6 +74,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
73
74
  focus: true,
74
75
  color: "#86efac",
75
76
  zoom: 1.6,
77
+ zoomOut: false,
76
78
  followMouse: true,
77
79
  duration: true,
78
80
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.35",
3
+ "version": "0.12.37",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -20,6 +20,7 @@ export const NavCartBlock = defineNavClickBlock<CartPage>({
20
20
  ctx.todos(["Find the product", "Add to cart", "Open cart"]);
21
21
  ctx.todoIndex(2);
22
22
  ctx.zoom(1.4);
23
+ ctx.zoomOut(false);
23
24
  ctx.highlights({
24
25
  cart: {
25
26
  selector: ".shopping_cart_link",
@@ -66,6 +66,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
66
66
  ctx.banner("In the cart");
67
67
  ctx.todos(["Find the product", "Add to cart", "Open cart"]);
68
68
  ctx.todoIndex(1);
69
+ ctx.zoomOut(false);
69
70
  ctx.ring("badge", {
70
71
  selector: InventorySel.cartBadge,
71
72
  label: "Cart badge",
@@ -73,6 +74,7 @@ export const AddToCartBlock = defineEffectBlock<Checkpoint<string>, ItemInCart>(
73
74
  focus: true,
74
75
  color: "#86efac",
75
76
  zoom: 1.6,
77
+ zoomOut: false,
76
78
  followMouse: true,
77
79
  duration: true,
78
80
  });