waygraph 0.15.27 → 0.15.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/highlights/css.js +2 -44
  2. package/dist/pilot-overlay.d.ts +0 -18
  3. package/dist/pilot-overlay.js +27 -32
  4. package/dist/runner/inpage/banner.js +68 -0
  5. package/dist/runner/inpage/core.js +267 -0
  6. package/dist/runner/inpage/device.js +211 -0
  7. package/dist/runner/inpage/panel-chrome.js +152 -0
  8. package/dist/runner/inpage/stamp.js +46 -0
  9. package/dist/runner/inpage/swipe-zoom.js +220 -0
  10. package/dist/runner/inpage/todos.js +332 -0
  11. package/dist/runner/overlay-css.js +7 -18
  12. package/dist/runner/overlay-install.js +15 -1269
  13. package/dist/step-overlay.js +2 -46
  14. package/dist/ui/components/banner.d.ts +2 -0
  15. package/dist/ui/components/banner.js +10 -0
  16. package/dist/ui/components/cursor.d.ts +2 -0
  17. package/dist/ui/components/cursor.js +19 -0
  18. package/dist/ui/components/ring.d.ts +4 -0
  19. package/dist/ui/components/ring.js +44 -0
  20. package/dist/ui/compose.d.ts +4 -0
  21. package/dist/ui/compose.js +7 -0
  22. package/dist/ui/tokens.d.ts +31 -0
  23. package/dist/ui/tokens.js +21 -0
  24. package/examples/saucedemo/.waygraph-auto/2324269a.log +0 -0
  25. package/examples/saucedemo/.waygraph-auto/3f088138.log +0 -0
  26. package/examples/saucedemo/.waygraph-auto/42e014d1.log +0 -0
  27. package/examples/saucedemo/.waygraph-auto/6706ead4.log +0 -0
  28. package/examples/saucedemo/.waygraph-auto/7773a809.log +0 -0
  29. package/examples/saucedemo/.waygraph-auto/94221b2b.log +0 -0
  30. package/examples/saucedemo/.waygraph-auto/c60cc55e.log +0 -0
  31. package/examples/saucedemo/.waygraph-auto/da6ebb31.log +0 -0
  32. package/examples/saucedemo/.waygraph-auto/e03cfd8e.log +0 -0
  33. package/examples/saucedemo/.waygraph-auto/f2943898.log +0 -0
  34. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { ringCss } from "../ui/components/ring.js";
1
2
  // Split out of the former 2,100-line highlights.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
3
  /**
3
4
  * The `#wg-ring` / `#wg-ring-label` CSS (tone/size/weight variants) - the
@@ -12,47 +13,4 @@
12
13
  * renders them belongs alongside it for the same reason. `cli.ts` imports
13
14
  * this constant instead of keeping its own duplicate copy.
14
15
  */
