waygraph 0.15.30 → 0.15.31

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.
@@ -1,4 +1,5 @@
1
1
  import { createInterface } from "node:readline/promises";
2
+ import { ensureShadowRoot } from "./ui/shadow.js";
2
3
  import { stdin as input, stdout as output } from "node:process";
3
4
  import { existsSync, readFileSync } from "node:fs";
4
5
  import { join } from "node:path";
@@ -168,7 +169,7 @@ export async function ensureMem(entry, mem, cli) {
168
169
  async function setAutoPanelRunning(page, label) {
169
170
  await page
170
171
  .evaluate((label) => {
171
- const panel = document.getElementById("wg-auto-panel");
172
+ const panel = __wgById("wg-auto-panel");
172
173
  if (!panel)
173
174
  return;
174
175
  const body = "<h3>waygraph auto</h3><div style='padding:12px 0;color:#c9a6ff'>" +
@@ -291,9 +292,10 @@ function memKeysForMenu(menu, library) {
291
292
  async function syncPanelMem(page, mem, keys) {
292
293
  if (keys.length === 0)
293
294
  return;
295
+ await ensureShadowRoot(page);
294
296
  const edits = (await page.evaluate(() => {
295
297
  const out = {};
296
- document.querySelectorAll("textarea.wg-mem-input").forEach((ta) => {
298
+ __wgQA("textarea.wg-mem-input").forEach((ta) => {
297
299
  const key = ta.getAttribute("data-mem-key");
298
300
  if (key)
299
301
  out[key] = ta.value;
@@ -382,14 +384,15 @@ async function headfulPick(page, menu, library, mem, waitPick) {
382
384
  html =
383
385
  `<style id="wg-auto-style">${PANEL_CSS}</style><div id="wg-auto-panel">` +
384
386
  html.slice(html.indexOf("<h3>"));
387
+ await ensureShadowRoot(page);
385
388
  await page.evaluate(`((inner) => {
386
- let panel = document.getElementById("wg-auto-panel");
389
+ let panel = __wgById("wg-auto-panel");
387
390
  if (!panel) {
388
391
  panel = document.createElement("div");
389
- document.documentElement.appendChild(panel);
392
+ __wgAdd(panel);
390
393
  }
391
394
  panel.outerHTML = inner;
392
- const root = document.getElementById("wg-auto-panel");
395
+ const root = __wgById("wg-auto-panel");
393
396
  if (!root) return;
394
397
  window.__wgWirePanelChrome = (el, storageKey, chromeTitle) => {
395
398
  if (!el) return;
@@ -446,7 +449,7 @@ async function headfulPick(page, menu, library, mem, waitPick) {
446
449
  window.__wgOverlayBeacon =
447
450
  window.__wgOverlayBeacon ||
448
451
  (() =>
449
- Array.from(document.querySelectorAll('[data-wg-ui="1"]')).map((el) => {
452
+ Array.from(__wgQA('[data-wg-ui="1"]')).map((el) => {
450
453
  const r = el.getBoundingClientRect();
451
454
  const st = getComputedStyle(el);
452
455
  return {
@@ -98,22 +98,22 @@ export async function showPilotFixtures(page, fixtures) {
98
98
  if (w.__wgPilotFxHideTimer)
99
99
  clearTimeout(w.__wgPilotFxHideTimer);
100
100
  const clearFocus = () => {
101
- const veil = document.getElementById("wg-pilot-fx-focus");
101
+ const veil = __wgById("wg-pilot-fx-focus");
102
102
  if (!veil)
103
103
  return;
104
104
  veil.classList.remove("wg-in");
105
105
  setTimeout(() => {
106
- const v = document.getElementById("wg-pilot-fx-focus");
106
+ const v = __wgById("wg-pilot-fx-focus");
107
107
  if (v && !v.classList.contains("wg-in"))
108
108
  v.remove();
109
109
  }, 320);
110
110
  };
111
111
  const applyFocus = (box) => {
112
- let veil = document.getElementById("wg-pilot-fx-focus");
112
+ let veil = __wgById("wg-pilot-fx-focus");
113
113
  if (!veil) {
114
114
  veil = document.createElement("div");
115
115
  veil.id = "wg-pilot-fx-focus";
116
- document.documentElement.appendChild(veil);
116
+ __wgAdd(veil);
117
117
  }
118
118
  const pad = 10;
119
119
  veil.style.left = `${Math.max(0, box.x - pad)}px`;
@@ -125,11 +125,11 @@ export async function showPilotFixtures(page, fixtures) {
125
125
  };
126
126
  const setZoomBadge = (scale) => {
127
127
  const level = Number.isFinite(scale) && scale > 0 ? scale : 1;
128
- let badge = document.getElementById("wg-pilot-fx-zoom");
128
+ let badge = __wgById("wg-pilot-fx-zoom");
129
129
  if (!badge) {
130
130
  badge = document.createElement("div");
131
131
  badge.id = "wg-pilot-fx-zoom";
132
- document.documentElement.appendChild(badge);
132
+ __wgAdd(badge);
133
133
  }
134
134
  const zoomed = level > 1.001 || level < 0.999;
135
135
  badge.dataset.zoomed = zoomed ? "1" : "0";
@@ -143,27 +143,27 @@ export async function showPilotFixtures(page, fixtures) {
143
143
  root.style.removeProperty("transform");
144
144
  root.style.removeProperty("transform-origin");
145
145
  root.style.removeProperty("transition");
146
- document.getElementById("wg-pilot-fx-zoom")?.remove();
146
+ __wgById("wg-pilot-fx-zoom")?.remove();
147
147
  };
148
148
  const setDeviceChip = (label) => {
149
- document.getElementById("wg-pilot-fx-device")?.remove();
149
+ __wgById("wg-pilot-fx-device")?.remove();
150
150
  if (!label)
151
151
  return;
152
152
  const chip = document.createElement("div");
153
153
  chip.id = "wg-pilot-fx-device";
154
154
  chip.dataset.preset = label;
155
155
  chip.textContent = `device \u00b7 ${label}`;
156
- document.documentElement.appendChild(chip);
156
+ __wgAdd(chip);
157
157
  };
158
158
  w.__wgPilotFxZoomOutOnHide = zoomOut;
159
159
  const clearFx = () => {
160
- document.querySelectorAll(".wg-pilot-fx-ring, .wg-pilot-fx-label").forEach((el) => el.remove());
161
- document.getElementById("wg-pilot-fx-todos")?.remove();
160
+ __wgQA(".wg-pilot-fx-ring, .wg-pilot-fx-label").forEach((el) => el.remove());
161
+ __wgById("wg-pilot-fx-todos")?.remove();
162
162
  clearFocus();
163
163
  if (w.__wgPilotFxZoomOutOnHide !== false || clearAll)
164
164
  clearZoom();
165
165
  if (clearAll) {
166
- document.getElementById("wg-pilot-fx-device")?.remove();
166
+ __wgById("wg-pilot-fx-device")?.remove();
167
167
  clearZoom();
168
168
  }
169
169
  };
@@ -206,8 +206,8 @@ export async function showPilotFixtures(page, fixtures) {
206
206
  if (ring.color) {
207
207
  labelEl.style.color = ring.color;
208
208
  }
209
- document.documentElement.appendChild(ringEl);
210
- document.documentElement.appendChild(labelEl);
209
+ __wgAdd(ringEl);
210
+ __wgAdd(labelEl);
211
211
  const lw = labelEl.offsetWidth;
212
212
  const lh = labelEl.offsetHeight;
213
213
  let labelLeft = rect.left - pad;
@@ -266,18 +266,18 @@ export async function showPilotFixtures(page, fixtures) {
266
266
  more.textContent = "+" + (todos.length - (end - start)) + " more";
267
267
  dock.appendChild(more);
268
268
  }
269
- document.documentElement.appendChild(dock);
269
+ __wgAdd(dock);
270
270
  }
271
271
  if (focusBox)
272
272
  applyFocus(focusBox);
273
273
  // Dim todos under rings / focus so captions stay readable.
274
- const dockEl = document.getElementById("wg-pilot-fx-todos");
274
+ const dockEl = __wgById("wg-pilot-fx-todos");
275
275
  if (dockEl && todoUi.behindRing !== false && (painted > 0 || focusBox)) {
276
276
  dockEl.classList.add("wg-todo-behind");
277
277
  }
278
278
  // Collision flip: if a painted ring intersects the dock, flip side once.
279
279
  if (dockEl && todoUi.collision !== false && painted > 0) {
280
- const firstRing = document.querySelector(".wg-pilot-fx-ring");
280
+ const firstRing = __wgQ(".wg-pilot-fx-ring");
281
281
  if (firstRing) {
282
282
  const rr = firstRing.getBoundingClientRect();
283
283
  const dr = dockEl.getBoundingClientRect();
@@ -10,9 +10,9 @@ export async function updatePilotOverlay(page, info) {
10
10
  await ensureInstalled(page);
11
11
  await page
12
12
  .evaluate(async ({ sessionId, snapshot, graph }) => {
13
- const badge = document.getElementById("wg-pilot-badge");
14
- const current = document.getElementById("wg-pilot-panel-current");
15
- const graphPanel = document.getElementById("wg-pilot-panel-graph");
13
+ const badge = __wgById("wg-pilot-badge");
14
+ const current = __wgById("wg-pilot-panel-current");
15
+ const graphPanel = __wgById("wg-pilot-panel-graph");
16
16
  if (!badge || !current || !graphPanel)
17
17
  return;
18
18
  const idPart = sessionId ? `session ${sessionId}` : "no session id";
@@ -1,5 +1,6 @@
1
1
  // Split out of the former 950-line pilot-overlay.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
2
  import { SURFACE } from "../ui/tokens.js";
3
+ import { ensureShadowRoot, installShadowRoot } from "../ui/shadow.js";
3
4
  import { pilotFxRingCss } from "../ui/components/ring.js";
4
5
  import { WAYGRAPH_RING_CSS } from "../highlights.js";
5
6
  // Real, direct user request: this overlay's own colors were an improvised
@@ -109,12 +110,9 @@ ${pilotFxRingCss()}
109
110
  ` + WAYGRAPH_RING_CSS;
110
111
  /** Browser-side shell install — runs on every document via context.addInitScript. */
111
112
  function installPilotOverlayShell(css) {
112
- if (document.getElementById("wg-pilot-overlay"))
113
+ if (__wgById("wg-pilot-overlay"))
113
114
  return;
114
- const style = document.createElement("style");
115
- style.id = "wg-pilot-overlay-css";
116
- style.textContent = css;
117
- document.documentElement.appendChild(style);
115
+ __wgCss(css, "wg-pilot-overlay-css");
118
116
  const root = document.createElement("div");
119
117
  root.id = "wg-pilot-overlay";
120
118
  root.innerHTML =
@@ -125,36 +123,36 @@ function installPilotOverlayShell(css) {
125
123
  '<div id="wg-pilot-panel-current"></div>' +
126
124
  '<div id="wg-pilot-panel-graph" style="display:none"></div>' +
127
125
  "</div>";
128
- document.documentElement.appendChild(root);
129
- document.getElementById("wg-pilot-badge").addEventListener("click", () => {
130
- document.getElementById("wg-pilot-panel").classList.toggle("wg-pilot-open");
126
+ __wgAdd(root);
127
+ __wgById("wg-pilot-badge").addEventListener("click", () => {
128
+ __wgById("wg-pilot-panel").classList.toggle("wg-pilot-open");
131
129
  });
132
- document.getElementById("wg-pilot-view-current").addEventListener("click", () => {
133
- document.getElementById("wg-pilot-view-current").classList.add("wg-pilot-tab-active");
134
- document.getElementById("wg-pilot-view-graph").classList.remove("wg-pilot-tab-active");
135
- document.getElementById("wg-pilot-panel-current").style.display = "";
136
- document.getElementById("wg-pilot-panel-graph").style.display = "none";
130
+ __wgById("wg-pilot-view-current").addEventListener("click", () => {
131
+ __wgById("wg-pilot-view-current").classList.add("wg-pilot-tab-active");
132
+ __wgById("wg-pilot-view-graph").classList.remove("wg-pilot-tab-active");
133
+ __wgById("wg-pilot-panel-current").style.display = "";
134
+ __wgById("wg-pilot-panel-graph").style.display = "none";
137
135
  });
138
- document.getElementById("wg-pilot-view-graph").addEventListener("click", () => {
139
- document.getElementById("wg-pilot-view-graph").classList.add("wg-pilot-tab-active");
140
- document.getElementById("wg-pilot-view-current").classList.remove("wg-pilot-tab-active");
141
- document.getElementById("wg-pilot-panel-graph").style.display = "";
142
- document.getElementById("wg-pilot-panel-current").style.display = "none";
136
+ __wgById("wg-pilot-view-graph").addEventListener("click", () => {
137
+ __wgById("wg-pilot-view-graph").classList.add("wg-pilot-tab-active");
138
+ __wgById("wg-pilot-view-current").classList.remove("wg-pilot-tab-active");
139
+ __wgById("wg-pilot-panel-graph").style.display = "";
140
+ __wgById("wg-pilot-panel-current").style.display = "none";
143
141
  });
144
- if (!document.getElementById("wg-pilot-activity")) {
142
+ if (!__wgById("wg-pilot-activity")) {
145
143
  const toast = document.createElement("div");
146
144
  toast.id = "wg-pilot-activity";
147
- document.documentElement.appendChild(toast);
145
+ __wgAdd(toast);
148
146
  }
149
- if (!document.getElementById("wg-ring")) {
147
+ if (!__wgById("wg-ring")) {
150
148
  const ring = document.createElement("div");
151
149
  ring.id = "wg-ring";
152
- document.documentElement.appendChild(ring);
150
+ __wgAdd(ring);
153
151
  }
154
- if (!document.getElementById("wg-ring-label")) {
152
+ if (!__wgById("wg-ring-label")) {
155
153
  const ringLabel = document.createElement("div");
156
154
  ringLabel.id = "wg-ring-label";
157
- document.documentElement.appendChild(ringLabel);
155
+ __wgAdd(ringLabel);
158
156
  }
159
157
  }
160
158
  /**
@@ -163,10 +161,12 @@ function installPilotOverlayShell(css) {
163
161
  * page is blank or replaced.
164
162
  */
165
163
  export async function installPersistentPilotOverlay(context) {
164
+ await context.addInitScript(installShadowRoot);
166
165
  await context.addInitScript(installPilotOverlayShell, OVERLAY_CSS);
167
166
  }
168
167
  /** Idempotent - safe to call before every update, matching narrate mode's own precedent. */
169
168
  export async function ensureInstalled(page) {
169
+ await ensureShadowRoot(page);
170
170
  await page.evaluate(installPilotOverlayShell, OVERLAY_CSS).catch(() => { });
171
171
  }
172
172
  /**
@@ -183,7 +183,7 @@ export async function showPilotActivity(page, text) {
183
183
  await ensureInstalled(page);
184
184
  await page
185
185
  .evaluate((text) => {
186
- const toast = document.getElementById("wg-pilot-activity");
186
+ const toast = __wgById("wg-pilot-activity");
187
187
  if (!toast)
188
188
  return;
189
189
  toast.textContent = text;
@@ -227,8 +227,8 @@ export async function showPilotVision(page, selector, label, tone = "auto") {
227
227
  await ensureInstalled(page);
228
228
  await page
229
229
  .evaluate(({ selector, label, tone }) => {
230
- const ring = document.getElementById("wg-ring");
231
- const tag = document.getElementById("wg-ring-label");
230
+ const ring = __wgById("wg-ring");
231
+ const tag = __wgById("wg-ring-label");
232
232
  if (!ring || !tag)
233
233
  return;
234
234
  const w = window;
@@ -48,7 +48,7 @@ export async function playSwipeTrail(page, opts) {
48
48
  : "left";
49
49
  const ms = Math.max(280, Math.min(1400, Number(payload.ms) || 720));
50
50
  const label = payload.label || "swipe";
51
- let layer = document.getElementById("wg-swipe-layer");
51
+ let layer = __wgById("wg-swipe-layer");
52
52
  if (layer) layer.remove();
53
53
  layer = document.createElement("div");
54
54
  layer.id = "wg-swipe-layer";
@@ -67,7 +67,7 @@ export async function playSwipeTrail(page, opts) {
67
67
  "border:1px solid rgba(56,189,248,.55);font:700 12px/1.2 system-ui,sans-serif;";
68
68
  layer.appendChild(lab);
69
69
  layer.appendChild(finger);
70
- document.documentElement.appendChild(layer);
70
+ __wgAdd(layer);
71
71
  const vw = window.innerWidth || 390;
72
72
  const vh = window.innerHeight || 844;
73
73
  const pad = Math.min(vw, vh) * 0.18;
@@ -121,7 +121,7 @@ export async function playSwipeTrail(page, opts) {
121
121
  });
122
122
  if (i === steps) {
123
123
  setTimeout(() => {
124
- const el = document.getElementById("wg-swipe-layer");
124
+ const el = __wgById("wg-swipe-layer");
125
125
  if (el) el.remove();
126
126
  }, 400);
127
127
  }
@@ -12,7 +12,7 @@ export function installBanner({ title, favicon, bannerPos, todoPos, envAutoplay,
12
12
  }
13
13
  };
14
14
  if (title) {
15
- let banner = document.getElementById("wg-banner");
15
+ let banner = __wgById("wg-banner");
16
16
  if (!banner) {
17
17
  banner = document.createElement("div");
18
18
  banner.id = "wg-banner";
@@ -39,7 +39,7 @@ export function installBanner({ title, favicon, bannerPos, todoPos, envAutoplay,
39
39
  text.textContent = title;
40
40
  banner.appendChild(tag);
41
41
  banner.appendChild(text);
42
- document.documentElement.appendChild(banner);
42
+ __wgAdd(banner);
43
43
  } else {
44
44
  // Fixture / episode title changes every step - update in place
45
45
  // (banner is created once; do not leave the first step's text stuck).
@@ -19,17 +19,17 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
19
19
  } catch {
20
20
  /* private mode / blocked storage - falls back to manual gating */
21
21
  }
22
- if (!document.getElementById("wg-ring")) {
22
+ if (!__wgById("wg-ring")) {
23
23
  const ring = document.createElement("div");
24
24
  ring.id = "wg-ring";
25
- document.documentElement.appendChild(ring);
25
+ __wgAdd(ring);
26
26
  }
27
- if (!document.getElementById("wg-ring-label")) {
27
+ if (!__wgById("wg-ring-label")) {
28
28
  const ringLabel = document.createElement("div");
29
29
  ringLabel.id = "wg-ring-label";
30
- document.documentElement.appendChild(ringLabel);
30
+ __wgAdd(ringLabel);
31
31
  }
32
- if (!document.getElementById("wg-cursor")) {
32
+ if (!__wgById("wg-cursor")) {
33
33
  const cursor = document.createElement("div");
34
34
  cursor.id = "wg-cursor";
35
35
  // Dark fill + white stroke, same as help-center-clip-engine's own
@@ -39,15 +39,15 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
39
39
  "<svg viewBox='0 0 32 32' width='24' height='24'>" +
40
40
  "<path fill='#0C0C1A' stroke='#fff' stroke-width='1.4' stroke-linejoin='round' " +
41
41
  "d='M6 3.5l1.4 22.5 5.8-5.4 4.2 9.4 3.6-1.6-4.2-9.2H26z'/></svg>";
42
- document.documentElement.appendChild(cursor);
42
+ __wgAdd(cursor);
43
43
  }
44
- if (!document.getElementById("wg-click-pulse")) {
44
+ if (!__wgById("wg-click-pulse")) {
45
45
  const pulse = document.createElement("div");
46
46
  pulse.id = "wg-click-pulse";
47
- document.documentElement.appendChild(pulse);
47
+ __wgAdd(pulse);
48
48
  }
49
49
  window.__wgMoveCursorTo = (x, y, ms, instant) => {
50
- const cursor = document.getElementById("wg-cursor");
50
+ const cursor = __wgById("wg-cursor");
51
51
  if (!cursor) return;
52
52
  cursor.style.setProperty("--wg-cursor-ms", (ms || 600) + "ms");
53
53
  if (instant) {
@@ -62,12 +62,12 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
62
62
  cursor.style.opacity = "1";
63
63
  };
64
64
  window.__wgHideCursor = () => {
65
- const cursor = document.getElementById("wg-cursor");
65
+ const cursor = __wgById("wg-cursor");
66
66
  if (cursor) cursor.style.opacity = "0";
67
67
  };
68
68
  window.__wgPositionRing = (box, label, tone, style) => {
69
- const ring = document.getElementById("wg-ring");
70
- const ringLabel = document.getElementById("wg-ring-label");
69
+ const ring = __wgById("wg-ring");
70
+ const ringLabel = __wgById("wg-ring-label");
71
71
  if (!ring || !ringLabel || !box) return;
72
72
  const raw = (tone || "planned") + "";
73
73
  const t =
@@ -119,12 +119,12 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
119
119
  window.__wgTodosSetBehind = (on) => {
120
120
  const ui = window.__wgTodoDockUi || {};
121
121
  if (ui.behindRing === false) {
122
- document.querySelectorAll(".wg-todo-dock, #wg-todo-dock").forEach((el) => {
122
+ __wgQA(".wg-todo-dock, #wg-todo-dock").forEach((el) => {
123
123
  el.classList.remove("wg-todo-behind");
124
124
  });
125
125
  return;
126
126
  }
127
- document.querySelectorAll(".wg-todo-dock, #wg-todo-dock").forEach((el) => {
127
+ __wgQA(".wg-todo-dock, #wg-todo-dock").forEach((el) => {
128
128
  el.classList.toggle("wg-todo-behind", !!on);
129
129
  });
130
130
  };
@@ -145,7 +145,7 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
145
145
  const r = el.getBoundingClientRect();
146
146
  return !(r.right < rx1 || r.left > rx2 || r.bottom < ry1 || r.top > ry2);
147
147
  };
148
- const docks = [...document.querySelectorAll(".wg-todo-dock, #wg-todo-dock")].filter(
148
+ const docks = [...__wgQA(".wg-todo-dock, #wg-todo-dock")].filter(
149
149
  (el, i, arr) => arr.indexOf(el) === i,
150
150
  );
151
151
  for (const dock of docks) {
@@ -194,7 +194,7 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
194
194
  tick();
195
195
  };
196
196
  window.__wgClickPulse = (x, y, tone) => {
197
- const pulse = document.getElementById("wg-click-pulse");
197
+ const pulse = __wgById("wg-click-pulse");
198
198
  if (!pulse) return;
199
199
  const raw = (tone || "planned") + "";
200
200
  pulse.dataset.tone =
@@ -214,14 +214,14 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
214
214
  // action finishes, not just its first sub-step.
215
215
  if (window.__wgNarrateOwnsRing) return;
216
216
  if (window.__wgStopRingFollow) window.__wgStopRingFollow();
217
- const ring = document.getElementById("wg-ring");
218
- const ringLabel = document.getElementById("wg-ring-label");
217
+ const ring = __wgById("wg-ring");
218
+ const ringLabel = __wgById("wg-ring-label");
219
219
  if (ring) ring.style.opacity = "0";
220
220
  if (ringLabel) ringLabel.style.opacity = "0";
221
221
  if (window.__wgClearFocus) window.__wgClearFocus();
222
222
  if (window.__wgTodosSetBehind) window.__wgTodosSetBehind(false);
223
223
  window.__wgTodoCollisionLocked = false;
224
- document.querySelectorAll(".wg-todo-dock[data-tucked], #wg-todo-dock[data-tucked]").forEach((el) => {
224
+ __wgQA(".wg-todo-dock[data-tucked], #wg-todo-dock[data-tucked]").forEach((el) => {
225
225
  delete el.dataset.tucked;
226
226
  });
227
227
  // Honor zoomOut:false - keep camera until next zoom / desktop clear.
@@ -236,11 +236,11 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
236
236
  }
237
237
  };
238
238
  window.__wgClearFocus = () => {
239
- const veil = document.getElementById("wg-focus-veil");
239
+ const veil = __wgById("wg-focus-veil");
240
240
  if (!veil) return;
241
241
  veil.classList.remove("wg-in");
242
242
  setTimeout(() => {
243
- const v = document.getElementById("wg-focus-veil");
243
+ const v = __wgById("wg-focus-veil");
244
244
  if (v && !v.classList.contains("wg-in")) v.remove();
245
245
  }, 320);
246
246
  };
@@ -249,12 +249,12 @@ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, t
249
249
  if (window.__wgClearFocus) window.__wgClearFocus();
250
250
  return;
251
251
  }
252
- let veil = document.getElementById("wg-focus-veil");
252
+ let veil = __wgById("wg-focus-veil");
253
253
  if (!veil) {
254
254
  veil = document.createElement("div");
255
255
  veil.id = "wg-focus-veil";
256
256
  veil.setAttribute("data-wg-ui", "1");
257
- document.documentElement.appendChild(veil);
257
+ __wgAdd(veil);
258
258
  }
259
259
  const pad = 10;
260
260
  veil.style.left = Math.max(0, box.x - pad) + "px";
@@ -10,7 +10,7 @@ export function installDevice({ title, favicon, bannerPos, todoPos, envAutoplay,
10
10
  ? payload.sync
11
11
  : "keep";
12
12
  const device = payload && payload.device ? payload.device : null;
13
- const cursor = document.getElementById("wg-cursor");
13
+ const cursor = __wgById("wg-cursor");
14
14
  const MOUSE_SVG =
15
15
  "<svg viewBox='0 0 32 32' width='24' height='24'>" +
16
16
  "<path fill='#0C0C1A' stroke='#fff' stroke-width='1.4' stroke-linejoin='round' " +
@@ -45,12 +45,12 @@ export function installDevice({ title, favicon, bannerPos, todoPos, envAutoplay,
45
45
  }
46
46
  };
47
47
  const ensureChip = (preset, touch, remain, orientation) => {
48
- let badge = document.getElementById("wg-device-badge");
48
+ let badge = __wgById("wg-device-badge");
49
49
  if (preset === "desktop" && !touch) {
50
50
  if (badge) {
51
51
  badge.classList.remove("wg-in");
52
52
  setTimeout(() => {
53
- const b = document.getElementById("wg-device-badge");
53
+ const b = __wgById("wg-device-badge");
54
54
  if (b) b.remove();
55
55
  }, 320);
56
56
  }
@@ -60,7 +60,7 @@ export function installDevice({ title, favicon, bannerPos, todoPos, envAutoplay,
60
60
  badge = document.createElement("div");
61
61
  badge.id = "wg-device-badge";
62
62
  badge.setAttribute("data-wg-ui", "1");
63
- document.documentElement.appendChild(badge);
63
+ __wgAdd(badge);
64
64
  }
65
65
  badge.dataset.preset = preset;
66
66
  badge.dataset.touch = touch ? "1" : "0";
@@ -85,7 +85,7 @@ export function installDevice({ title, favicon, bannerPos, todoPos, envAutoplay,
85
85
  badge.classList.add("wg-in");
86
86
  };
87
87
  const showToast = (preset, touch, title, sub, orientation) => {
88
- let toast = document.getElementById("wg-device-toast");
88
+ let toast = __wgById("wg-device-toast");
89
89
  if (toast && toast._wgTimer) {
90
90
  clearTimeout(toast._wgTimer);
91
91
  toast._wgTimer = null;
@@ -94,10 +94,10 @@ export function installDevice({ title, favicon, bannerPos, todoPos, envAutoplay,
94
94
  toast = document.createElement("div");
95
95
  toast.id = "wg-device-toast";
96
96
  toast.setAttribute("data-wg-ui", "1");
97
- document.documentElement.appendChild(toast);
97
+ __wgAdd(toast);
98
98
  }
99
99
  // Hide chip while toast is up (same corner).
100
- const badge = document.getElementById("wg-device-badge");
100
+ const badge = __wgById("wg-device-badge");
101
101
  if (badge) badge.classList.remove("wg-in");
102
102
  toast.dataset.preset = preset;
103
103
  toast.innerHTML =
@@ -115,7 +115,7 @@ export function installDevice({ title, favicon, bannerPos, todoPos, envAutoplay,
115
115
  toast.classList.add("wg-out");
116
116
  toast.classList.remove("wg-in");
117
117
  setTimeout(() => {
118
- const t = document.getElementById("wg-device-toast");
118
+ const t = __wgById("wg-device-toast");
119
119
  if (t) t.remove();
120
120
  if (preset !== "desktop" || touch) {
121
121
  ensureChip(preset, touch, true, orientation);
@@ -142,13 +142,13 @@ export function installDevice({ title, favicon, bannerPos, todoPos, envAutoplay,
142
142
  "landscape",
143
143
  );
144
144
  setTimeout(() => {
145
- const badge = document.getElementById("wg-device-badge");
145
+ const badge = __wgById("wg-device-badge");
146
146
  if (badge) badge.remove();
147
147
  }, 2600);
148
148
  } else {
149
- const badge = document.getElementById("wg-device-badge");
149
+ const badge = __wgById("wg-device-badge");
150
150
  if (badge) badge.remove();
151
- const toast = document.getElementById("wg-device-toast");
151
+ const toast = __wgById("wg-device-toast");
152
152
  if (toast) toast.remove();
153
153
  }
154
154
  window.__wgDevicePreset = "desktop";
@@ -69,7 +69,7 @@ export function installPanelChrome({ title, favicon, bannerPos, todoPos, envAuto
69
69
  } catch {
70
70
  /* ignore */
71
71
  }
72
- const runBtn = document.getElementById("wg-run");
72
+ const runBtn = __wgById("wg-run");
73
73
  const show = !!hidden && !auto && !!runBtn;
74
74
  miniNext.classList.toggle("wg-mini-next-show", show);
75
75
  if (runBtn) {
@@ -125,7 +125,7 @@ export function installPanelChrome({ title, favicon, bannerPos, todoPos, envAuto
125
125
  miniNext.dataset.wgWired = "1";
126
126
  miniNext.addEventListener("click", (e) => {
127
127
  e.stopPropagation();
128
- const runBtn = document.getElementById("wg-run");
128
+ const runBtn = __wgById("wg-run");
129
129
  if (runBtn && !runBtn.disabled) {
130
130
  runBtn.click();
131
131
  return;
@@ -133,7 +133,7 @@ export function installPanelChrome({ title, favicon, bannerPos, todoPos, envAuto
133
133
  if (typeof window.__wgNext === "function") window.__wgNext({});
134
134
  });
135
135
  }
136
- const autoCb = document.getElementById("wg-autoplay-cb");
136
+ const autoCb = __wgById("wg-autoplay-cb");
137
137
  if (autoCb && !autoCb.dataset.wgMiniWired) {
138
138
  autoCb.dataset.wgMiniWired = "1";
139
139
  autoCb.addEventListener("change", () => {
@@ -20,7 +20,7 @@ export function installStamp({ title, favicon, bannerPos, todoPos, envAutoplay,
20
20
  el.setAttribute("data-wg-ready", meta.ready === false ? "0" : "1");
21
21
  };
22
22
  window.__wgOverlayBeacon = () => {
23
- return Array.from(document.querySelectorAll("[data-wg-ui=\"1\"]")).map((el) => {
23
+ return Array.from(__wgQA("[data-wg-ui=\"1\"]")).map((el) => {
24
24
  const r = el.getBoundingClientRect();
25
25
  const st = getComputedStyle(el);
26
26
  return {
@@ -41,6 +41,6 @@ export function installStamp({ title, favicon, bannerPos, todoPos, envAutoplay,
41
41
  };
42
42
  });
43
43
  };
44
- const existingBanner = document.getElementById("wg-banner");
44
+ const existingBanner = __wgById("wg-banner");
45
45
  if (existingBanner) window.__wgStampModal(existingBanner, "banner", { ready: true });
46
46
  }
@@ -15,7 +15,7 @@ export function installSwipeZoom({ title, favicon, bannerPos, todoPos, envAutopl
15
15
  o.label ||
16
16
  (dir === "left" || dir === "right" ? "swipe" : "swipe") +
17
17
  (o.hint ? " \u00b7 " + o.hint : "");
18
- let layer = document.getElementById("wg-swipe-layer");
18
+ let layer = __wgById("wg-swipe-layer");
19
19
  if (layer && layer._wgTimer) {
20
20
  clearTimeout(layer._wgTimer);
21
21
  layer._wgTimer = null;
@@ -31,7 +31,7 @@ export function installSwipeZoom({ title, favicon, bannerPos, todoPos, envAutopl
31
31
  lab.textContent = label;
32
32
  layer.appendChild(lab);
33
33
  layer.appendChild(finger);
34
- document.documentElement.appendChild(layer);
34
+ __wgAdd(layer);
35
35
  const vw = window.innerWidth || 390;
36
36
  const vh = window.innerHeight || 844;
37
37
  const pad = Math.min(vw, vh) * 0.18;
@@ -89,7 +89,7 @@ export function installSwipeZoom({ title, favicon, bannerPos, todoPos, envAutopl
89
89
  finger.style.opacity = "0";
90
90
  layer.classList.remove("wg-in");
91
91
  layer._wgTimer = setTimeout(() => {
92
- const el = document.getElementById("wg-swipe-layer");
92
+ const el = __wgById("wg-swipe-layer");
93
93
  if (el) el.remove();
94
94
  }, 380);
95
95
  }
@@ -108,7 +108,7 @@ export function installSwipeZoom({ title, favicon, bannerPos, todoPos, envAutopl
108
108
  delete el.dataset.wgZoomed;
109
109
  });
110
110
  // Undo any prior shell-camera experiment - restore device fit scale only.
111
- const shell = document.getElementById("wg-device-shell");
111
+ const shell = __wgById("wg-device-shell");
112
112
  if (shell && shell.dataset.wgCamBase != null) {
113
113
  shell.style.transform = shell.dataset.wgCamBase;
114
114
  if (shell.dataset.wgCamOrigin) {
@@ -130,12 +130,12 @@ export function installSwipeZoom({ title, favicon, bannerPos, todoPos, envAutopl
130
130
  window.__wgSetZoomBadge = (scale, sel) => {
131
131
  const n = Number(scale);
132
132
  const level = Number.isFinite(n) && n > 0 ? n : 1;
133
- let badge = document.getElementById("wg-zoom-badge");
133
+ let badge = __wgById("wg-zoom-badge");
134
134
  if (!badge) {
135
135
  badge = document.createElement("div");
136
136
  badge.id = "wg-zoom-badge";
137
137
  badge.setAttribute("data-wg-ui", "1");
138
- document.documentElement.appendChild(badge);
138
+ __wgAdd(badge);
139
139
  }
140
140
  const zoomed = level > 1.001 || level < 0.999;
141
141
  badge.dataset.zoomed = zoomed ? "1" : "0";
@@ -156,12 +156,12 @@ export function installSwipeZoom({ title, favicon, bannerPos, todoPos, envAutopl
156
156
  }
157
157
  };
158
158
  window.__wgSetTypingBadge = (on, label) => {
159
- let badge = document.getElementById("wg-typing-badge");
159
+ let badge = __wgById("wg-typing-badge");
160
160
  if (!on) {
161
161
  if (badge) {
162
162
  badge.classList.remove("wg-in");
163
163
  setTimeout(() => {
164
- const b = document.getElementById("wg-typing-badge");
164
+ const b = __wgById("wg-typing-badge");
165
165
  if (b && !b.classList.contains("wg-in")) b.remove();
166
166
  }, 220);
167
167
  }
@@ -171,7 +171,7 @@ export function installSwipeZoom({ title, favicon, bannerPos, todoPos, envAutopl
171
171
  badge = document.createElement("div");
172
172
  badge.id = "wg-typing-badge";
173
173
  badge.setAttribute("data-wg-ui", "1");
174
- document.documentElement.appendChild(badge);
174
+ __wgAdd(badge);
175
175
  }
176
176
  const text = label ? String(label).slice(0, 40) : "typing";
177
177
  badge.innerHTML =
@@ -196,7 +196,7 @@ export function installSwipeZoom({ title, favicon, bannerPos, todoPos, envAutopl
196
196
  el.style.removeProperty("position");
197
197
  delete el.dataset.wgZoomed;
198
198
  });
199
- const shell = document.getElementById("wg-device-shell");
199
+ const shell = __wgById("wg-device-shell");
200
200
  if (shell && shell.dataset.wgCamBase != null) {
201
201
  shell.style.transform = shell.dataset.wgCamBase;
202
202
  shell.style.transformOrigin = shell.dataset.wgCamOrigin || "center center";
@@ -43,10 +43,10 @@ export function installTodos({ title, favicon, bannerPos, todoPos, envAutoplay,
43
43
  "_default";
44
44
 
45
45
  // Drop any in-panel leftover (old builds put #wg-todos in .wg-body).
46
- document.querySelectorAll("#wg-panel #wg-todos").forEach((el) => el.remove());
46
+ __wgQA("#wg-panel #wg-todos").forEach((el) => el.remove());
47
47
 
48
48
  const allDocks = () =>
49
- [...document.querySelectorAll(".wg-todo-dock, #wg-todo-dock")].filter(
49
+ [...__wgQA(".wg-todo-dock, #wg-todo-dock")].filter(
50
50
  (el, i, arr) => arr.indexOf(el) === i,
51
51
  );
52
52
  const relayoutTodoDocks = () => {
@@ -60,11 +60,11 @@ export function installTodos({ title, favicon, bannerPos, todoPos, envAutoplay,
60
60
  const findDock = (key) => {
61
61
  if (key === "_default") {
62
62
  return (
63
- document.querySelector('.wg-todo-dock[data-wg-todo-key="_default"]') ||
64
- document.getElementById("wg-todo-dock")
63
+ __wgQ('.wg-todo-dock[data-wg-todo-key="_default"]') ||
64
+ __wgById("wg-todo-dock")
65
65
  );
66
66
  }
67
- const all = document.querySelectorAll(".wg-todo-dock");
67
+ const all = __wgQA(".wg-todo-dock");
68
68
  for (let i = 0; i < all.length; i++) {
69
69
  if (all[i].getAttribute("data-wg-todo-key") === String(key)) return all[i];
70
70
  }
@@ -141,7 +141,7 @@ export function installTodos({ title, favicon, bannerPos, todoPos, envAutoplay,
141
141
  /* ignore */
142
142
  }
143
143
  });
144
- document.documentElement.appendChild(dock);
144
+ __wgAdd(dock);
145
145
  } else if (wantPos) {
146
146
  dock.dataset.pos = wantPos;
147
147
  try {
@@ -115,24 +115,6 @@ export const RING_CSS =
115
115
  ".wg-todo-dock .wg-todo-dock-title{font:700 11px/1.2 system-ui,sans-serif;color:#c9a6ff;" +
116
116
  "letter-spacing:.04em;text-transform:uppercase;margin:0 0 8px;}" +
117
117
  ".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;}" +
118
- // Video device stage: keep recordVideo size fixed; center a device-sized shell.
119
- "html.wg-video-device-stage{background:#0b1220 !important;}" +
120
- "html.wg-video-device-stage body{margin:0 !important;min-height:100vh !important;" +
121
- "display:flex !important;align-items:center !important;justify-content:center !important;" +
122
- "background:#0b1220 !important;overflow:hidden !important;}" +
123
- "#wg-device-shell{flex-shrink:0;overflow:auto;background:#fff;" +
124
- "border-radius:16px;box-shadow:0 24px 80px rgba(0,0,0,.55),0 0 0 1px rgba(255,255,255,.08);" +
125
- "transform-origin:center center;" +
126
- /* Force radius clip on all corners (Chrome + transform/scale). */
127
- "-webkit-mask-image:-webkit-radial-gradient(white,black);" +
128
- "isolation:isolate;" +
129
- "transition:border-radius .55s cubic-bezier(.22,1,.36,1),box-shadow .55s ease," +
130
- "transform .55s cubic-bezier(.22,1,.36,1),width .5s ease,height .5s ease,max-width .5s ease,max-height .5s ease;}" +
131
- "#wg-device-shell.wg-shell-enter{border-radius:0;box-shadow:none;}" +
132
- "#wg-device-shell.wg-shell-shutter-out{border-radius:0 !important;" +
133
- "box-shadow:none !important;outline:none !important;border:none !important;}" +
134
- "#wg-device-shell.wg-shell-desktop-flat{border-radius:0 !important;box-shadow:none !important;" +
135
- "outline:none !important;border:none !important;-webkit-mask-image:none;}" +
136
118
  // Always-on zoom HUD - bottom-right (top-left is crowded: todos + banners).
137
119
  "#wg-zoom-badge{position:fixed;z-index:2147483646;bottom:14px;right:14px;" +
138
120
  "display:flex;align-items:center;gap:7px;padding:6px 11px 6px 8px;" +
@@ -329,3 +311,24 @@ export const WAYGRAPH_FAVICON =
329
311
  "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>" +
330
312
  "<circle cx='16' cy='16' r='14' fill='#7C3AED'/></svg>",
331
313
  );
314
+
315
+ // Rules that style the HOST page (device stage) - these must stay in the light DOM, outside the overlay's shadow root.
316
+ export const HOST_CSS =
317
+ // Video device stage: keep recordVideo size fixed; center a device-sized shell.
318
+ "html.wg-video-device-stage{background:#0b1220 !important;}" +
319
+ "html.wg-video-device-stage body{margin:0 !important;min-height:100vh !important;" +
320
+ "display:flex !important;align-items:center !important;justify-content:center !important;" +
321
+ "background:#0b1220 !important;overflow:hidden !important;}" +
322
+ "#wg-device-shell{flex-shrink:0;overflow:auto;background:#fff;" +
323
+ "border-radius:16px;box-shadow:0 24px 80px rgba(0,0,0,.55),0 0 0 1px rgba(255,255,255,.08);" +
324
+ "transform-origin:center center;" +
325
+ /* Force radius clip on all corners (Chrome + transform/scale). */
326
+ "-webkit-mask-image:-webkit-radial-gradient(white,black);" +
327
+ "isolation:isolate;" +
328
+ "transition:border-radius .55s cubic-bezier(.22,1,.36,1),box-shadow .55s ease," +
329
+ "transform .55s cubic-bezier(.22,1,.36,1),width .5s ease,height .5s ease,max-width .5s ease,max-height .5s ease;}" +
330
+ "#wg-device-shell.wg-shell-enter{border-radius:0;box-shadow:none;}" +
331
+ "#wg-device-shell.wg-shell-shutter-out{border-radius:0 !important;" +
332
+ "box-shadow:none !important;outline:none !important;border:none !important;}" +
333
+ "#wg-device-shell.wg-shell-desktop-flat{border-radius:0 !important;box-shadow:none !important;" +
334
+ "outline:none !important;border:none !important;-webkit-mask-image:none;}";
@@ -1,6 +1,7 @@
1
1
  // Moved verbatim from the former CHAIN_RUNNER_SCRIPT template string in cli.ts (see src/ARCHITECTURE.md).
2
2
  // Runs inside the target project's own waygraph copy - keep it dependency-light and self-contained.
3
- import { RING_CSS, WAYGRAPH_FAVICON } from "./overlay-css.js";
3
+ import { HOST_CSS, RING_CSS, WAYGRAPH_FAVICON } from "./overlay-css.js";
4
+ import { ensureShadowRoot } from "../ui/shadow.js";
4
5
  import { resolveTodoDockUi } from "../highlights.js";
5
6
  import { installCore } from "./inpage/core.js";
6
7
  import { installSwipeZoom } from "./inpage/swipe-zoom.js";
@@ -12,7 +13,9 @@ import { installDevice } from "./inpage/device.js";
12
13
  import { applyVideoDeviceStage } from "./device-stage.js";
13
14
 
14
15
  export async function installOverlay(page, title) {
15
- await page.addStyleTag({ content: RING_CSS }).catch(() => {});
16
+ await ensureShadowRoot(page);
17
+ await page.evaluate(([css, key]) => __wgCss(css, key), [RING_CSS, "wg-overlay-css"]).catch(() => {});
18
+ await page.addStyleTag({ content: HOST_CSS }).catch(() => {});
16
19
  // Default top-left; override with WAYGRAPH_TITLE_POS=left|center|right.
17
20
  // Click cycles left -> center -> right (persisted in localStorage so a
18
21
  // navigation that rebuilds the banner keeps the human's last pick).
@@ -247,7 +247,7 @@ export async function cycleHighlightRings(page, highlights, gatesFast, opts) {
247
247
  return;
248
248
  }
249
249
  // Fallback for pages that never got installOverlay sync helper.
250
- const ul = document.getElementById("wg-todos");
250
+ const ul = __wgById("wg-todos");
251
251
  if (!ul) return;
252
252
  ul.innerHTML = todos
253
253
  .map((t) => {
@@ -66,7 +66,7 @@ export async function presentSlides(page, slides, _gate, opts) {
66
66
  }
67
67
  await page.evaluate(
68
68
  (info) => {
69
- let panel = document.getElementById("wg-panel");
69
+ let panel = __wgById("wg-panel");
70
70
  const isNew = !panel;
71
71
  if (!panel) {
72
72
  panel = document.createElement("div");
@@ -139,7 +139,7 @@ export async function presentSlides(page, slides, _gate, opts) {
139
139
  ">Auto-advancing...</div>" +
140
140
  "</div>";
141
141
  if (isNew) {
142
- document.documentElement.appendChild(panel);
142
+ __wgAdd(panel);
143
143
  requestAnimationFrame(() => panel.classList.add("wg-in"));
144
144
  }
145
145
  if (window.__wgWirePanelChrome) {
@@ -172,7 +172,7 @@ export async function presentSlides(page, slides, _gate, opts) {
172
172
  } catch {
173
173
  /* ignore */
174
174
  }
175
- const cb = document.getElementById("wg-autoplay-cb");
175
+ const cb = __wgById("wg-autoplay-cb");
176
176
  if (cb) {
177
177
  cb.checked = autoNow;
178
178
  cb.addEventListener("change", () => {
@@ -181,17 +181,17 @@ export async function presentSlides(page, slides, _gate, opts) {
181
181
  } catch {
182
182
  /* ignore */
183
183
  }
184
- const manual = document.getElementById("wg-gate-manual");
185
- const auto = document.getElementById("wg-gate-auto");
184
+ const manual = __wgById("wg-gate-manual");
185
+ const auto = __wgById("wg-gate-auto");
186
186
  if (manual) manual.style.display = cb.checked ? "none" : "";
187
187
  if (auto) auto.style.display = cb.checked ? "" : "none";
188
188
  });
189
189
  }
190
- const manual = document.getElementById("wg-gate-manual");
191
- const auto = document.getElementById("wg-gate-auto");
190
+ const manual = __wgById("wg-gate-manual");
191
+ const auto = __wgById("wg-gate-auto");
192
192
  if (manual) manual.style.display = autoNow ? "none" : "";
193
193
  if (auto) auto.style.display = autoNow ? "" : "none";
194
- const runBtn = document.getElementById("wg-run");
194
+ const runBtn = __wgById("wg-run");
195
195
  if (runBtn) {
196
196
  runBtn.removeAttribute("data-wg-acked");
197
197
  runBtn.addEventListener("click", (e) => {
@@ -205,8 +205,8 @@ export async function presentSlides(page, slides, _gate, opts) {
205
205
  const unlockAt = Date.now() + info.dwellMs;
206
206
  const tick = () => {
207
207
  const left = Math.max(0, unlockAt - Date.now());
208
- const hint = document.getElementById("wg-dwell-hint");
209
- const btn = document.getElementById("wg-run");
208
+ const hint = __wgById("wg-dwell-hint");
209
+ const btn = __wgById("wg-run");
210
210
  if (left <= 0) {
211
211
  if (hint) hint.remove();
212
212
  if (btn) btn.disabled = false;
@@ -248,7 +248,7 @@ export async function presentSlides(page, slides, _gate, opts) {
248
248
  for (;;) {
249
249
  const acked = await page
250
250
  .evaluate(() => {
251
- const btn = document.getElementById("wg-run");
251
+ const btn = __wgById("wg-run");
252
252
  return !!(btn && btn.getAttribute("data-wg-acked") === "1");
253
253
  })
254
254
  .catch(() => false);
@@ -33,7 +33,7 @@ export async function renderBeforeStep(page, info) {
33
33
  // flash - it only ever fades in ONCE, the first time this page
34
34
  // genuinely has no panel yet (a real navigation wiped the whole
35
35
  // document, or this is the very first step).
36
- let panel = document.getElementById("wg-panel");
36
+ let panel = __wgById("wg-panel");
37
37
  const isNewPanel = !panel;
38
38
  if (!panel) {
39
39
  panel = document.createElement("div");
@@ -126,7 +126,7 @@ export async function renderBeforeStep(page, info) {
126
126
  ">Auto-advancing...</div>";
127
127
  panel.innerHTML = html;
128
128
  if (isNewPanel) {
129
- document.documentElement.appendChild(panel);
129
+ __wgAdd(panel);
130
130
  requestAnimationFrame(() => panel.classList.add("wg-in"));
131
131
  } else {
132
132
  panel.classList.add("wg-in");
@@ -204,7 +204,7 @@ export async function renderBeforeStep(page, info) {
204
204
  panel.querySelectorAll("textarea[data-key]").forEach((ta) => {
205
205
  ta.addEventListener("input", applyKeyPretty);
206
206
  });
207
- const runBtn = document.getElementById("wg-run");
207
+ const runBtn = __wgById("wg-run");
208
208
  if (runBtn) {
209
209
  runBtn.addEventListener("click", () => {
210
210
  const edits = {};
@@ -220,7 +220,7 @@ export async function renderBeforeStep(page, info) {
220
220
  // still always wins over an active autoplay wait, whichever the
221
221
  // checkbox says - this is the "hybrid" control: autoplay is a
222
222
  // default, not a lockout.
223
- const cb = document.getElementById("wg-autoplay-cb");
223
+ const cb = __wgById("wg-autoplay-cb");
224
224
  if (cb) {
225
225
  cb.addEventListener("change", () => {
226
226
  try {
@@ -228,8 +228,8 @@ export async function renderBeforeStep(page, info) {
228
228
  } catch {
229
229
  /* private mode / blocked storage - toggle still works this page */
230
230
  }
231
- const manual = document.getElementById("wg-gate-manual");
232
- const auto = document.getElementById("wg-gate-auto");
231
+ const manual = __wgById("wg-gate-manual");
232
+ const auto = __wgById("wg-gate-auto");
233
233
  if (manual) manual.style.display = cb.checked ? "none" : "";
234
234
  if (auto) auto.style.display = cb.checked ? "" : "none";
235
235
  });
@@ -256,8 +256,8 @@ export async function renderBeforeStep(page, info) {
256
256
  return {
257
257
  ok: !!(panel && textOk && opacityOk),
258
258
  beacons,
259
- hasRun: !!document.getElementById("wg-run"),
260
- readyAttr: document.getElementById("wg-panel")?.getAttribute("data-wg-ready") || null,
259
+ hasRun: !!__wgById("wg-run"),
260
+ readyAttr: __wgById("wg-panel")?.getAttribute("data-wg-ready") || null,
261
261
  };
262
262
  })
263
263
  .catch((e) => ({ ok: false, reason: String(e && e.message ? e.message : e) }));
@@ -412,7 +412,7 @@ export async function renderAfterStep(page, info) {
412
412
  await page
413
413
  .evaluate((info) => {
414
414
  // Reused in place - see renderBeforeStep's own comment on this.
415
- let panel = document.getElementById("wg-panel");
415
+ let panel = __wgById("wg-panel");
416
416
  const isNewPanel = !panel;
417
417
  if (!panel) {
418
418
  panel = document.createElement("div");
@@ -497,7 +497,7 @@ export async function renderAfterStep(page, info) {
497
497
  resultHtml +
498
498
  gateHtml;
499
499
  if (isNewPanel) {
500
- document.documentElement.appendChild(panel);
500
+ __wgAdd(panel);
501
501
  requestAnimationFrame(() => panel.classList.add("wg-in"));
502
502
  } else {
503
503
  panel.classList.add("wg-in");
@@ -546,9 +546,9 @@ export async function renderAfterStep(page, info) {
546
546
  panel.querySelector(".wg-result-json").style.display = wantPretty ? "none" : "block";
547
547
  });
548
548
  });
549
- const runBtn = document.getElementById("wg-run");
549
+ const runBtn = __wgById("wg-run");
550
550
  if (runBtn) runBtn.addEventListener("click", () => window.__wgNext({}));
551
- const cb = document.getElementById("wg-autoplay-cb");
551
+ const cb = __wgById("wg-autoplay-cb");
552
552
  if (cb) {
553
553
  cb.addEventListener("change", () => {
554
554
  try {
@@ -556,8 +556,8 @@ export async function renderAfterStep(page, info) {
556
556
  } catch {
557
557
  /* private mode / blocked storage - toggle still works this page */
558
558
  }
559
- const manual = document.getElementById("wg-gate-manual");
560
- const auto = document.getElementById("wg-gate-auto");
559
+ const manual = __wgById("wg-gate-manual");
560
+ const auto = __wgById("wg-gate-auto");
561
561
  if (manual) manual.style.display = cb.checked ? "none" : "";
562
562
  if (auto) auto.style.display = cb.checked ? "" : "none";
563
563
  });
@@ -579,7 +579,7 @@ async function renderStepError(page, info) {
579
579
  await page
580
580
  .evaluate((info) => {
581
581
  // Reused in place - see renderBeforeStep's own comment on this.
582
- let panel = document.getElementById("wg-panel");
582
+ let panel = __wgById("wg-panel");
583
583
  const isNewPanel = !panel;
584
584
  if (!panel) {
585
585
  panel = document.createElement("div");
@@ -631,7 +631,7 @@ async function renderStepError(page, info) {
631
631
  "<button id=\"wg-run\" class=\"wg-error-stop\">Stop</button>" +
632
632
  "</div>";
633
633
  if (isNewPanel) {
634
- document.documentElement.appendChild(panel);
634
+ __wgAdd(panel);
635
635
  requestAnimationFrame(() => panel.classList.add("wg-in"));
636
636
  } else {
637
637
  panel.classList.add("wg-in");
@@ -651,9 +651,9 @@ async function renderStepError(page, info) {
651
651
  ready: true,
652
652
  });
653
653
  }
654
- const runBtn = document.getElementById("wg-run");
654
+ const runBtn = __wgById("wg-run");
655
655
  if (runBtn) runBtn.addEventListener("click", () => window.__wgNext({}));
656
- const retryBtn = document.getElementById("wg-error-retry");
656
+ const retryBtn = __wgById("wg-error-retry");
657
657
  if (retryBtn) retryBtn.addEventListener("click", () => window.__wgNext({ __wgRetry: "1" }));
658
658
  }, errorPayload)
659
659
  .catch(() => {});
@@ -38,17 +38,17 @@ import { formatHighlightCaption } from "../highlights.js";
38
38
  export async function markStepRunning(page, _opts) {
39
39
  await page
40
40
  .evaluate(() => {
41
- const runBtn = document.getElementById("wg-run");
41
+ const runBtn = __wgById("wg-run");
42
42
  if (runBtn) {
43
43
  runBtn.disabled = true;
44
44
  runBtn.textContent = "Running \u25B6";
45
45
  }
46
- const autoEl = document.getElementById("wg-gate-auto");
46
+ const autoEl = __wgById("wg-gate-auto");
47
47
  if (autoEl) autoEl.textContent = "Running...";
48
- document.querySelectorAll("#wg-panel textarea[data-key]").forEach((ta) => {
48
+ __wgQA("#wg-panel textarea[data-key]").forEach((ta) => {
49
49
  ta.disabled = true;
50
50
  });
51
- const panel = document.getElementById("wg-panel");
51
+ const panel = __wgById("wg-panel");
52
52
  if (panel) {
53
53
  panel.classList.add("wg-collapsed");
54
54
  panel.setAttribute("data-wg-collapsed", "1");
@@ -83,13 +83,13 @@ export async function markStepRunning(page, _opts) {
83
83
  export async function restoreTheaterAfterNavigation(page, todoDockRef, deviceRef, stubBeforeRef) {
84
84
  const needs =
85
85
  (await page
86
- .evaluate(() => !window.__wgSyncTodos || !document.getElementById("wg-ring"))
86
+ .evaluate(() => !window.__wgSyncTodos || !__wgById("wg-ring"))
87
87
  .catch(() => true)) || false;
88
88
  if (!needs) {
89
89
  // Overlay survived (SPA / no full document wipe) - still refresh dock
90
90
  // if Node has a carry and the DOM lost .wg-todo-dock.
91
91
  const hasDock = await page
92
- .evaluate(() => !!document.querySelector(".wg-todo-dock, #wg-todo-dock"))
92
+ .evaluate(() => !!__wgQ(".wg-todo-dock, #wg-todo-dock"))
93
93
  .catch(() => false);
94
94
  if (hasDock || !todoDockRef || !todoDockRef.current) return;
95
95
  }
@@ -162,10 +162,10 @@ export async function teardownOverlay(page) {
162
162
  "wg-device-toast",
163
163
  "wg-device-badge",
164
164
  ]) {
165
- const el = document.getElementById(id);
165
+ const el = __wgById(id);
166
166
  if (el) el.remove();
167
167
  }
168
- document.querySelectorAll(".wg-todo-dock").forEach((el) => el.remove());
168
+ __wgQA(".wg-todo-dock").forEach((el) => el.remove());
169
169
  document.documentElement.classList.remove("wg-video-device-stage");
170
170
  })
171
171
  .catch(() => {});
@@ -115,7 +115,7 @@ export async function syncTodoDockAdvance(page, todoDockRef, stubIndex, stub) {
115
115
  export async function probeTodoDocksOnPage(page) {
116
116
  return page
117
117
  .evaluate(() => {
118
- const docks = [...document.querySelectorAll(".wg-todo-dock, #wg-todo-dock")];
118
+ const docks = [...__wgQA(".wg-todo-dock, #wg-todo-dock")];
119
119
  return docks.map((el) => {
120
120
  const r = el.getBoundingClientRect();
121
121
  const items = el.querySelectorAll("li");
@@ -1,38 +1,40 @@
1
+ import { ensureShadowRoot } from "./ui/shadow.js";
1
2
  import { composeStepOverlay } from "./ui/compose.js";
2
3
  const RING_CSS = composeStepOverlay();
3
4
  const FAVICON = "data:image/svg+xml," +
4
5
  encodeURIComponent("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><circle cx='16' cy='16' r='14' fill='#7C3AED'/></svg>");
5
6
  export async function installDemoChrome(page, title = "waygraph auto", options = {}) {
6
7
  const showBanner = options.banner !== false;
7
- await page.addStyleTag({ content: RING_CSS }).catch(() => { });
8
+ await ensureShadowRoot(page);
9
+ await page.evaluate(([css, key]) => __wgCss(css, key), [RING_CSS, "wg-overlay-css"]).catch(() => { });
8
10
  await page
9
11
  .evaluate(({ title, favicon, showBanner }) => {
10
- if (!document.getElementById("wg-ring")) {
12
+ if (!__wgById("wg-ring")) {
11
13
  const ring = document.createElement("div");
12
14
  ring.id = "wg-ring";
13
- document.documentElement.appendChild(ring);
15
+ __wgAdd(ring);
14
16
  }
15
- if (!document.getElementById("wg-ring-label")) {
17
+ if (!__wgById("wg-ring-label")) {
16
18
  const ringLabel = document.createElement("div");
17
19
  ringLabel.id = "wg-ring-label";
18
- document.documentElement.appendChild(ringLabel);
20
+ __wgAdd(ringLabel);
19
21
  }
20
- if (!document.getElementById("wg-cursor")) {
22
+ if (!__wgById("wg-cursor")) {
21
23
  const cursor = document.createElement("div");
22
24
  cursor.id = "wg-cursor";
23
25
  cursor.innerHTML =
24
26
  "<svg viewBox='0 0 32 32' width='24' height='24'>" +
25
27
  "<path fill='#0C0C1A' stroke='#fff' stroke-width='1.4' stroke-linejoin='round' " +
26
28
  "d='M6 3.5l1.4 22.5 5.8-5.4 4.2 9.4 3.6-1.6-4.2-9.2H26z'/></svg>";
27
- document.documentElement.appendChild(cursor);
29
+ __wgAdd(cursor);
28
30
  }
29
- if (!document.getElementById("wg-click-pulse")) {
31
+ if (!__wgById("wg-click-pulse")) {
30
32
  const pulse = document.createElement("div");
31
33
  pulse.id = "wg-click-pulse";
32
- document.documentElement.appendChild(pulse);
34
+ __wgAdd(pulse);
33
35
  }
34
36
  window.__wgMoveCursorTo = (x, y, ms) => {
35
- const cursor = document.getElementById("wg-cursor");
37
+ const cursor = __wgById("wg-cursor");
36
38
  if (!cursor)
37
39
  return;
38
40
  cursor.style.setProperty("--wg-cursor-ms", (ms || 600) + "ms");
@@ -40,7 +42,7 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
40
42
  cursor.style.opacity = "1";
41
43
  };
42
44
  window.__wgClickPulse = (x, y, tone) => {
43
- const pulse = document.getElementById("wg-click-pulse");
45
+ const pulse = __wgById("wg-click-pulse");
44
46
  if (!pulse)
45
47
  return;
46
48
  const raw = (tone || "planned") + "";
@@ -59,8 +61,8 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
59
61
  pulse.classList.add("wg-pulse");
60
62
  };
61
63
  window.__wgPositionRing = (box, label, tone, style) => {
62
- const ring = document.getElementById("wg-ring");
63
- const ringLabel = document.getElementById("wg-ring-label");
64
+ const ring = __wgById("wg-ring");
65
+ const ringLabel = __wgById("wg-ring-label");
64
66
  if (!ring || !ringLabel)
65
67
  return;
66
68
  const raw = (tone || "planned") + "";
@@ -92,25 +94,25 @@ export async function installDemoChrome(page, title = "waygraph auto", options =
92
94
  ringLabel.style.opacity = "1";
93
95
  };
94
96
  window.__wgHideRing = () => {
95
- const ring = document.getElementById("wg-ring");
96
- const ringLabel = document.getElementById("wg-ring-label");
97
+ const ring = __wgById("wg-ring");
98
+ const ringLabel = __wgById("wg-ring-label");
97
99
  if (ring)
98
100
  ring.style.opacity = "0";
99
101
  if (ringLabel)
100
102
  ringLabel.style.opacity = "0";
101
103
  };
102
- if (showBanner && title && !document.getElementById("wg-banner")) {
104
+ if (showBanner && title && !__wgById("wg-banner")) {
103
105
  const banner = document.createElement("div");
104
106
  banner.id = "wg-banner";
105
107
  banner.innerHTML =
106
108
  "<span class='wg-banner-tag'>waygraph auto</span><span>" + title + "</span>";
107
- document.documentElement.appendChild(banner);
109
+ __wgAdd(banner);
108
110
  banner.setAttribute("data-wg-ui", "1");
109
111
  banner.setAttribute("data-wg-modal", "banner");
110
112
  banner.setAttribute("data-wg-ready", "1");
111
113
  }
112
114
  else {
113
- const existing = document.getElementById("wg-banner");
115
+ const existing = __wgById("wg-banner");
114
116
  if (existing) {
115
117
  existing.setAttribute("data-wg-ui", "1");
116
118
  existing.setAttribute("data-wg-modal", "banner");
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Shadow-DOM mount for every waygraph overlay element. Overlay UI lives in one open shadow root under
3
+ * `<div id="wg-root">`, so the host page's CSS can't restyle it and ours can't leak out.
4
+ *
5
+ * `installShadowRoot` runs INSIDE the page (page.evaluate / addInitScript), so it must stay
6
+ * self-contained. It defines the `__wg*` helpers the overlay code uses in place of `document.*`:
7
+ * lookups check the shadow root first and fall back to the light DOM (the device stage shell and
8
+ * host-page markers such as `[data-wg-zoomed]` intentionally stay in the light DOM).
9
+ */
10
+ import type { BrowserContext, Page } from "@playwright/test";
11
+ export declare function installShadowRoot(): void;
12
+ /** Idempotent; call before any overlay install on a page. */
13
+ export declare function ensureShadowRoot(page: Page): Promise<void>;
14
+ /** Also define the helpers on every future document (pilot overlay persists across navigations). */
15
+ export declare function installShadowRootEverywhere(context: BrowserContext): Promise<void>;
@@ -0,0 +1,42 @@
1
+ export function installShadowRoot() {
2
+ const w = window;
3
+ if (w.__wgById)
4
+ return;
5
+ const root = () => {
6
+ let host = document.getElementById("wg-root");
7
+ if (!host || !host.shadowRoot) {
8
+ host?.remove();
9
+ host = document.createElement("div");
10
+ host.id = "wg-root";
11
+ // No box of its own: children are position:fixed, so the host must never affect page layout.
12
+ host.style.cssText = "display:contents";
13
+ host.attachShadow({ mode: "open" });
14
+ document.documentElement.appendChild(host);
15
+ }
16
+ return host.shadowRoot;
17
+ };
18
+ w.__wgRoot = root;
19
+ w.__wgById = (id) => root().getElementById(id) || document.getElementById(id);
20
+ w.__wgQ = (sel) => root().querySelector(sel) || document.querySelector(sel);
21
+ w.__wgQA = (sel) => Array.from(root().querySelectorAll(sel)).concat(Array.from(document.querySelectorAll(sel)));
22
+ w.__wgAdd = (node) => root().appendChild(node);
23
+ w.__wgCss = (css, key) => {
24
+ const r = root();
25
+ let style = Array.from(r.children).find((c) => c.tagName === "STYLE" && c.getAttribute("data-key") === key);
26
+ if (!style) {
27
+ style = document.createElement("style");
28
+ style.setAttribute("data-key", key);
29
+ r.insertBefore(style, r.firstChild);
30
+ }
31
+ if (style.textContent !== css)
32
+ style.textContent = css;
33
+ };
34
+ }
35
+ /** Idempotent; call before any overlay install on a page. */
36
+ export async function ensureShadowRoot(page) {
37
+ await page.evaluate(installShadowRoot).catch(() => { });
38
+ }
39
+ /** Also define the helpers on every future document (pilot overlay persists across navigations). */
40
+ export async function installShadowRootEverywhere(context) {
41
+ await context.addInitScript(installShadowRoot);
42
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.30",
3
+ "version": "0.15.31",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",