waygraph 0.12.31 → 0.12.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -274,12 +274,12 @@ async function findBlock(projectDir, ref) {
274
274
  if (byRuntimeName.length === 1) return byRuntimeName[0];
275
275
  if (byRuntimeName.length > 1) {
276
276
  throw new Error(
277
- "waygraph chain: \\"" + ref + "\\" matches " + byRuntimeName.length + " Blocks by name - " +
277
+ "waygraph chain: \\\"" + ref + "\\\" matches " + byRuntimeName.length + " Blocks by name - " +
278
278
  byRuntimeName.map((b) => b.exportName + " (" + b.file + ")").join(", ") +
279
279
  ". Use the export name to disambiguate.",
280
280
  );
281
281
  }
282
- throw new Error("waygraph chain: no Block named \\"" + ref + "\\" found under " + projectDir);
282
+ throw new Error("waygraph chain: no Block named \\\"" + ref + "\\\" found under " + projectDir);
283
283
  }
284
284
 
285
285
  /**
@@ -316,7 +316,7 @@ function parseChainSpec(spec) {
316
316
  const m = /^([A-Za-z_$][\\w-]*)\\s*(?:\\(([\\s\\S]*)\\))?$/.exec(s);
317
317
  if (!m) {
318
318
  throw new Error(
319
- "waygraph chain: could not parse segment \\"" + s + "\\" - expected \\"blockName\\" or \\"blockName({...json...})\\"",
319
+ "waygraph chain: could not parse segment \\\"" + s + "\\\" - expected \\\"blockName\\\" or \\\"blockName({...json...})\\\"",
320
320
  );
321
321
  }
322
322
  const ref = m[1];
@@ -329,7 +329,7 @@ function seedMemFromRequires(mem, requires, json, label) {
329
329
  if (requires.length === 0) {
330
330
  if (json !== undefined) {
331
331
  throw new Error(
332
- "waygraph chain: \\"" + label + "\\" takes no input (empty requires) but got a payload: " + json,
332
+ "waygraph chain: \\\"" + label + "\\\" takes no input (empty requires) but got a payload: " + json,
333
333
  );
334
334
  }
335
335
  return;
@@ -341,7 +341,7 @@ function seedMemFromRequires(mem, requires, json, label) {
341
341
  json = fromEnv;
342
342
  } else {
343
343
  throw new Error(
344
- "waygraph chain: \\"" + label + "\\" requires " + requires.map((k) => k.name).join(", ") +
344
+ "waygraph chain: \\\"" + label + "\\\" requires " + requires.map((k) => k.name).join(", ") +
345
345
  " - give a JSON payload (inline blockName({...}) or --data '{...}')",
346
346
  );
347
347
  }
@@ -350,7 +350,7 @@ function seedMemFromRequires(mem, requires, json, label) {
350
350
  try {
351
351
  parsed = JSON.parse(json);
352
352
  } catch (err) {
353
- throw new Error("waygraph chain: \\"" + label + "\\" payload is not valid JSON - " + String(err));
353
+ throw new Error("waygraph chain: \\\"" + label + "\\\" payload is not valid JSON - " + String(err));
354
354
  }
355
355
  // Keyed-by-name when every require name is a top-level key (preferred for
356
356
  // --data and for {"saucedemo.credentials":{...}} even with one require).
@@ -367,13 +367,13 @@ function seedMemFromRequires(mem, requires, json, label) {
367
367
  }
368
368
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
369
369
  throw new Error(
370
- "waygraph chain: \\"" + label + "\\" requires " + requires.length + " keys (" +
370
+ "waygraph chain: \\\"" + label + "\\\" requires " + requires.length + " keys (" +
371
371
  requires.map((k) => k.name).join(", ") + ") - payload must be an object keyed by each key's name",
372
372
  );
373
373
  }
374
374
  for (const k of requires) {
375
375
  if (!(k.name in parsed)) {
376
- throw new Error("waygraph chain: \\"" + label + "\\" payload is missing required key \\"" + k.name + "\\"");
376
+ throw new Error("waygraph chain: \\\"" + label + "\\\" payload is missing required key \\\"" + k.name + "\\\"");
377
377
  }
378
378
  mem.set(k, parsed[k.name]);
379
379
  }
@@ -413,19 +413,11 @@ const RING_CSS =
413
413
  "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
414
414
  "border:2.5px solid #7C3AED;border-radius:10px;" +
415
415
  "box-shadow:0 0 0 4px rgba(124,58,237,.16);" +
416
- "transition:opacity .32s ease,left .28s ease,top .28s ease,width .28s ease,height .28s ease,border-color .15s,box-shadow .32s ease;}" +
417
- // Unquoted [data-focus=1]: this lives inside CHAIN_RUNNER_SCRIPT's template
418
- // literal - a \" here would be eaten by the outer template and break the
419
- // generated .mjs. CSS allows unquoted attr values for identifiers.
420
- "#wg-ring[data-focus=1]{box-shadow:0 0 0 4px rgba(124,58,237,.2),0 0 0 9999px rgba(15,6,32,.55);}" +
421
- "#wg-ring[data-tone=auto][data-focus=1]{box-shadow:0 0 0 4px rgba(156,163,175,.28),0 0 0 9999px rgba(15,6,32,.55);}" +
422
- "#wg-ring[data-tone=info][data-focus=1]{box-shadow:0 0 0 4px rgba(59,130,246,.22),0 0 0 9999px rgba(15,6,32,.55);}" +
423
- "#wg-ring[data-tone=warning][data-focus=1]{box-shadow:0 0 0 4px rgba(234,179,8,.22),0 0 0 9999px rgba(15,6,32,.55);}" +
424
- "#wg-ring[data-tone=danger][data-focus=1]{box-shadow:0 0 0 4px rgba(239,68,68,.22),0 0 0 9999px rgba(15,6,32,.55);}" +
425
- "#wg-ring[data-tone=success][data-focus=1]{box-shadow:0 0 0 4px rgba(34,197,94,.22),0 0 0 9999px rgba(15,6,32,.55);}" +
426
- // Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
427
- // Automation (verify fallback, unmatched fill/click) = gray - operators
428
- // can ignore engine checks and watch purple + semantic tones.
416
+ "transition:opacity .32s ease,border-color .15s;}" +
417
+ "#wg-focus-veil{position:fixed;z-index:2147483645;pointer-events:none;opacity:0;" +
418
+ "border-radius:10px;box-shadow:0 0 0 9999px rgba(12,6,28,.62);" +
419
+ "transition:opacity .28s ease;}" +
420
+ "#wg-focus-veil.wg-focus-on{opacity:1;}" +
429
421
  "#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
430
422
  "#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
431
423
  "#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
@@ -441,7 +433,7 @@ const RING_CSS =
441
433
  // window.__wgPositionRing in installOverlay.
442
434
  "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
443
435
  "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
444
- "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .32s ease,left .28s ease,top .28s ease,background .15s,color .15s;}" +
436
+ "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .32s ease,background .15s,color .15s;}" +
445
437
  "#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
446
438
  "#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
447
439
  "#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
@@ -683,6 +675,11 @@ async function installOverlay(page, title) {
683
675
  } catch {
684
676
  /* private mode / blocked storage - falls back to manual gating */