15
- export const WAYGRAPH_RING_CSS = "#wg-ring{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
16
- "border:2.5px solid #7C3AED;border-radius:10px;" +
17
- // Opacity fade only - NEVER transition border-color/box-shadow. Tone swaps
18
- // (auto gray -> planned purple -> warning yellow) must snap instantly;
19
- // color transitions read as a muddy gray/purple/yellow morph on ring 2+.
20
- "box-shadow:0 0 0 4px rgba(124,58,237,.16);transition:opacity .3s ease;}" +
21
- // Planned (stubs / fixtures / YAP / instruction.highlights) = purple.
22
- // Automation (verify fallback, unmatched fill/click) = gray - operators
23
- // can ignore engine checks and watch purple + semantic tones.
24
- "#wg-ring[data-tone=planned]{border-color:#7C3AED;box-shadow:0 0 0 4px rgba(124,58,237,.16);}" +
25
- "#wg-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}" +
26
- "#wg-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}" +
27
- "#wg-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}" +
28
- "#wg-ring[data-tone=danger]{border-color:#EF4444;box-shadow:0 0 0 4px rgba(239,68,68,.22);}" +
29
- "#wg-ring[data-tone=success]{border-color:#22C55E;box-shadow:0 0 0 4px rgba(34,197,94,.22);}" +
30
- // Real, direct user request: a visible color distinction between a real
31
- // Playwright-driven action (auto gray, unchanged) and a pure DOM
32
- // inspection (this - orange).
33
- "#wg-ring[data-tone=orange]{border-color:#F97316;box-shadow:0 0 0 4px rgba(249,115,22,.22);}" +
34
- // A real element, not a ::after pseudo-element - a pseudo-element's
35
- // position is CSS-relative to the ring's own box (left:0 always meant
36
- // "the ring's own left edge"), so it had no way to be clamped back onto
37
- // screen when that box sat near a viewport edge - the label's text just
38
- // ran off, invisibly, with no overflow guard at all. A real sibling can
39
- // be measured (its actual rendered width) and repositioned in JS -
40
- // pushed back onto screen, same width, never shrunk. See
41
- // window.__wgPositionRing in cli.ts's own installOverlay.
42
- "#wg-ring-label{position:fixed;z-index:2147483646;pointer-events:none;opacity:0;" +
43
- "max-width:min(360px,70vw);white-space:normal;padding:6px 10px;border-radius:7px;background:#7C3AED;color:#fff;" +
44
- "font:600 12px/1.35 system-ui,sans-serif;transition:opacity .3s ease;}" +
45
- "#wg-ring-label[data-tone=planned]{background:#7C3AED;color:#fff;}" +
46
- "#wg-ring-label[data-tone=auto]{background:#6B7280;color:#fff;}" +
47
- "#wg-ring-label[data-tone=info]{background:#2563EB;color:#fff;}" +
48
- "#wg-ring-label[data-tone=warning]{background:#EAB308;color:#1c1917;}" +
49
- "#wg-ring-label[data-tone=danger]{background:#DC2626;color:#fff;}" +
50
- "#wg-ring-label[data-tone=success]{background:#16A34A;color:#fff;}" +
51
- "#wg-ring-label[data-tone=orange]{background:#EA580C;color:#fff;}" +
52
- // size: ring pad + label font; weight: label boldness
53
- "#wg-ring[data-size=sm]{border-width:1.5px;border-radius:8px;}" +
54
- "#wg-ring[data-size=lg]{border-width:4px;border-radius:12px;}" +
55
- "#wg-ring-label[data-size=sm]{font-size:10px;line-height:1.25;padding:4px 7px;border-radius:5px;}" +
56
- "#wg-ring-label[data-size=lg]{font-size:16px;line-height:1.35;padding:8px 14px;border-radius:9px;max-width:min(480px,85vw);}" +
57
- "#wg-ring-label[data-weight=bold]{font-weight:800;}" +
58
- "#wg-ring-label[data-weight=normal]{font-weight:600;}";
16
+ export const WAYGRAPH_RING_CSS = ringCss();
@@ -1,21 +1,3 @@
1
- /**
2
- * A small, self-contained on-page indicator for a live AutoSession -
3
- * "this browser is being driven by Waygraph," plus an expandable panel
4
- * showing the session's own current live menu. Real, direct user request:
5
- * a headful Pilot session looks like an ordinary browser tab with no
6
- * indication anything is driving it, which matters once a human might be
7
- * co-driving the same visible session (see `AutoSession.resync()`'s own
8
- * doc comment for the related state-desync problem that scenario causes).
9
- *
10
- * Deliberately NOT built on `src/cli.ts`'s own demo-panel/banner overlay
11
- * system, for the same reason `pilot-v1`'s narrate-mode ring renderer
12
- * wasn't either (see the git history at tag
13
- * `waygraph-pilot-v1-logs-prettified`): `cli.ts` runs `main().catch(...)`
14
- * unconditionally at module load with no `import.meta.url` guard, so
15
- * importing anything from it here would trigger the whole CLI's argument
16
- * dispatch as a side effect of loading a library module - unsafe. This
17
- * ships its own small, purpose-built badge + panel instead.
18
- */
19
1
  import type { BrowserContext, Page } from "@playwright/test";
20
2
  import type { SessionSnapshot } from "./auto-session.js";
21
3
  import { type HighlightTone, type DevicePreset, type DeviceState, type TodoDockUiOpts } from "./highlights.js";