685
677
  }
678
+ if (!document.getElementById("wg-focus-veil")) {
679
+ const veil = document.createElement("div");
680
+ veil.id = "wg-focus-veil";
681
+ document.documentElement.appendChild(veil);
682
+ }
686
683
  if (!document.getElementById("wg-ring")) {
687
684
  const ring = document.createElement("div");
688
685
  ring.id = "wg-ring";
@@ -733,6 +730,7 @@ async function installOverlay(page, title) {
733
730
  window.__wgPositionRing = (box, label, tone, style) => {
734
731
  const ring = document.getElementById("wg-ring");
735
732
  const ringLabel = document.getElementById("wg-ring-label");
733
+ const veil = document.getElementById("wg-focus-veil");
736
734
  if (!ring || !ringLabel) return;
737
735
  const raw = (tone || "planned") + "";
738
736
  const t =
@@ -757,8 +755,8 @@ async function installOverlay(page, title) {
757
755
  const vh = window.innerHeight;
758
756
  let left = box.x - pad;
759
757
  let top = box.y - pad;
760
- const width = box.width + pad * 2;
761
- const height = box.height + pad * 2;
758
+ const width = Math.max(0, box.width + pad * 2);
759
+ const height = Math.max(0, box.height + pad * 2);
762
760
  if (left < margin) left = margin;
763
761
  if (top < margin) top = margin;
764
762
  if (left + width > vw - margin) left = Math.max(margin, vw - margin - width);
@@ -768,24 +766,100 @@ async function installOverlay(page, title) {
768
766
  ring.style.width = width + "px";
769
767
  ring.style.height = height + "px";
770
768
  ring.style.opacity = "1";
769
+ if (veil) {
770
+ veil.style.left = left + "px";
771
+ veil.style.top = top + "px";
772
+ veil.style.width = width + "px";
773
+ veil.style.height = height + "px";
774
+ const br = size === "sm" ? "8px" : size === "lg" ? "12px" : "10px";
775
+ veil.style.borderRadius = br;
776
+ if (st.focus) veil.classList.add("wg-focus-on");
777
+ else veil.classList.remove("wg-focus-on");
778
+ }
771
779
  ringLabel.textContent = label;
772
780
  ringLabel.style.opacity = "1";
773
781
  const capColor = st.color || st.labelColor || st.textColor;
774
782
  if (capColor) ringLabel.style.color = String(capColor);
775
783
  else ringLabel.style.removeProperty("color");
776
- // Measure the label's own natural width/height (its real
777
- // rendered size, unchanged) before deciding where it fits.
778
784
  const lw = ringLabel.offsetWidth;
779
785
  const lh = ringLabel.offsetHeight;
780
786
  let labelLeft = left;
781
787
  let labelTop = top + height + 8;
782
- if (labelTop + lh > vh - margin) labelTop = top - lh - 8; // flip above if it'd overflow the bottom
788
+ if (labelTop + lh > vh - margin) labelTop = top - lh - 8;
783
789
  if (labelTop < margin) labelTop = margin;
784
790
  if (labelLeft + lw > vw - margin) labelLeft = Math.max(margin, vw - margin - lw);
785
791
  if (labelLeft < margin) labelLeft = margin;
786
792
  ringLabel.style.left = labelLeft + "px";
787
793
  ringLabel.style.top = labelTop + "px";
788
794
  };
795
+ window.__wgHideFocusVeil = () => {
796
+ const veil = document.getElementById("wg-focus-veil");
797
+ if (veil) veil.classList.remove("wg-focus-on");
798
+ };
799
+ window.__wgStartHighlightTrack = (opts) => {
800
+ if (window.__wgStopHighlightTrack) window.__wgStopHighlightTrack();
801
+ const sel = opts.sel;
802
+ const label = opts.label;
803
+ const tone = opts.tone || "planned";
804
+ const size = opts.size || "md";
805
+ const weight = opts.weight || "normal";
806
+ const focus = !!opts.focus;
807
+ const color = opts.color || "";
808
+ const zoom = opts.zoom != null && opts.zoom > 1 ? opts.zoom : 1;
809
+ const followMouse = opts.followMouse !== false && zoom > 1;
810
+ const camMs = opts.camMs != null ? opts.camMs : 700;
811
+ let settled = zoom <= 1;
812
+ const settleAt = Date.now() + camMs;
813
+ const style = { size, weight, focus, color };
814
+ const tick = () => {
815
+ const el = document.querySelector(sel);
816
+ if (el && window.__wgPositionRing) {
817
+ const r = el.getBoundingClientRect();
818
+ window.__wgPositionRing(
819
+ { x: r.left, y: r.top, width: r.width, height: r.height },
820
+ label,
821
+ tone,
822
+ style,
823
+ );
824
+ }
825
+ if (!settled && Date.now() >= settleAt) settled = true;
826
+ window.__wgRingRaf = requestAnimationFrame(tick);
827
+ };
828
+ if (zoom > 1 && window.__wgSetCamera) {
829
+ const el0 = document.querySelector(sel);
830
+ if (el0) {
831
+ const r0 = el0.getBoundingClientRect();
832
+ window.__wgSetCamera(
833
+ r0.left + r0.width / 2,
834
+ r0.top + r0.height / 2,
835
+ zoom,
836
+ camMs,
837
+ );
838
+ }
839
+ }
840
+ window.__wgRingRaf = requestAnimationFrame(tick);
841
+ const onMove = (ev) => {
842
+ if (!followMouse || !settled) return;
843
+ if (!window.__wgSetCamera) return;
844
+ const cam = window.__wgCamera;
845
+ const scale = (cam && cam.scale > 1 ? cam.scale : zoom) || zoom;
846
+ if (!(scale > 1)) return;
847
+ window.__wgSetCamera(ev.clientX, ev.clientY, scale, 140);
848
+ };
849
+ window.addEventListener("mousemove", onMove, { passive: true });
850
+ window.__wgRingTrack = onMove;
851
+ window.__wgStopHighlightTrack = () => {
852
+ if (window.__wgRingRaf) {
853
+ cancelAnimationFrame(window.__wgRingRaf);
854
+ window.__wgRingRaf = null;
855
+ }
856
+ if (window.__wgRingTrack) {
857
+ window.removeEventListener("mousemove", window.__wgRingTrack);
858
+ window.__wgRingTrack = null;
859
+ }
860
+ window.__wgStopHighlightTrack = null;
861
+ };
862
+ };
789
863
  window.__wgClickPulse = (x, y, tone) => {
790
864
  const pulse = document.getElementById("wg-click-pulse");
791
865
  if (!pulse) return;
@@ -810,6 +884,7 @@ async function installOverlay(page, title) {
810
884
  const ringLabel = document.getElementById("wg-ring-label");
811
885
  if (ring) ring.style.opacity = "0";
812
886
  if (ringLabel) ringLabel.style.opacity = "0";
887
+ if (window.__wgHideFocusVeil) window.__wgHideFocusVeil();
813
888
  if (window.__wgClearZoom) window.__wgClearZoom();
814
889
  };
815
890
  // Screen Studio-style camera: scale <body>, keep overlays on <html> fixed.
@@ -861,23 +936,27 @@ async function installOverlay(page, title) {
861
936
  : t.done
862
937
  ? "wg-todo-done"
863
938
  : "wg-todo-pending";
864
- const mark = t.done ? "\u2713" : t.current ? "\u2192" : "\u25CB";
939
+ const mark = t.done
940
+ ? String.fromCharCode(0x2713)
941
+ : t.current
942
+ ? String.fromCharCode(0x2192)
943
+ : String.fromCharCode(0x25cb);
865
944
  return (
866
- "<li class=\"" +
945
+ '<li class="' +
867
946
  cls +
868
- "\"><span class=\"wg-todo-mark\">" +
947
+ '"><span class="wg-todo-mark">' +
869
948
  mark +
870
- "</span><span>" +
949
+ '</span><span>' +
871
950
  esc(t.text || "") +
872
- "</span></li>"
951
+ '</span></li>'
873
952
  );
874
953
  })
875
954
  .join("");
876
955
  modal.innerHTML =
877
- "<div class=\"wg-todo-head\">Checklist</div><ul>" + rows + "</ul>";
956
+ '<div class="wg-todo-head">Checklist</div><ul>' + rows + '</ul>';
878
957
  modal.classList.add("wg-todo-on");
879
958
  };
880
- window.__wgResetCamera = () => {
959
+ window.__wgResetCamera = () => {
881
960
  window.__wgSetCamera(window.innerWidth / 2, window.innerHeight / 2, 1);
882
961
  };
883
962
  window.__wgClearZoom = () => {
@@ -1066,7 +1145,7 @@ async function renderBeforeStep(page, info) {
1066
1145
  // active, the episode 2 tab is inactive"). Absent entirely for an ad
1067
1146
  // hoc block chain with no named Flows.
1068
1147
  const episodesHtml = info.allEpisodes && info.allEpisodes.length > 0
1069
- ? "<div id=\\"wg-episodes\\">" +
1148
+ ? "<div id=\\\"wg-episodes\\\">" +
1070
1149
  info.allEpisodes
1071
1150
  .map((e) => {
1072
1151
  const cls = e.episodeNumber < info.episodeNumber ? "wg-ep-done"
@@ -1077,7 +1156,7 @@ async function renderBeforeStep(page, info) {
1077
1156
  const enteredCls = info.justEnteredEpisode && e.episodeNumber === info.episodeNumber
1078
1157
  ? " wg-ep-entered"
1079
1158
  : "";
1080
- return "<span class=\\"wg-ep-tab " + cls + enteredCls + "\\">Episode " + e.episodeNumber + ": " +
1159
+ return "<span class=\\\"wg-ep-tab " + cls + enteredCls + "\\\">Episode " + e.episodeNumber + ": " +
1081
1160
  esc(e.episodeTitle || "") + "</span>";
1082
1161
  })
1083
1162
  .join("") +
@@ -1093,62 +1172,66 @@ async function renderBeforeStep(page, info) {
1093
1172
  .map((name, idx) => {
1094
1173
  const cls = idx < info.moduleIndex ? "wg-mod-done" : idx === info.moduleIndex ? "wg-mod-current" : "wg-mod-upcoming";
1095
1174
  const desc = info.allDescriptions && info.allDescriptions[idx];
1096
- const titleAttr = desc ? " title=\\"" + esc(desc) + "\\"" : "";
1097
- return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
1175
+ const titleAttr = desc ? " title=\\\"" + esc(desc) + "\\\"" : "";
1176
+ return "<span class=\\\"wg-mod " + cls + "\\\"" + titleAttr + ">" + name + "</span>";
1098
1177
  })
1099
1178
  .join("");
1100
1179
  const narrationHtml = info.description
1101
- ? "<div class=\\"wg-narration\\">" + esc(info.description) + "</div>"
1180
+ ? "<div class=\\\"wg-narration\\\">" + esc(info.description) + "</div>"
1102
1181
  : "";
1103
1182
  const paceHtml =
1104
1183
  info.paceBadge || info.paceLabel
1105
- ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
1184
+ ? "<div class=\\\"wg-pace\\\" data-pace-kind=\\\"" +
1106
1185
  esc(info.paceKind || "normal") +
1107
- "\\"><span class=\\"wg-pace-badge\\">" +
1186
+ "\\\"><span class=\\\"wg-pace-badge\\\">" +
1108
1187
  esc(info.paceBadge || "1x") +
1109
1188
  "</span><span>" +
1110
1189
  esc(info.paceLabel || "") +
1111
1190
  "</span></div>"
1112
1191
  : "";
1113
1192
  let html =
1114
- "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
1193
+ "<div id=\\\"wg-progress\\\"><div id=\\\"wg-progress-bar\\\" style=\\\"width:" + pct + "%\\\"></div></div>" +
1115
1194
  episodesHtml +
1116
1195
  paceHtml +
1117
- "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1196
+ "<div id=\\\"wg-modules\\\" class=\\\"" + modulesClass + "\\\">" + modulesHtml + "</div>" +
1118
1197
  "<h3>Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + "</h3>" +
1119
1198
  narrationHtml +
1120
1199
  ((() => {
1121
1200
  const list = info.todos || [];
1122
1201
  if (!list.length) return "";
1123
1202
  return (
1124
- "<ul id=\"wg-todos\">" +
1203
+ '<ul id="wg-todos">' +
1125
1204
  list
1126
1205
  .map((t) => {
1127
1206
  const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
1128
- const mark = t.done ? "\u2713" : t.current ? "\u2192" : "\u25CB";
1207
+ const mark = t.done
1208
+ ? String.fromCharCode(0x2713)
1209
+ : t.current
1210
+ ? String.fromCharCode(0x2192)
1211
+ : String.fromCharCode(0x25cb);
1129
1212
  return (
1130
- "<li class=\"" +
1213
+ '<li class="' +
1131
1214
  cls +
1132
- "\"><span class=\"wg-todo-mark\">" +
1215
+ '"><span class="wg-todo-mark">' +
1133
1216
  mark +
1134
- "</span><span>" +
1217
+ '</span><span>' +
1135
1218
  esc(t.text || "") +
1136
- "</span></li>"
1219
+ '</span></li>'
1137
1220
  );
1138
1221
  })
1139
1222
  .join("") +
1140
- "</ul>"
1223
+ '</ul>'
1141
1224
  );
1142
1225
  })());
1143
1226
  if (info.keys.length === 0) {
1144
- html += "<div class=\\"wg-key\\">(no MemKeys required)</div>";
1227
+ html += "<div class=\\\"wg-key\\\">(no MemKeys required)</div>";
1145
1228
  }
1146
1229
  for (const k of info.keys) {
1147
1230
  html +=
1148
- "<div class=\\"wg-key\\"><label>" + k.name + "</label>" +
1149
- "<textarea data-key=\\"" + k.name + "\\" rows=\\"2\\">" +
1231
+ "<div class=\\\"wg-key\\\"><label>" + k.name + "</label>" +
1232
+ "<textarea data-key=\\\"" + k.name + "\\\" rows=\\\"2\\\">" +
1150
1233
  k.value.replace(/</g, "&lt;") + "</textarea>" +
1151
- "<div class=\\"wg-key-pretty\\" data-pretty-for=\\"" + esc(k.name) + "\\"></div>" +
1234
+ "<div class=\\\"wg-key-pretty\\\" data-pretty-for=\\\"" + esc(k.name) + "\\\"></div>" +
1152
1235
  "</div>";
1153
1236
  }
1154
1237
  let autoNow = false;
@@ -1158,12 +1241,12 @@ async function renderBeforeStep(page, info) {
1158
1241
  /* private mode / blocked storage - defaults to manual */
1159
1242
  }
1160
1243
  html +=
1161
- "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
1244
+ "<div class=\\\"wg-autoplay-row\\\"><label><input type=\\\"checkbox\\\" id=\\\"wg-autoplay-cb\\\"" +
1162
1245
  (autoNow ? " checked" : "") +
1163
1246
  "> Auto-advance</label></div>" +
1164
- "<div id=\\"wg-gate-manual\\"" + (autoNow ? " style=\\"display:none\\"" : "") +
1165
- "><button id=\\"wg-run\\">Run this step \\u25B6</button></div>" +
1166
- "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" + (autoNow ? "" : " style=\\"display:none\\"") +
1247
+ "<div id=\\\"wg-gate-manual\\\"" + (autoNow ? " style=\\\"display:none\\\"" : "") +
1248
+ "><button id=\\\"wg-run\\\">Run this step \\u25B6</button></div>" +
1249
+ "<div id=\\\"wg-gate-auto\\\" class=\\\"wg-auto\\\"" + (autoNow ? "" : " style=\\\"display:none\\\"") +
1167
1250
  ">Auto-advancing...</div>";
1168
1251
  panel.innerHTML = html;
1169
1252
  if (isNewPanel) {
@@ -1196,7 +1279,7 @@ async function renderBeforeStep(page, info) {
1196
1279
  return Object.entries(parsed)
1197
1280
  .map(([field, value]) => {
1198
1281
  const label = field.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
1199
- return "<div class=\\"wg-key-pretty-row\\"><span class=\\"wg-key-pretty-label\\">" +
1282
+ return "<div class=\\\"wg-key-pretty-row\\\"><span class=\\\"wg-key-pretty-label\\\">" +
1200
1283
  esc(label) + ":</span> " + esc(String(value)) + "</div>";
1201
1284
  })
1202
1285
  .join("");
@@ -1205,7 +1288,7 @@ async function renderBeforeStep(page, info) {
1205
1288
  const prettyOn = window.__wgPretty !== false;
1206
1289
  panel.querySelectorAll("textarea[data-key]").forEach((ta) => {
1207
1290
  const key = ta.getAttribute("data-key");
1208
- const previewEl = panel.querySelector(".wg-key-pretty[data-pretty-for=\\"" + key + "\\"]");
1291
+ const previewEl = panel.querySelector(".wg-key-pretty[data-pretty-for=\\\"" + key + "\\\"]");
1209
1292
  if (!previewEl) return;
1210
1293
  previewEl.style.display = prettyOn ? "" : "none";
1211
1294
  if (prettyOn) previewEl.innerHTML = prettyMemValue(ta.value);
@@ -1423,15 +1506,18 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1423
1506
  const stopLiveFollow = async () => {
1424
1507
  await page
1425
1508
  .evaluate(() => {
1509
+ if (window.__wgStopHighlightTrack) window.__wgStopHighlightTrack();
1426
1510
  if (window.__wgRingRaf) {
1427
1511
  cancelAnimationFrame(window.__wgRingRaf);
1428
1512
  window.__wgRingRaf = null;
1429
1513
  }
1430
1514
  if (window.__wgRingTrack) {
1515
+ window.removeEventListener("mousemove", window.__wgRingTrack);
1431
1516
  window.removeEventListener("resize", window.__wgRingTrack);
1432
1517
  window.removeEventListener("scroll", window.__wgRingTrack, true);
1433
1518
  window.__wgRingTrack = null;
1434
1519
  }
1520
+ if (window.__wgHideFocusVeil) window.__wgHideFocusVeil();
1435
1521
  })
1436
1522
  .catch(() => {});
1437
1523
  };
@@ -1454,12 +1540,16 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1454
1540
  } else if (h.todos && h.todos.length) {
1455
1541
  await setDemoTodos(page, normalizeTodos(h.todos, h.todoIndex));
1456
1542
  }
1457
- const cx = box.x + box.width / 2;
1458
- const cy = box.y + box.height / 2;
1459
1543
  const z = h.zoom != null && Number(h.zoom) > 1 ? Number(h.zoom) : 1;
1460
1544
  const camMs = 700;
1461
- // One-shot camera: zoom + center together (no zoom-then-align).
1462
- await applyCameraZoom(page, box, z, camMs);
1545
+ const followMouse =
1546
+ h.followMouse !== undefined
1547
+ ? !!h.followMouse
1548
+ : h.follow !== undefined
1549
+ ? !!h.follow
1550
+ : z > 1;
1551
+ const cx = box.x + box.width / 2;
1552
+ const cy = box.y + box.height / 2;
1463
1553
  if (h.cursor !== false) {
1464
1554
  await page
1465
1555
  .evaluate(({ cx, cy, camMs }) => {
@@ -1467,41 +1557,40 @@ async function cycleHighlightRings(page, highlights, gatesFast, opts) {
1467
1557
  }, { cx, cy, camMs })
1468
1558
  .catch(() => {});
1469
1559
  }
1470
- await showRing(page, box, h.label, h.tone || "planned", {
1471
- size: h.size,
1472
- weight: h.weight,
1473
- focus: !!h.focus,
1474
- color: h.color || h.labelColor || h.textColor,
1475
- });
1476
- await new Promise((res) => setTimeout(res, camMs));
1477
- // Live ring track only - camera stays locked (no per-frame re-pan).
1560
+ // Track first: ring rides visual rect through the zoom (no re-adjust).
1478
1561
  await page
1479
- .evaluate(({ sel, label, tone, size, weight, focus, color }) => {
1480
- const tick = () => {
1481
- const el = document.querySelector(sel);
1482
- if (!el) {
1483
- window.__wgRingRaf = requestAnimationFrame(tick);
1484
- return;
1562
+ .evaluate(
1563
+ ({ sel, label, tone, size, weight, focus, color, zoom, followMouse, camMs }) => {
1564
+ if (window.__wgStartHighlightTrack) {
1565
+ window.__wgStartHighlightTrack({
1566
+ sel,
1567
+ label,
1568
+ tone,
1569
+ size,
1570
+ weight,
1571
+ focus,
1572
+ color,
1573
+ zoom,
1574
+ followMouse,
1575
+ camMs,
1576
+ });
1485
1577
  }
1486
- const r = el.getBoundingClientRect();
1487
- const box = { x: r.left, y: r.top, width: r.width, height: r.height };
1488
- if (window.__wgPositionRing) {
1489
- window.__wgPositionRing(box, label, tone || "planned", { size, weight, focus, color });
1490
- }
1491
- window.__wgRingRaf = requestAnimationFrame(tick);
1492
- };
1493
- window.__wgRingRaf = requestAnimationFrame(tick);
1494
- window.__wgRingTrack = () => {};
1495
- }, {
1496
- sel: h.selector,
1497
- label: h.label,
1498
- tone: h.tone || "planned",
1499
- size: h.size || "md",
1500
- weight: h.weight || "normal",
1501
- focus: !!h.focus,
1502
- color: h.color || h.labelColor || h.textColor || "",
1503
- })
1578
+ },
1579
+ {
1580
+ sel: h.selector,
1581
+ label: h.label,
1582
+ tone: h.tone || "planned",
1583
+ size: h.size || "md",
1584
+ weight: h.weight || "normal",
1585
+ focus: !!h.focus,
1586
+ color: h.color || h.labelColor || h.textColor || "",
1587
+ zoom: z,
1588
+ followMouse,
1589
+ camMs,
1590
+ },
1591
+ )
1504
1592
  .catch(() => {});
1593
+ await new Promise((res) => setTimeout(res, camMs));
1505
1594
 
1506
1595
  // Appear settle
1507
1596
  await new Promise((res) => setTimeout(res, fadeMs));
@@ -1570,8 +1659,8 @@ async function renderAfterStep(page, info) {
1570
1659
  .map((name, idx) => {
1571
1660
  const cls = idx <= info.moduleIndex ? "wg-mod-done" : "wg-mod-upcoming";
1572
1661
  const desc = info.allDescriptions && info.allDescriptions[idx];
1573
- const titleAttr = desc ? " title=\\"" + escA(desc) + "\\"" : "";
1574
- return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
1662
+ const titleAttr = desc ? " title=\\\"" + escA(desc) + "\\\"" : "";
1663
+ return "<span class=\\\"wg-mod " + cls + "\\\"" + titleAttr + ">" + name + "</span>";
1575
1664
  })
1576
1665
  .join("");
1577
1666
  // QA-friendly by default ("LoginSuccess" -> "Login Success") - raw
@@ -1582,13 +1671,13 @@ async function renderAfterStep(page, info) {
1582
1671
  .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") || info.resultTag;
1583
1672
  const pretty = window.__wgPretty !== false;
1584
1673
  const resultHtml =
1585
- "<div class=\\"wg-toggle\\">" +
1586
- "<button class=\\"wg-toggle-btn" + (pretty ? " wg-active" : "") + "\\" data-mode=\\"pretty\\">Pretty</button>" +
1587
- "<button class=\\"wg-toggle-btn" + (pretty ? "" : " wg-active") + "\\" data-mode=\\"json\\">JSON</button>" +
1674
+ "<div class=\\\"wg-toggle\\\">" +
1675
+ "<button class=\\\"wg-toggle-btn" + (pretty ? " wg-active" : "") + "\\\" data-mode=\\\"pretty\\\">Pretty</button>" +
1676
+ "<button class=\\\"wg-toggle-btn" + (pretty ? "" : " wg-active") + "\\\" data-mode=\\\"json\\\">JSON</button>" +
1588
1677
  "</div>" +
1589
- "<div class=\\"wg-result wg-result-pretty\\" style=\\"display:" + (pretty ? "block" : "none") + "\\">" +
1678
+ "<div class=\\\"wg-result wg-result-pretty\\\" style=\\\"display:" + (pretty ? "block" : "none") + "\\\">" +
1590
1679
  prettyText + "</div>" +
1591
- "<div class=\\"wg-result wg-result-json\\" style=\\"display:" + (pretty ? "none" : "block") + "\\">" +
1680
+ "<div class=\\\"wg-result wg-result-json\\\" style=\\\"display:" + (pretty ? "none" : "block") + "\\\">" +
1592
1681
  info.resultTag + "</div>";
1593
1682
  let autoNow = false;
1594
1683
  try {
@@ -1597,21 +1686,21 @@ async function renderAfterStep(page, info) {
1597
1686
  /* private mode / blocked storage - defaults to manual */
1598
1687
  }
1599
1688
  const gateHtml =
1600
- "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
1689
+ "<div class=\\\"wg-autoplay-row\\\"><label><input type=\\\"checkbox\\\" id=\\\"wg-autoplay-cb\\\"" +
1601
1690
  (autoNow ? " checked" : "") +
1602
1691
  "> Auto-advance</label></div>" +
1603
- "<div id=\\"wg-gate-manual\\"" + (autoNow ? " style=\\"display:none\\"" : "") +
1604
- "><button id=\\"wg-run\\">" + buttonLabel + "</button></div>" +
1605
- "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" + (autoNow ? "" : " style=\\"display:none\\"") +
1692
+ "<div id=\\\"wg-gate-manual\\\"" + (autoNow ? " style=\\\"display:none\\\"" : "") +
1693
+ "><button id=\\\"wg-run\\\">" + buttonLabel + "</button></div>" +
1694
+ "<div id=\\\"wg-gate-auto\\\" class=\\\"wg-auto\\\"" + (autoNow ? "" : " style=\\\"display:none\\\"") +
1606
1695
  ">Auto-advancing...</div>";
1607
1696
  const episodesHtml = info.allEpisodes && info.allEpisodes.length > 0
1608
- ? "<div id=\\"wg-episodes\\">" +
1697
+ ? "<div id=\\\"wg-episodes\\\">" +
1609
1698
  info.allEpisodes
1610
1699
  .map((e) => {
1611
1700
  const cls = e.episodeNumber < info.episodeNumber ? "wg-ep-done"
1612
1701
  : e.episodeNumber === info.episodeNumber ? "wg-ep-current"
1613
1702
  : "wg-ep-upcoming";
1614
- return "<span class=\\"wg-ep-tab " + cls + "\\">Episode " + e.episodeNumber + ": " +
1703
+ return "<span class=\\\"wg-ep-tab " + cls + "\\\">Episode " + e.episodeNumber + ": " +
1615
1704
  escA(e.episodeTitle || "") + "</span>";
1616
1705
  })
1617
1706
  .join("") +
@@ -1619,41 +1708,45 @@ async function renderAfterStep(page, info) {
1619
1708
  : "";
1620
1709
  const paceHtml =
1621
1710
  info.paceBadge || info.paceLabel
1622
- ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
1711
+ ? "<div class=\\\"wg-pace\\\" data-pace-kind=\\\"" +
1623
1712
  escA(info.paceKind || "normal") +
1624
- "\\"><span class=\\"wg-pace-badge\\">" +
1713
+ "\\\"><span class=\\\"wg-pace-badge\\\">" +
1625
1714
  escA(info.paceBadge || "1x") +
1626
1715
  "</span><span>" +
1627
1716
  escA(info.paceLabel || "") +
1628
1717
  "</span></div>"
1629
1718
  : "";
1630
1719
  panel.innerHTML =
1631
- "<div id=\\"wg-progress\\"><div id=\\"wg-progress-bar\\" style=\\"width:" + pct + "%\\"></div></div>" +
1720
+ "<div id=\\\"wg-progress\\\"><div id=\\\"wg-progress-bar\\\" style=\\\"width:" + pct + "%\\\"></div></div>" +
1632
1721
  episodesHtml +
1633
1722
  paceHtml +
1634
- "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1723
+ "<div id=\\\"wg-modules\\\" class=\\\"" + modulesClass + "\\\">" + modulesHtml + "</div>" +
1635
1724
  "<h3>" + heading + "</h3>" +
1636
1725
  ((() => {
1637
1726
  const list = info.todos || [];
1638
1727
  if (!list.length) return "";
1639
1728
  return (
1640
- "<ul id=\"wg-todos\">" +
1729
+ '<ul id="wg-todos">' +
1641
1730
  list
1642
1731
  .map((t) => {
1643
1732
  const cls = t.current ? "wg-todo-current" : t.done ? "wg-todo-done" : "wg-todo-pending";
1644
- const mark = t.done ? "\u2713" : t.current ? "\u2192" : "\u25CB";
1733
+ const mark = t.done
1734
+ ? String.fromCharCode(0x2713)
1735
+ : t.current
1736
+ ? String.fromCharCode(0x2192)
1737
+ : String.fromCharCode(0x25cb);
1645
1738
  return (
1646
- "<li class=\"" +
1739
+ '<li class="' +
1647
1740
  cls +
1648
- "\"><span class=\"wg-todo-mark\">" +
1741
+ '"><span class="wg-todo-mark">' +
1649
1742
  mark +
1650
- "</span><span>" +
1743
+ '</span><span>' +
1651
1744
  escA(t.text || "") +
1652
- "</span></li>"
1745
+ '</span></li>'
1653
1746
  );
1654
1747
  })
1655
1748
  .join("") +
1656
- "</ul>"
1749
+ '</ul>'
1657
1750
  );
1658
1751
  })()) +
1659
1752
  resultHtml +
@@ -1722,13 +1815,13 @@ async function renderStepError(page, info) {
1722
1815
  if (!isNewPanel) panel.classList.add("wg-in");
1723
1816
  const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
1724
1817
  const episodesHtml = info.allEpisodes && info.allEpisodes.length > 0
1725
- ? "<div id=\\"wg-episodes\\">" +
1818
+ ? "<div id=\\\"wg-episodes\\\">" +
1726
1819
  info.allEpisodes
1727
1820
  .map((e) => {
1728
1821
  const cls = e.episodeNumber < info.episodeNumber ? "wg-ep-done"
1729
1822
  : e.episodeNumber === info.episodeNumber ? "wg-ep-current"
1730
1823
  : "wg-ep-upcoming";
1731
- return "<span class=\\"wg-ep-tab " + cls + "\\">Episode " + e.episodeNumber + ": " +
1824
+ return "<span class=\\\"wg-ep-tab " + cls + "\\\">Episode " + e.episodeNumber + ": " +
1732
1825
  esc(e.episodeTitle || "") + "</span>";
1733
1826
  })
1734
1827
  .join("") +
@@ -1739,8 +1832,8 @@ async function renderStepError(page, info) {
1739
1832
  .map((name, idx) => {
1740
1833
  const cls = idx === info.moduleIndex ? "wg-mod-current" : idx < info.moduleIndex ? "wg-mod-done" : "wg-mod-upcoming";
1741
1834
  const desc = info.allDescriptions && info.allDescriptions[idx];
1742
- const titleAttr = desc ? " title=\\"" + esc(desc) + "\\"" : "";
1743
- return "<span class=\\"wg-mod " + cls + "\\"" + titleAttr + ">" + name + "</span>";
1835
+ const titleAttr = desc ? " title=\\\"" + esc(desc) + "\\\"" : "";
1836
+ return "<span class=\\\"wg-mod " + cls + "\\\"" + titleAttr + ">" + name + "</span>";
1744
1837
  })
1745
1838
  .join("");
1746
1839
  const retryLabel = info.episodeNumber !== undefined ? "Retry Episode " + info.episodeNumber : "Retry";
@@ -1748,17 +1841,17 @@ async function renderStepError(page, info) {
1748
1841
  ? "Expected outcome - Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + " failed as intended"
1749
1842
  : "Step " + (info.index + 1) + " / " + info.total + " - " + info.blockName + " threw";
1750
1843
  const reasonHtml = isExpected
1751
- ? "<div class=\\"wg-expected-reason\\">" + esc(info.expectedFailureReason) + "</div>"
1844
+ ? "<div class=\\\"wg-expected-reason\\\">" + esc(info.expectedFailureReason) + "</div>"
1752
1845
  : "";
1753
1846
  panel.innerHTML =
1754
1847
  episodesHtml +
1755
- "<div id=\\"wg-modules\\" class=\\"" + modulesClass + "\\">" + modulesHtml + "</div>" +
1756
- "<h3 class=\\"" + (isExpected ? "wg-expected-heading" : "wg-error-heading") + "\\">" + headingText + "</h3>" +
1848
+ "<div id=\\\"wg-modules\\\" class=\\\"" + modulesClass + "\\\">" + modulesHtml + "</div>" +
1849
+ "<h3 class=\\\"" + (isExpected ? "wg-expected-heading" : "wg-error-heading") + "\\\">" + headingText + "</h3>" +
1757
1850
  reasonHtml +
1758
- "<div class=\\"wg-error-msg\\">" + esc(info.message) + "</div>" +
1759
- "<div class=\\"wg-error-actions\\">" +
1760
- "<button id=\\"wg-error-retry\\" class=\\"wg-error-retry\\">" + esc(retryLabel) + "</button>" +
1761
- "<button id=\\"wg-run\\" class=\\"wg-error-stop\\">Stop</button>" +
1851
+ "<div class=\\\"wg-error-msg\\\">" + esc(info.message) + "</div>" +
1852
+ "<div class=\\\"wg-error-actions\\\">" +
1853
+ "<button id=\\\"wg-error-retry\\\" class=\\\"wg-error-retry\\\">" + esc(retryLabel) + "</button>" +
1854
+ "<button id=\\\"wg-run\\\" class=\\\"wg-error-stop\\\">Stop</button>" +
1762
1855
  "</div>";
1763
1856
  if (isNewPanel) {
1764
1857
  document.documentElement.appendChild(panel);
@@ -2022,25 +2115,25 @@ async function presentSlides(page, slides, _gate, opts) {
2022
2115
  String(t).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
2023
2116
  const episodeLine =
2024
2117
  info.episodeNumber !== undefined && info.episodeNumber !== null
2025
- ? "<div class=\\"wg-episode\\">Episode " +
2118
+ ? "<div class=\\\"wg-episode\\\">Episode " +
2026
2119
  info.episodeNumber +
2027
2120
  (info.episodeTitle ? ": " + esc(info.episodeTitle) : "") +
2028
2121
  " · yap slides</div>"
2029
2122
  : "";
2030
2123
  const paceHtml =
2031
2124
  info.paceBadge || info.paceLabel
2032
- ? "<div class=\\"wg-pace\\" data-pace-kind=\\"" +
2125
+ ? "<div class=\\\"wg-pace\\\" data-pace-kind=\\\"" +
2033
2126
  esc(info.paceKind || "normal") +
2034
- "\\"><span class=\\"wg-pace-badge\\">" +
2127
+ "\\\"><span class=\\\"wg-pace-badge\\\">" +
2035
2128
  esc(info.paceBadge || "1x") +
2036
2129
  "</span><span>" +
2037
2130
  esc(info.paceLabel || "") +
2038
2131
  "</span></div>"
2039
2132
  : "";
2040
2133
  panel.innerHTML =
2041
- "<div class=\\"wg-chrome\\"><span class=\\"wg-chrome-title\\">waygraph demo</span>" +
2042
- "<button type=\\"button\\" class=\\"wg-hide-btn\\" data-wg-toggle=\\"1\\">Hide</button></div>" +
2043
- "<div class=\\"wg-body\\">" +
2134
+ "<div class=\\\"wg-chrome\\\"><span class=\\\"wg-chrome-title\\\">waygraph demo</span>" +
2135
+ "<button type=\\\"button\\\" class=\\\"wg-hide-btn\\\" data-wg-toggle=\\\"1\\\">Hide</button></div>" +
2136
+ "<div class=\\\"wg-body\\\">" +
2044
2137
  episodeLine +
2045
2138
  paceHtml +
2046
2139
  "<h3>Slide " +
@@ -2050,35 +2143,35 @@ async function presentSlides(page, slides, _gate, opts) {
2050
2143
  (info.blockName ? " · " + esc(info.blockName) : "") +
2051
2144
  "</h3>" +
2052
2145
  (info.tag
2053
- ? "<div style=\\"border:none;padding:0;margin:0 0 6px;font-size:11px;color:#c9a6ff\\">" +
2146
+ ? "<div style=\\\"border:none;padding:0;margin:0 0 6px;font-size:11px;color:#c9a6ff\\\">" +
2054
2147
  esc(info.tag) +
2055
2148
  "</div>"
2056
2149
  : "") +
2057
- "<div class=\\"wg-narration\\">" +
2150
+ "<div class=\\\"wg-narration\\\">" +
2058
2151
  esc(info.caption) +
2059
2152
  "</div>" +
2060
2153
  (info.detail
2061
- ? "<div class=\\"wg-result-pretty\\" style=\\"margin:0 0 12px;color:#f0e8ff\\">" +
2154
+ ? "<div class=\\\"wg-result-pretty\\\" style=\\\"margin:0 0 12px;color:#f0e8ff\\\">" +
2062
2155
  esc(info.detail) +
2063
2156
  "</div>"
2064
2157
  : "") +
2065
2158
  (info.dwellMs != null
2066
- ? "<div class=\\"wg-auto\\" id=\\"wg-dwell-hint\\" style=\\"margin:0 0 8px\\">Hold " +
2159
+ ? "<div class=\\\"wg-auto\\\" id=\\\"wg-dwell-hint\\\" style=\\\"margin:0 0 8px\\\">Hold " +
2067
2160
  (info.dwellMs / 1000).toFixed(1) +
2068
2161
  "s before next...</div>"
2069
2162
  : "") +
2070
- "<div class=\\"wg-autoplay-row\\"><label><input type=\\"checkbox\\" id=\\"wg-autoplay-cb\\"" +
2163
+ "<div class=\\\"wg-autoplay-row\\\"><label><input type=\\\"checkbox\\\" id=\\\"wg-autoplay-cb\\\"" +
2071
2164
  (info.autoNow ? " checked" : "") +
2072
2165
  "> Auto-advance</label></div>" +
2073
- "<div id=\\"wg-gate-manual\\"" +
2074
- (info.autoNow ? " style=\\"display:none\\"" : "") +
2075
- "><button type=\\"button\\" id=\\"wg-run\\"" +
2166
+ "<div id=\\\"wg-gate-manual\\\"" +
2167
+ (info.autoNow ? " style=\\\"display:none\\\"" : "") +
2168
+ "><button type=\\\"button\\\" id=\\\"wg-run\\\"" +
2076
2169
  (info.dwellMs != null ? " disabled" : "") +
2077
2170
  ">" +
2078
2171
  (info.isLast ? "Continue \\u25B6" : "Next slide \\u25B6") +
2079
2172
  "</button></div>" +
2080
- "<div id=\\"wg-gate-auto\\" class=\\"wg-auto\\"" +
2081
- (info.autoNow ? "" : " style=\\"display:none\\"") +
2173
+ "<div id=\\\"wg-gate-auto\\\" class=\\\"wg-auto\\\"" +
2174
+ (info.autoNow ? "" : " style=\\\"display:none\\\"") +
2082
2175
  ">Auto-advancing...</div>" +
2083
2176
  "</div>";
2084
2177
  if (isNew) {
@@ -2243,7 +2336,7 @@ async function teardownOverlay(page) {
2243
2336
  window.__wgRingTrack = null;
2244
2337
  }
2245
2338
  window.__wgNarrateOwnsRing = false;
2246
- for (const id of ["wg-panel", "wg-ring", "wg-ring-label", "wg-cursor", "wg-click-pulse", "wg-banner", "wg-todo-modal"]) {
2339
+ for (const id of ["wg-panel", "wg-ring", "wg-ring-label", "wg-cursor", "wg-click-pulse", "wg-banner", "wg-todo-modal", "wg-focus-veil"]) {
2247
2340
  const el = document.getElementById(id);
2248
2341
  if (el) el.remove();
2249
2342
  }
@@ -89,6 +89,14 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
89
89
  * Spotlight: dim the rest of the page, keep this target as the focal point.
90
90
  */
91
91
  focus?: boolean;
92
+ /**
93
+ * After camera zoom settles, pan with the real mouse pointer (Screen Studio).
94
+ * Default: on when `zoom` > 1. Set false to lock the camera on the target.
95
+ * Alias: `follow`.
96
+ */
97
+ followMouse?: boolean;
98
+ /** Alias for {@link WaygraphHighlightStub.followMouse}. */
99
+ follow?: boolean;
92
100
  /**
93
101
  * Caption text color (CSS color). Overrides tone label color when set.
94
102
  * Aliases accepted at runtime: `color`, `labelColor`, `textColor`.
@@ -101,7 +109,7 @@ export interface WaygraphHighlightStub extends FixtureDurationFields {
101
109
  cursor?: boolean;
102
110
  }
103
111
  /** Flow fixture patch: label required; selector optional (inherits from stub). */
104
- export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone" | "size" | "weight" | "zoom" | "focus" | "color" | "cursor"> & FixtureDurationFields;
112
+ export type WaygraphHighlightFixture = Partial<Pick<WaygraphHighlightStub, "selector">> & Required<Pick<WaygraphHighlightStub, "label">> & Pick<WaygraphHighlightStub, "detail" | "tag" | "tone" | "size" | "weight" | "zoom" | "focus" | "followMouse" | "follow" | "color" | "cursor"> & FixtureDurationFields;
105
113
  export type HighlightStubPhase = Record<string, WaygraphHighlightStub>;
106
114
  /**
107
115
  * Open stub lifecycle context. Authors set rings + episode fixtures freely —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.12.31",
3
+ "version": "0.12.33",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",