@@ -1,3 +1,23 @@
1
+ /**
2
+ * A small, self-contained on-page indicator for a live AutoSession -
3
+ * "this browser is being driven by Waygraph," plus an expandable panel
4
+ * showing the session's own current live menu. Real, direct user request:
5
+ * a headful Pilot session looks like an ordinary browser tab with no
6
+ * indication anything is driving it, which matters once a human might be
7
+ * co-driving the same visible session (see `AutoSession.resync()`'s own
8
+ * doc comment for the related state-desync problem that scenario causes).
9
+ *
10
+ * Deliberately NOT built on `src/cli.ts`'s own demo-panel/banner overlay
11
+ * system, for the same reason `pilot-v1`'s narrate-mode ring renderer
12
+ * wasn't either (see the git history at tag
13
+ * `waygraph-pilot-v1-logs-prettified`): `cli.ts` runs `main().catch(...)`
14
+ * unconditionally at module load with no `import.meta.url` guard, so
15
+ * importing anything from it here would trigger the whole CLI's argument
16
+ * dispatch as a side effect of loading a library module - unsafe. This
17
+ * ships its own small, purpose-built badge + panel instead.
18
+ */
19
+ import { pilotFxRingCss } from "./ui/components/ring.js";
20
+ import { SURFACE } from "./ui/tokens.js";
1
21
  import { WAYGRAPH_RING_CSS, normalizeHighlightTone, normalizeHighlightSize, normalizeHighlightWeight, formatHighlightCaption, resolveDeviceState, resolveTodoDockUi, } from "./highlights.js";
2
22
  // Real, direct user request: this overlay's own colors were an improvised
3
23
  // purple/white scheme (#a78bfa / #1a1033) that didn't match anything else -
@@ -22,7 +42,7 @@ const OVERLAY_CSS = `
22
42
  }
23
43
  #wg-pilot-panel {
24
44
  display: none; margin-top: 6px; max-width: 420px; max-height: 320px;
25
- overflow-y: auto; background: rgba(20,10,40,.94); color: #fff; border-radius: 8px;
45
+ overflow-y: auto; background: ${SURFACE}; color: #fff; border-radius: 8px;
26
46
  box-shadow: 0 2px 8px rgba(0,0,0,0.25); padding: 8px 10px;
27
47
  }
28
48
  /* The graph tab renders a real Mermaid flowchart, which needs more room than
@@ -55,42 +75,17 @@ const OVERLAY_CSS = `
55
75
  #wg-pilot-activity {
56
76
  position: fixed; z-index: 2147483000; bottom: 12px; left: 12px;
57
77
  font: 12px/1.4 ui-monospace, "SF Mono", Consolas, monospace;
58
- background: rgba(20,10,40,.94); color: #fff; padding: 6px 10px; border-radius: 8px;
78
+ background: ${SURFACE}; color: #fff; padding: 6px 10px; border-radius: 8px;
59
79
  box-shadow: 0 2px 8px rgba(0,0,0,0.25); white-space: nowrap; pointer-events: none;
60
80
  opacity: 0; transform: translateY(4px); transition: opacity 0.15s, transform 0.15s;
61
81
  }
62
82
  #wg-pilot-activity.wg-pilot-toast-show { opacity: 1; transform: translateY(0); }
63
- /* Agent-sent fixture rings share tone/size CSS with #wg-ring (demo language). */
64
- .wg-pilot-fx-ring{position:fixed;z-index:2147483645;pointer-events:none;opacity:0;
65
- border:3px solid #7C3AED;border-radius:10px;box-sizing:border-box;
66
- transition:opacity .12s ease,left .12s,top .12s,width .12s,height .12s;}
67
- .wg-pilot-fx-ring[data-tone=planned]{border-color:#7C3AED;box-shadow:0 0 0 4px rgba(124,58,237,.16);}
68
- .wg-pilot-fx-ring[data-tone=auto]{border-color:#9CA3AF;box-shadow:0 0 0 4px rgba(156,163,175,.28);}
69
- .wg-pilot-fx-ring[data-tone=info]{border-color:#3B82F6;box-shadow:0 0 0 4px rgba(59,130,246,.22);}
70
- .wg-pilot-fx-ring[data-tone=warning]{border-color:#EAB308;box-shadow:0 0 0 4px rgba(234,179,8,.22);}
71
- .wg-pilot-fx-ring[data-tone=danger]{border-color:#EF4444;box-shadow:0 0 0 4px rgba(239,68,68,.22);}
72
- .wg-pilot-fx-ring[data-tone=success]{border-color:#22C55E;box-shadow:0 0 0 4px rgba(34,197,94,.22);}
73
- .wg-pilot-fx-ring[data-tone=orange]{border-color:#F97316;box-shadow:0 0 0 4px rgba(249,115,22,.22);}
74
- .wg-pilot-fx-label{position:fixed;z-index:2147483645;pointer-events:none;opacity:0;
75
- font:600 12px/1.3 ui-sans-serif,system-ui,sans-serif;padding:5px 9px;border-radius:6px;
76
- max-width:min(360px,80vw);box-shadow:0 2px 8px rgba(0,0,0,.25);white-space:nowrap;
77
- overflow:hidden;text-overflow:ellipsis;transition:opacity .12s ease;}
78
- .wg-pilot-fx-label[data-tone=planned]{background:#7C3AED;color:#fff;}
79
- .wg-pilot-fx-label[data-tone=auto]{background:#6B7280;color:#fff;}
80
- .wg-pilot-fx-label[data-tone=info]{background:#2563EB;color:#fff;}
81
- .wg-pilot-fx-label[data-tone=warning]{background:#EAB308;color:#1c1917;}
82
- .wg-pilot-fx-label[data-tone=danger]{background:#DC2626;color:#fff;}
83
- .wg-pilot-fx-label[data-tone=success]{background:#16A34A;color:#fff;}
84
- .wg-pilot-fx-label[data-tone=orange]{background:#EA580C;color:#fff;}
85
- .wg-pilot-fx-ring[data-size=sm]{border-width:1.5px;border-radius:8px;}
86
- .wg-pilot-fx-ring[data-size=lg]{border-width:4px;border-radius:12px;}
87
- .wg-pilot-fx-label[data-size=sm]{font-size:10px;padding:4px 7px;}
88
- .wg-pilot-fx-label[data-size=lg]{font-size:16px;padding:8px 14px;}
89
- .wg-pilot-fx-label[data-weight=bold]{font-weight:800;}
83
+ /* Agent-sent fixture rings: generated from ui/tokens (tones shared with #wg-ring). */
84
+ ${pilotFxRingCss()}
90
85
  #wg-pilot-fx-todos{
91
86
  position:fixed;z-index:2147483645;top:72px;max-width:280px;max-height:calc(100vh - 100px);overflow:auto;
92
87
  font:12px/1.4 ui-sans-serif,system-ui,sans-serif;color:#fff;
93
- background:rgba(20,10,40,.94);border-radius:10px;padding:10px 12px;
88
+ background:${SURFACE};border-radius:10px;padding:10px 12px;
94
89
  box-shadow:0 2px 10px rgba(0,0,0,.3);pointer-events:auto;
95
90
  transition:opacity .25s ease;
96
91
  }
@@ -113,7 +108,7 @@ const OVERLAY_CSS = `
113
108
  /* Zoom HUD (demo parity - badge only; no CSS scale of the page). */
114
109
  #wg-pilot-fx-zoom{position:fixed;z-index:2147483646;bottom:14px;right:14px;
115
110
  display:flex;align-items:center;gap:7px;padding:6px 11px 6px 8px;
116
- border-radius:999px;background:rgba(20,10,40,.94);color:#f0e8ff;
111
+ border-radius:999px;background:${SURFACE};color:#f0e8ff;
117
112
  border:1px solid rgba(250,204,21,.6);box-shadow:0 6px 18px rgba(0,0,0,.4);
118
113
  font:700 12px/1.2 system-ui,sans-serif;pointer-events:none;}
119
114
  #wg-pilot-fx-zoom .wg-pilot-fx-zoom-ico{width:18px;height:18px;display:flex;align-items:center;justify-content:center;
@@ -122,7 +117,7 @@ const OVERLAY_CSS = `
122
117
  #wg-pilot-fx-zoom .wg-pilot-fx-zoom-val{color:#fde68a;font-variant-numeric:tabular-nums;font-weight:800;min-width:3.2em;}
123
118
  #wg-pilot-fx-zoom[data-zoomed="1"]{border-color:#fbbf24;}
124
119
  #wg-pilot-fx-device{position:fixed;z-index:2147483646;top:14px;right:14px;
125
- padding:8px 12px;border-radius:10px;background:rgba(20,10,40,.94);color:#f0e8ff;
120
+ padding:8px 12px;border-radius:10px;background:${SURFACE};color:#f0e8ff;
126
121
  border:1px solid rgba(124,58,237,.55);font:700 12px/1.2 system-ui,sans-serif;
127
122
  pointer-events:none;box-shadow:0 6px 18px rgba(0,0,0,.35);}
128
123
  #wg-pilot-fx-device[data-preset=mobile]{border-color:#3B82F6;}
@@ -0,0 +1,68 @@
1
+ // In-page installer (runs inside the browser via page.evaluate): must stay self-contained - no module-scope closure.
2
+ // Split out of the former single installOverlay evaluate (see src/ARCHITECTURE.md). Shares state only via window.__wg*.
3
+ /* eslint-disable no-unused-vars */
4
+ export function installBanner({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi }) {
5
+ const POSITIONS = ["left", "center", "right"];
6
+ const applyPos = (el, pos) => {
7
+ el.dataset.pos = pos;
8
+ try {
9
+ localStorage.setItem("wg-banner-pos", pos);
10
+ } catch {
11
+ /* private mode / blocked storage - position still applies this page */
12
+ }
13
+ };
14
+ if (title) {
15
+ let banner = document.getElementById("wg-banner");
16
+ if (!banner) {
17
+ banner = document.createElement("div");
18
+ banner.id = "wg-banner";
19
+ let saved = null;
20
+ try {
21
+ saved = localStorage.getItem("wg-banner-pos");
22
+ } catch {
23
+ /* ignore */
24
+ }
25
+ const startPos =
26
+ saved && POSITIONS.includes(saved) ? saved : bannerPos;
27
+ applyPos(banner, startPos);
28
+ banner.title = "Click to move: top left / center / right";
29
+ banner.addEventListener("click", (e) => {
30
+ e.stopPropagation();
31
+ const i = POSITIONS.indexOf(banner.dataset.pos || "left");
32
+ applyPos(banner, POSITIONS[(i + 1) % POSITIONS.length]);
33
+ });
34
+ const tag = document.createElement("span");
35
+ tag.className = "wg-banner-tag";
36
+ tag.textContent = "waygraph demo";
37
+ const text = document.createElement("span");
38
+ text.className = "wg-banner-text";
39
+ text.textContent = title;
40
+ banner.appendChild(tag);
41
+ banner.appendChild(text);
42
+ document.documentElement.appendChild(banner);
43
+ } else {
44
+ // Fixture / episode title changes every step - update in place
45
+ // (banner is created once; do not leave the first step's text stuck).
46
+ let text = banner.querySelector(".wg-banner-text");
47
+ if (!text) {
48
+ text = document.createElement("span");
49
+ text.className = "wg-banner-text";
50
+ banner.appendChild(text);
51
+ }
52
+ text.textContent = title;
53
+ }
54
+ }
55
+ // Tab title/favicon: a real navigation resets document.title and any
56
+ // <link rel="icon"> the new document brings, so re-check (not
57
+ // re-append) on every call instead of a one-time flag.
58
+ if (!document.title.startsWith("[waygraph] ")) {
59
+ document.title = "[waygraph] " + document.title;
60
+ }
61
+ let iconLink = document.querySelector("link[rel~='icon']");
62
+ if (!iconLink) {
63
+ iconLink = document.createElement("link");
64
+ iconLink.rel = "icon";
65
+ document.head.appendChild(iconLink);
66
+ }
67
+ if (iconLink.href !== favicon) iconLink.href = favicon;
68
+ }
@@ -0,0 +1,267 @@
1
+ // In-page installer (runs inside the browser via page.evaluate): must stay self-contained - no module-scope closure.
2
+ // Split out of the former single installOverlay evaluate (see src/ARCHITECTURE.md). Shares state only via window.__wg*.
3
+ /* eslint-disable no-unused-vars */
4
+ export function installCore({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi }) {
5
+ window.__wgTodoDockUi = todoDockUi || {
6
+ compact: true,
7
+ cap: 5,
8
+ expandCap: 14,
9
+ collision: true,
10
+ behindRing: true,
11
+ };
12
+ // Seed the live autoplay toggle from the env default on first ever
13
+ // load only - a real navigation re-runs this, and re-stamping here
14
+ // would silently undo a human's mid-run checkbox click.
15
+ try {
16
+ if (localStorage.getItem("wg-autoplay") === null) {
17
+ localStorage.setItem("wg-autoplay", envAutoplay ? "1" : "0");
18
+ }
19
+ } catch {
20
+ /* private mode / blocked storage - falls back to manual gating */
21
+ }
22
+ if (!document.getElementById("wg-ring")) {
23
+ const ring = document.createElement("div");
24
+ ring.id = "wg-ring";
25
+ document.documentElement.appendChild(ring);
26
+ }
27
+ if (!document.getElementById("wg-ring-label")) {
28
+ const ringLabel = document.createElement("div");
29
+ ringLabel.id = "wg-ring-label";
30
+ document.documentElement.appendChild(ringLabel);
31
+ }
32
+ if (!document.getElementById("wg-cursor")) {
33
+ const cursor = document.createElement("div");
34
+ cursor.id = "wg-cursor";
35
+ // Dark fill + white stroke, same as help-center-clip-engine's own
36
+ // #clip-cursor - visible against any page background, light or
37
+ // dark, unlike a plain solid-white shape.
38
+ cursor.innerHTML =
39
+ "<svg viewBox='0 0 32 32' width='24' height='24'>" +
40
+ "<path fill='#0C0C1A' stroke='#fff' stroke-width='1.4' stroke-linejoin='round' " +
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);
43
+ }
44
+ if (!document.getElementById("wg-click-pulse")) {
45
+ const pulse = document.createElement("div");
46
+ pulse.id = "wg-click-pulse";
47
+ document.documentElement.appendChild(pulse);
48
+ }
49
+ window.__wgMoveCursorTo = (x, y, ms, instant) => {
50
+ const cursor = document.getElementById("wg-cursor");
51
+ if (!cursor) return;
52
+ cursor.style.setProperty("--wg-cursor-ms", (ms || 600) + "ms");
53
+ if (instant) {
54
+ const prev = cursor.style.transition;
55
+ cursor.style.transition = "none";
56
+ cursor.style.transform = "translate(" + x + "px," + y + "px)";
57
+ void cursor.offsetWidth;
58
+ cursor.style.transition = prev || "";
59
+ } else {
60
+ cursor.style.transform = "translate(" + x + "px," + y + "px)";
61
+ }
62
+ cursor.style.opacity = "1";
63
+ };
64
+ window.__wgHideCursor = () => {
65
+ const cursor = document.getElementById("wg-cursor");
66
+ if (cursor) cursor.style.opacity = "0";
67
+ };
68
+ window.__wgPositionRing = (box, label, tone, style) => {
69
+ const ring = document.getElementById("wg-ring");
70
+ const ringLabel = document.getElementById("wg-ring-label");
71
+ if (!ring || !ringLabel || !box) return;
72
+ const raw = (tone || "planned") + "";
73
+ const t =
74
+ raw === "auto" || raw === "info" || raw === "warning" || raw === "danger" || raw === "success"
75
+ ? raw
76
+ : "planned";
77
+ const st = style && typeof style === "object" ? style : {};
78
+ const sizeRaw = (st.size || "md") + "";
79
+ const size =
80
+ sizeRaw === "sm" || sizeRaw === "lg" ? sizeRaw : "md";
81
+ const weightRaw = (st.weight || "normal") + "";
82
+ const weight = weightRaw === "bold" ? "bold" : "normal";
83
+ ring.dataset.tone = t;
84
+ ringLabel.dataset.tone = t;
85
+ ring.dataset.size = size;
86
+ ringLabel.dataset.size = size;
87
+ ringLabel.dataset.weight = weight;
88
+ const pad = size === "sm" ? 3 : size === "lg" ? 10 : 6;
89
+ const margin = 6;
90
+ const vw = window.innerWidth;
91
+ const vh = window.innerHeight;
92
+ // Pin ring EXACTLY to the element - never clamp the ring away from
93
+ // the target (that looked "strapped on" the viewport).
94
+ const left = box.x - pad;
95
+ const top = box.y - pad;
96
+ const width = Math.max(4, box.width + pad * 2);
97
+ const height = Math.max(4, box.height + pad * 2);
98
+ ring.style.left = left + "px";
99
+ ring.style.top = top + "px";
100
+ ring.style.width = width + "px";
101
+ ring.style.height = height + "px";
102
+ ring.style.opacity = "1";
103
+ ringLabel.textContent = label || "";
104
+ ringLabel.style.opacity = "1";
105
+ const lw = ringLabel.offsetWidth;
106
+ const lh = ringLabel.offsetHeight;
107
+ let labelLeft = left;
108
+ let labelTop = top + height + 8;
109
+ if (labelTop + lh > vh - margin) labelTop = top - lh - 8;
110
+ if (labelTop < margin) labelTop = margin;
111
+ if (labelLeft + lw > vw - margin) labelLeft = Math.max(margin, vw - margin - lw);
112
+ if (labelLeft < margin) labelLeft = margin;
113
+ ringLabel.style.left = labelLeft + "px";
114
+ ringLabel.style.top = labelTop + "px";
115
+ if (window.__wgTodosSetBehind) window.__wgTodosSetBehind(true);
116
+ if (window.__wgTodosAvoidRingCollision) window.__wgTodosAvoidRingCollision(box);
117
+ };
118
+ /** Dim/lower floating todo docks while a ring caption is the focus. */
119
+ window.__wgTodosSetBehind = (on) => {
120
+ const ui = window.__wgTodoDockUi || {};
121
+ if (ui.behindRing === false) {
122
+ document.querySelectorAll(".wg-todo-dock, #wg-todo-dock").forEach((el) => {
123
+ el.classList.remove("wg-todo-behind");
124
+ });
125
+ return;
126
+ }
127
+ document.querySelectorAll(".wg-todo-dock, #wg-todo-dock").forEach((el) => {
128
+ el.classList.toggle("wg-todo-behind", !!on);
129
+ });
130
+ };
131
+ /**
132
+ * If the active ring box intersects a todo dock, flip L/R once.
133
+ * Locked until hideRing so follow-rAF does not thrash.
134
+ */
135
+ window.__wgTodosAvoidRingCollision = (box) => {
136
+ const ui = window.__wgTodoDockUi || {};
137
+ if (ui.collision === false) return;
138
+ if (!box || window.__wgTodoCollisionLocked) return;
139
+ const pad = 10;
140
+ const rx1 = box.x - pad;
141
+ const ry1 = box.y - pad;
142
+ const rx2 = box.x + box.width + pad;
143
+ const ry2 = box.y + box.height + pad;
144
+ const overlaps = (el) => {
145
+ const r = el.getBoundingClientRect();
146
+ return !(r.right < rx1 || r.left > rx2 || r.bottom < ry1 || r.top > ry2);
147
+ };
148
+ const docks = [...document.querySelectorAll(".wg-todo-dock, #wg-todo-dock")].filter(
149
+ (el, i, arr) => arr.indexOf(el) === i,
150
+ );
151
+ for (const dock of docks) {
152
+ if (!overlaps(dock)) continue;
153
+ const cur = dock.dataset.pos === "right" ? "right" : "left";
154
+ dock.dataset.pos = cur === "right" ? "left" : "right";
155
+ try {
156
+ localStorage.setItem("wg-todo-pos", dock.dataset.pos);
157
+ } catch {
158
+ /* ignore */
159
+ }
160
+ // Still colliding after flip - tuck (shorter max-height).
161
+ if (overlaps(dock)) dock.dataset.tucked = "1";
162
+ else delete dock.dataset.tucked;
163
+ window.__wgTodoCollisionLocked = true;
164
+ break;
165
+ }
166
+ };
167
+ /** Live-follow a selector with rAF (accurate under device shell scale). */
168
+ window.__wgStopRingFollow = () => {
169
+ if (window.__wgRingFollowRaf) {
170
+ cancelAnimationFrame(window.__wgRingFollowRaf);
171
+ window.__wgRingFollowRaf = 0;
172
+ }
173
+ window.__wgRingFollowSel = "";
174
+ };
175
+ window.__wgFollowRing = (sel, label, tone, style, focus) => {
176
+ window.__wgStopRingFollow();
177
+ if (!sel) return;
178
+ window.__wgRingFollowSel = sel;
179
+ const tick = () => {
180
+ if (window.__wgRingFollowSel !== sel) return;
181
+ const el = document.querySelector(sel);
182
+ if (!el) {
183
+ window.__wgRingFollowRaf = requestAnimationFrame(tick);
184
+ return;
185
+ }
186
+ const r = el.getBoundingClientRect();
187
+ const box = { x: r.x, y: r.y, width: r.width, height: r.height };
188
+ if (window.__wgPositionRing) {
189
+ window.__wgPositionRing(box, label, tone || "planned", style || {});
190
+ }
191
+ if (focus && window.__wgApplyFocus) window.__wgApplyFocus(box);
192
+ window.__wgRingFollowRaf = requestAnimationFrame(tick);
193
+ };
194
+ tick();
195
+ };
196
+ window.__wgClickPulse = (x, y, tone) => {
197
+ const pulse = document.getElementById("wg-click-pulse");
198
+ if (!pulse) return;
199
+ const raw = (tone || "planned") + "";
200
+ pulse.dataset.tone =
201
+ raw === "auto" || raw === "info" || raw === "warning" || raw === "danger" || raw === "success"
202
+ ? raw
203
+ : "planned";
204
+ pulse.style.left = x + "px";
205
+ pulse.style.top = y + "px";
206
+ pulse.classList.remove("wg-pulse");
207
+ void pulse.offsetWidth;
208
+ pulse.classList.add("wg-pulse");
209
+ };
210
+ window.__wgHideRing = (opts) => {
211
+ // While a narrate() call owns the ring (mid multi-step action),
212
+ // the auto-highlight click/fill patches' own end-of-step hide is
213
+ // a no-op - narrate() itself clears it once the WHOLE wrapped
214
+ // action finishes, not just its first sub-step.
215
+ if (window.__wgNarrateOwnsRing) return;
216
+ if (window.__wgStopRingFollow) window.__wgStopRingFollow();
217
+ const ring = document.getElementById("wg-ring");
218
+ const ringLabel = document.getElementById("wg-ring-label");
219
+ if (ring) ring.style.opacity = "0";
220
+ if (ringLabel) ringLabel.style.opacity = "0";
221
+ if (window.__wgClearFocus) window.__wgClearFocus();
222
+ if (window.__wgTodosSetBehind) window.__wgTodosSetBehind(false);
223
+ window.__wgTodoCollisionLocked = false;
224
+ document.querySelectorAll(".wg-todo-dock[data-tucked], #wg-todo-dock[data-tucked]").forEach((el) => {
225
+ delete el.dataset.tucked;
226
+ });
227
+ // Honor zoomOut:false - keep camera until next zoom / desktop clear.
228
+ const forceClear = opts && opts.clearZoom === true;
229
+ const skipClear = opts && opts.clearZoom === false;
230
+ if (
231
+ !skipClear &&
232
+ (forceClear || window.__wgZoomOutOnHide !== false) &&
233
+ window.__wgClearZoom
234
+ ) {
235
+ window.__wgClearZoom();
236
+ }
237
+ };
238
+ window.__wgClearFocus = () => {
239
+ const veil = document.getElementById("wg-focus-veil");
240
+ if (!veil) return;
241
+ veil.classList.remove("wg-in");
242
+ setTimeout(() => {
243
+ const v = document.getElementById("wg-focus-veil");
244
+ if (v && !v.classList.contains("wg-in")) v.remove();
245
+ }, 320);
246
+ };
247
+ window.__wgApplyFocus = (box) => {
248
+ if (!box) {
249
+ if (window.__wgClearFocus) window.__wgClearFocus();
250
+ return;
251
+ }
252
+ let veil = document.getElementById("wg-focus-veil");
253
+ if (!veil) {
254
+ veil = document.createElement("div");
255
+ veil.id = "wg-focus-veil";
256
+ veil.setAttribute("data-wg-ui", "1");
257
+ document.documentElement.appendChild(veil);
258
+ }
259
+ const pad = 10;
260
+ veil.style.left = Math.max(0, box.x - pad) + "px";
261
+ veil.style.top = Math.max(0, box.y - pad) + "px";
262
+ veil.style.width = Math.max(8, box.width + pad * 2) + "px";
263
+ veil.style.height = Math.max(8, box.height + pad * 2) + "px";
264
+ void veil.offsetWidth;
265
+ veil.classList.add("wg-in");
266
+ };
267
+ }