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
@@ -0,0 +1,211 @@
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 installDevice({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi }) {
5
+ // Device toast + chip (0.13.2+) - announce on set/clear; quiet chip on keep.
6
+ // Payload: { sync, device?, remain?, announce? }
7
+ window.__wgSyncDevice = (payload) => {
8
+ const sync =
9
+ payload && (payload.sync === "clear" || payload.sync === "keep" || payload.sync === "set")
10
+ ? payload.sync
11
+ : "keep";
12
+ const device = payload && payload.device ? payload.device : null;
13
+ const cursor = document.getElementById("wg-cursor");
14
+ const MOUSE_SVG =
15
+ "<svg viewBox='0 0 32 32' width='24' height='24'>" +
16
+ "<path fill='#0C0C1A' stroke='#fff' stroke-width='1.4' stroke-linejoin='round' " +
17
+ "d='M6 3.5l1.4 22.5 5.8-5.4 4.2 9.4 3.6-1.6-4.2-9.2H26z'/></svg>";
18
+ const FINGER_SVG =
19
+ "<svg viewBox='0 0 32 32' width='28' height='28'>" +
20
+ "<ellipse cx='16' cy='22' rx='7' ry='8' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
21
+ "<rect x='12' y='6' width='8' height='16' rx='4' fill='#0C0C1A' stroke='#fff' stroke-width='1.4'/>" +
22
+ "</svg>";
23
+ const ICONS = {
24
+ mobile:
25
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
26
+ '<rect x="7" y="2.5" width="10" height="19" rx="2.2"/>' +
27
+ '<circle cx="12" cy="18.2" r="1.1" fill="#fff" stroke="none"/></svg>',
28
+ tablet:
29
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
30
+ '<rect x="3.5" y="4" width="17" height="16" rx="2"/>' +
31
+ '<circle cx="12" cy="17.2" r="1" fill="#fff" stroke="none"/></svg>',
32
+ desktop:
33
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
34
+ '<rect x="2.5" y="3.5" width="19" height="12.5" rx="1.5"/>' +
35
+ '<path d="M8 20h8M12 16v4"/></svg>',
36
+ };
37
+ const setCursor = (touch) => {
38
+ if (!cursor) return;
39
+ if (touch) {
40
+ cursor.setAttribute("data-touch", "1");
41
+ cursor.innerHTML = FINGER_SVG;
42
+ } else {
43
+ cursor.removeAttribute("data-touch");
44
+ cursor.innerHTML = MOUSE_SVG;
45
+ }
46
+ };
47
+ const ensureChip = (preset, touch, remain, orientation) => {
48
+ let badge = document.getElementById("wg-device-badge");
49
+ if (preset === "desktop" && !touch) {
50
+ if (badge) {
51
+ badge.classList.remove("wg-in");
52
+ setTimeout(() => {
53
+ const b = document.getElementById("wg-device-badge");
54
+ if (b) b.remove();
55
+ }, 320);
56
+ }
57
+ return;
58
+ }
59
+ if (!badge) {
60
+ badge = document.createElement("div");
61
+ badge.id = "wg-device-badge";
62
+ badge.setAttribute("data-wg-ui", "1");
63
+ document.documentElement.appendChild(badge);
64
+ }
65
+ badge.dataset.preset = preset;
66
+ badge.dataset.touch = touch ? "1" : "0";
67
+ const orient =
68
+ orientation === "landscape" || orientation === "portrait"
69
+ ? orientation
70
+ : "";
71
+ if (orient) badge.dataset.orientation = orient;
72
+ const chipLabel =
73
+ preset === "desktop" && !touch
74
+ ? "desktop"
75
+ : preset +
76
+ (orient ? " · " + (orient === "landscape" ? "land" : "port") : "") +
77
+ (remain ? " · remain" : "");
78
+ badge.innerHTML =
79
+ '<span class="wg-dev-icon">' +
80
+ (ICONS[preset] || ICONS.desktop) +
81
+ '</span><span class="wg-dev-label">' +
82
+ chipLabel +
83
+ "</span>";
84
+ void badge.offsetWidth;
85
+ badge.classList.add("wg-in");
86
+ };
87
+ const showToast = (preset, touch, title, sub, orientation) => {
88
+ let toast = document.getElementById("wg-device-toast");
89
+ if (toast && toast._wgTimer) {
90
+ clearTimeout(toast._wgTimer);
91
+ toast._wgTimer = null;
92
+ }
93
+ if (!toast) {
94
+ toast = document.createElement("div");
95
+ toast.id = "wg-device-toast";
96
+ toast.setAttribute("data-wg-ui", "1");
97
+ document.documentElement.appendChild(toast);
98
+ }
99
+ // Hide chip while toast is up (same corner).
100
+ const badge = document.getElementById("wg-device-badge");
101
+ if (badge) badge.classList.remove("wg-in");
102
+ toast.dataset.preset = preset;
103
+ toast.innerHTML =
104
+ '<span class="wg-dev-icon">' +
105
+ (ICONS[preset] || ICONS.desktop) +
106
+ '</span><span class="wg-dev-copy"><div class="wg-dev-title">' +
107
+ title +
108
+ '</div><div class="wg-dev-sub">' +
109
+ sub +
110
+ "</div></span>";
111
+ toast.classList.remove("wg-out");
112
+ void toast.offsetWidth;
113
+ toast.classList.add("wg-in");
114
+ toast._wgTimer = setTimeout(() => {
115
+ toast.classList.add("wg-out");
116
+ toast.classList.remove("wg-in");
117
+ setTimeout(() => {
118
+ const t = document.getElementById("wg-device-toast");
119
+ if (t) t.remove();
120
+ if (preset !== "desktop" || touch) {
121
+ ensureChip(preset, touch, true, orientation);
122
+ }
123
+ }, 380);
124
+ }, 2200);
125
+ };
126
+
127
+ if (sync === "keep" && !device) return;
128
+
129
+ const isDesktopClear =
130
+ sync === "clear" ||
131
+ (device && device.preset === "desktop" && !device.touchMode);
132
+ if (isDesktopClear) {
133
+ setCursor(false);
134
+ const announce = payload.announce !== false && sync !== "keep";
135
+ if (announce) {
136
+ const vp = device && device.viewport ? device.viewport : { width: 1280, height: 720 };
137
+ showToast(
138
+ "desktop",
139
+ false,
140
+ "Back to desktop",
141
+ vp.width + "×" + vp.height + " · mouse",
142
+ "landscape",
143
+ );
144
+ setTimeout(() => {
145
+ const badge = document.getElementById("wg-device-badge");
146
+ if (badge) badge.remove();
147
+ }, 2600);
148
+ } else {
149
+ const badge = document.getElementById("wg-device-badge");
150
+ if (badge) badge.remove();
151
+ const toast = document.getElementById("wg-device-toast");
152
+ if (toast) toast.remove();
153
+ }
154
+ window.__wgDevicePreset = "desktop";
155
+ window.__wgDeviceOrientation = "landscape";
156
+ window.__wgDeviceTouch = false;
157
+ document.documentElement.dataset.wgTouch = "0";
158
+ if (window.__wgEnsureZoomBadge) window.__wgEnsureZoomBadge();
159
+ return;
160
+ }
161
+
162
+ if (!device) return;
163
+ const preset = device.preset || "desktop";
164
+ const touch = !!device.touchMode;
165
+ const remain = payload.remain !== false && sync !== "clear";
166
+ const vp = device.viewport || {};
167
+ const orient =
168
+ device.orientation === "landscape" || device.orientation === "portrait"
169
+ ? device.orientation
170
+ : vp.height > vp.width
171
+ ? "portrait"
172
+ : "landscape";
173
+ const prev = window.__wgDevicePreset || "";
174
+ const prevOrient = window.__wgDeviceOrientation || "";
175
+ const presetChanged = prev !== preset || (touch && prev === "desktop");
176
+ const orientChanged = prevOrient !== "" && prevOrient !== orient;
177
+ const changed = presetChanged || orientChanged || prev === "";
178
+ setCursor(touch);
179
+ window.__wgDevicePreset = preset;
180
+ window.__wgDeviceOrientation = orient;
181
+ window.__wgDeviceTouch = touch;
182
+ document.documentElement.dataset.wgTouch = touch ? "1" : "0";
183
+ if (window.__wgEnsureZoomBadge) window.__wgEnsureZoomBadge();
184
+
185
+ const announce =
186
+ payload.announce === true ||
187
+ (payload.announce !== false && (sync === "set" || sync === "clear") && changed);
188
+
189
+ const size =
190
+ (vp.width || "?") +
191
+ "×" +
192
+ (vp.height || "?") +
193
+ " · " +
194
+ orient +
195
+ (touch ? " · touch" : "");
196
+ let titleText = {
197
+ mobile: "Now in mobile mode",
198
+ tablet: "Now in tablet mode",
199
+ desktop: "Now in desktop mode",
200
+ }[preset] || "Device updated";
201
+ if (orientChanged && !presetChanged) {
202
+ titleText =
203
+ orient === "landscape" ? "Rotated to landscape" : "Rotated to portrait";
204
+ }
205
+ if (announce) {
206
+ showToast(preset, touch, titleText, size, orient);
207
+ } else {
208
+ ensureChip(preset, touch, remain, orient);
209
+ }
210
+ };
211
+ }
@@ -0,0 +1,152 @@
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 installPanelChrome({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi }) {
5
+ // Hide/Show for #wg-panel - call after every panel.innerHTML refresh.
6
+ // Collapsed = "N / M · block" + Next (when not auto). Never pass
7
+ // forceCollapsed:false - omit so Hide/localStorage wins.
8
+ window.__wgWirePanelChrome = (panel, storageKey, chromeTitle, opts) => {
9
+ if (!panel) return;
10
+ opts = opts || {};
11
+ const ensureChrome = () => {
12
+ let chrome = panel.querySelector(":scope > .wg-chrome");
13
+ if (!chrome) {
14
+ const body = document.createElement("div");
15
+ body.className = "wg-body";
16
+ while (panel.firstChild) body.appendChild(panel.firstChild);
17
+ chrome = document.createElement("div");
18
+ chrome.className = "wg-chrome";
19
+ const titleEl = document.createElement("span");
20
+ titleEl.className = "wg-chrome-title";
21
+ titleEl.textContent = chromeTitle || "waygraph demo";
22
+ const actions = document.createElement("div");
23
+ actions.className = "wg-chrome-actions";
24
+ const nextBtn = document.createElement("button");
25
+ nextBtn.type = "button";
26
+ nextBtn.className = "wg-mini-next";
27
+ nextBtn.setAttribute("data-wg-mini-next", "1");
28
+ nextBtn.textContent = "Next ▶";
29
+ const btn = document.createElement("button");
30
+ btn.type = "button";
31
+ btn.className = "wg-hide-btn";
32
+ btn.setAttribute("data-wg-toggle", "1");
33
+ btn.textContent = "Hide";
34
+ actions.appendChild(nextBtn);
35
+ actions.appendChild(btn);
36
+ chrome.appendChild(titleEl);
37
+ chrome.appendChild(actions);
38
+ panel.appendChild(chrome);
39
+ panel.appendChild(body);
40
+ return;
41
+ }
42
+ // Yap slides ship a bare chrome - ensure actions + mini Next exist.
43
+ let actions = chrome.querySelector(".wg-chrome-actions");
44
+ if (!actions) {
45
+ actions = document.createElement("div");
46
+ actions.className = "wg-chrome-actions";
47
+ const hide = chrome.querySelector("[data-wg-toggle]");
48
+ if (hide) actions.appendChild(hide);
49
+ chrome.appendChild(actions);
50
+ }
51
+ if (!actions.querySelector("[data-wg-mini-next]")) {
52
+ const nextBtn = document.createElement("button");
53
+ nextBtn.type = "button";
54
+ nextBtn.className = "wg-mini-next";
55
+ nextBtn.setAttribute("data-wg-mini-next", "1");
56
+ nextBtn.textContent = "Next ▶";
57
+ actions.insertBefore(nextBtn, actions.firstChild);
58
+ }
59
+ };
60
+ ensureChrome();
61
+ const stepLabel = opts.stepLabel || panel.dataset.wgStepLabel || "";
62
+ if (stepLabel) panel.dataset.wgStepLabel = stepLabel;
63
+ const syncMiniNext = (hidden) => {
64
+ const miniNext = panel.querySelector("[data-wg-mini-next]");
65
+ if (!miniNext) return;
66
+ let auto = false;
67
+ try {
68
+ auto = localStorage.getItem("wg-autoplay") === "1";
69
+ } catch {
70
+ /* ignore */
71
+ }
72
+ const runBtn = document.getElementById("wg-run");
73
+ const show = !!hidden && !auto && !!runBtn;
74
+ miniNext.classList.toggle("wg-mini-next-show", show);
75
+ if (runBtn) {
76
+ miniNext.textContent = runBtn.textContent || "Next ▶";
77
+ miniNext.disabled = !!runBtn.disabled;
78
+ } else {
79
+ miniNext.disabled = true;
80
+ }
81
+ };
82
+ const apply = (hidden, persist) => {
83
+ panel.classList.toggle("wg-collapsed", hidden);
84
+ panel.setAttribute("data-wg-collapsed", hidden ? "1" : "0");
85
+ const t = panel.querySelector("[data-wg-toggle]");
86
+ const titleEl = panel.querySelector(".wg-chrome-title");
87
+ const label = panel.dataset.wgStepLabel || stepLabel;
88
+ if (titleEl) {
89
+ titleEl.textContent = hidden
90
+ ? (label || chromeTitle || "waygraph demo")
91
+ : (chromeTitle || "waygraph demo");
92
+ }
93
+ if (t) t.textContent = hidden ? "Show" : "Hide";
94
+ syncMiniNext(hidden);
95
+ if (persist !== false) {
96
+ try {
97
+ localStorage.setItem(storageKey, hidden ? "1" : "0");
98
+ } catch {
99
+ /* private mode */
100
+ }
101
+ }
102
+ };
103
+ let hidden = false;
104
+ if (opts.forceCollapsed === true) {
105
+ hidden = true;
106
+ } else {
107
+ try {
108
+ hidden = localStorage.getItem(storageKey) === "1";
109
+ } catch {
110
+ /* ignore */
111
+ }
112
+ }
113
+ // force mini does not overwrite Hide preference in storage.
114
+ apply(hidden, opts.forceCollapsed === true ? false : true);
115
+ const toggle = panel.querySelector("[data-wg-toggle]");
116
+ if (toggle && !toggle.dataset.wgWired) {
117
+ toggle.dataset.wgWired = "1";
118
+ toggle.addEventListener("click", (e) => {
119
+ e.stopPropagation();
120
+ apply(!panel.classList.contains("wg-collapsed"), true);
121
+ });
122
+ }
123
+ const miniNext = panel.querySelector("[data-wg-mini-next]");
124
+ if (miniNext && !miniNext.dataset.wgWired) {
125
+ miniNext.dataset.wgWired = "1";
126
+ miniNext.addEventListener("click", (e) => {
127
+ e.stopPropagation();
128
+ const runBtn = document.getElementById("wg-run");
129
+ if (runBtn && !runBtn.disabled) {
130
+ runBtn.click();
131
+ return;
132
+ }
133
+ if (typeof window.__wgNext === "function") window.__wgNext({});
134
+ });
135
+ }
136
+ const autoCb = document.getElementById("wg-autoplay-cb");
137
+ if (autoCb && !autoCb.dataset.wgMiniWired) {
138
+ autoCb.dataset.wgMiniWired = "1";
139
+ autoCb.addEventListener("change", () => {
140
+ syncMiniNext(panel.classList.contains("wg-collapsed"));
141
+ });
142
+ }
143
+ const cur = panel.querySelector("#wg-modules .wg-mod-current");
144
+ if (cur && typeof cur.scrollIntoView === "function") {
145
+ try {
146
+ cur.scrollIntoView({ inline: "center", block: "nearest", behavior: "smooth" });
147
+ } catch {
148
+ cur.scrollIntoView(false);
149
+ }
150
+ }
151
+ };
152
+ }
@@ -0,0 +1,46 @@
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 installStamp({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi }) {
5
+ // Blind-agent overlay beacons: every modal root gets data-wg-ui /
6
+ // data-wg-modal / data-wg-ready so tests can fail on a blank panel.
7
+ window.__wgStampModal = (el, kind, meta) => {
8
+ if (!el) return;
9
+ meta = meta || {};
10
+ el.setAttribute("data-wg-ui", "1");
11
+ el.setAttribute("data-wg-modal", kind || "panel");
12
+ if (meta.phase != null) el.setAttribute("data-wg-phase", String(meta.phase));
13
+ if (meta.step != null) el.setAttribute("data-wg-step", String(meta.step));
14
+ if (meta.total != null) el.setAttribute("data-wg-total", String(meta.total));
15
+ if (meta.block != null) el.setAttribute("data-wg-block", String(meta.block));
16
+ el.setAttribute(
17
+ "data-wg-collapsed",
18
+ el.classList.contains("wg-collapsed") ? "1" : "0",
19
+ );
20
+ el.setAttribute("data-wg-ready", meta.ready === false ? "0" : "1");
21
+ };
22
+ window.__wgOverlayBeacon = () => {
23
+ return Array.from(document.querySelectorAll("[data-wg-ui=\"1\"]")).map((el) => {
24
+ const r = el.getBoundingClientRect();
25
+ const st = getComputedStyle(el);
26
+ return {
27
+ id: el.id || null,
28
+ modal: el.getAttribute("data-wg-modal"),
29
+ ready: el.getAttribute("data-wg-ready") === "1",
30
+ phase: el.getAttribute("data-wg-phase"),
31
+ step: el.getAttribute("data-wg-step"),
32
+ block: el.getAttribute("data-wg-block"),
33
+ collapsed:
34
+ el.getAttribute("data-wg-collapsed") === "1" ||
35
+ el.classList.contains("wg-collapsed"),
36
+ opacity: st.opacity,
37
+ textLen: (el.innerText || "").trim().length,
38
+ w: Math.round(r.width),
39
+ h: Math.round(r.height),
40
+ visible: r.width > 0 && r.height > 0 && Number(st.opacity) > 0.05,
41
+ };
42
+ });
43
+ };
44
+ const existingBanner = document.getElementById("wg-banner");
45
+ if (existingBanner) window.__wgStampModal(existingBanner, "banner", { ready: true });
46
+ }
@@ -0,0 +1,220 @@
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 installSwipeZoom({ title, favicon, bannerPos, todoPos, envAutoplay, todoDockUi }) {
5
+ /**
6
+ * Finger swipe trail for touch theater (orientation rotate / device change).
7
+ * opts: { dir: 'left'|'right'|'up'|'down', label?, ms? }
8
+ * Returns duration waited (ms) via Promise when called from page.evaluate async.
9
+ */
10
+ window.__wgSwipeTrail = (opts) => {
11
+ const o = opts || {};
12
+ const dir = o.dir === "right" || o.dir === "up" || o.dir === "down" ? o.dir : "left";
13
+ const ms = Math.max(280, Math.min(1400, Number(o.ms) || 720));
14
+ const label =
15
+ o.label ||
16
+ (dir === "left" || dir === "right" ? "swipe" : "swipe") +
17
+ (o.hint ? " \u00b7 " + o.hint : "");
18
+ let layer = document.getElementById("wg-swipe-layer");
19
+ if (layer && layer._wgTimer) {
20
+ clearTimeout(layer._wgTimer);
21
+ layer._wgTimer = null;
22
+ }
23
+ if (layer) layer.remove();
24
+ layer = document.createElement("div");
25
+ layer.id = "wg-swipe-layer";
26
+ layer.setAttribute("data-wg-ui", "1");
27
+ const finger = document.createElement("div");
28
+ finger.className = "wg-swipe-finger";
29
+ const lab = document.createElement("div");
30
+ lab.className = "wg-swipe-label";
31
+ lab.textContent = label;
32
+ layer.appendChild(lab);
33
+ layer.appendChild(finger);
34
+ document.documentElement.appendChild(layer);
35
+ const vw = window.innerWidth || 390;
36
+ const vh = window.innerHeight || 844;
37
+ const pad = Math.min(vw, vh) * 0.18;
38
+ let x0;
39
+ let y0;
40
+ let x1;
41
+ let y1;
42
+ if (dir === "left") {
43
+ x0 = vw - pad;
44
+ x1 = pad;
45
+ y0 = y1 = vh * 0.52;
46
+ } else if (dir === "right") {
47
+ x0 = pad;
48
+ x1 = vw - pad;
49
+ y0 = y1 = vh * 0.52;
50
+ } else if (dir === "up") {
51
+ x0 = x1 = vw * 0.5;
52
+ y0 = vh - pad;
53
+ y1 = pad;
54
+ } else {
55
+ x0 = x1 = vw * 0.5;
56
+ y0 = pad;
57
+ y1 = vh - pad;
58
+ }
59
+ finger.style.left = x0 + "px";
60
+ finger.style.top = y0 + "px";
61
+ finger.style.opacity = "1";
62
+ void layer.offsetWidth;
63
+ layer.classList.add("wg-in");
64
+ const steps = 12;
65
+ const stepMs = Math.floor(ms / steps);
66
+ let i = 0;
67
+ const tick = () => {
68
+ i += 1;
69
+ const t = Math.min(1, i / steps);
70
+ const e = t * t * (3 - 2 * t);
71
+ const x = x0 + (x1 - x0) * e;
72
+ const y = y0 + (y1 - y0) * e;
73
+ finger.style.left = x + "px";
74
+ finger.style.top = y + "px";
75
+ const dot = document.createElement("div");
76
+ dot.className = "wg-swipe-dot";
77
+ dot.style.left = x + "px";
78
+ dot.style.top = y + "px";
79
+ dot.style.opacity = String(0.85 - t * 0.55);
80
+ layer.appendChild(dot);
81
+ requestAnimationFrame(() => {
82
+ dot.style.transition = "opacity .45s ease, transform .45s ease";
83
+ dot.style.opacity = "0";
84
+ dot.style.transform = "scale(1.8)";
85
+ });
86
+ if (i < steps) {
87
+ layer._wgTimer = setTimeout(tick, stepMs);
88
+ } else {
89
+ finger.style.opacity = "0";
90
+ layer.classList.remove("wg-in");
91
+ layer._wgTimer = setTimeout(() => {
92
+ const el = document.getElementById("wg-swipe-layer");
93
+ if (el) el.remove();
94
+ }, 380);
95
+ }
96
+ };
97
+ layer._wgTimer = setTimeout(tick, 40);
98
+ return ms + 120;
99
+ };
100
+ window.__wgClearZoom = () => {
101
+ // Strip leftover element transforms from older zoom path.
102
+ document.querySelectorAll("[data-wg-zoomed=\"1\"]").forEach((el) => {
103
+ el.style.removeProperty("transform");
104
+ el.style.removeProperty("transform-origin");
105
+ el.style.removeProperty("transition");
106
+ el.style.removeProperty("z-index");
107
+ el.style.removeProperty("position");
108
+ delete el.dataset.wgZoomed;
109
+ });
110
+ // Undo any prior shell-camera experiment - restore device fit scale only.
111
+ const shell = document.getElementById("wg-device-shell");
112
+ if (shell && shell.dataset.wgCamBase != null) {
113
+ shell.style.transform = shell.dataset.wgCamBase;
114
+ if (shell.dataset.wgCamOrigin) {
115
+ shell.style.transformOrigin = shell.dataset.wgCamOrigin;
116
+ } else {
117
+ shell.style.transformOrigin = "center center";
118
+ }
119
+ delete shell.dataset.wgCamBase;
120
+ delete shell.dataset.wgCamOrigin;
121
+ delete shell.dataset.wgCamZoom;
122
+ }
123
+ window.__wgZoomLevel = 1;
124
+ window.__wgZoomSel = "";
125
+ if (window.__wgSetZoomBadge) window.__wgSetZoomBadge(1, "");
126
+ };
127
+ const ZOOM_ICO =
128
+ '<svg viewBox="0 0 24 24" fill="none" stroke="#fde68a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
129
+ '<circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>';
130
+ window.__wgSetZoomBadge = (scale, sel) => {
131
+ const n = Number(scale);
132
+ const level = Number.isFinite(n) && n > 0 ? n : 1;
133
+ let badge = document.getElementById("wg-zoom-badge");
134
+ if (!badge) {
135
+ badge = document.createElement("div");
136
+ badge.id = "wg-zoom-badge";
137
+ badge.setAttribute("data-wg-ui", "1");
138
+ document.documentElement.appendChild(badge);
139
+ }
140
+ const zoomed = level > 1.001 || level < 0.999;
141
+ badge.dataset.zoomed = zoomed ? "1" : "0";
142
+ const txt = level.toFixed(2) + "\u00d7";
143
+ badge.innerHTML =
144
+ '<span class="wg-zoom-ico">' +
145
+ ZOOM_ICO +
146
+ '</span><span class="wg-zoom-val">' +
147
+ txt +
148
+ "</span>";
149
+ badge.classList.add("wg-in");
150
+ window.__wgZoomLevel = level;
151
+ window.__wgZoomSel = sel || "";
152
+ };
153
+ window.__wgEnsureZoomBadge = () => {
154
+ if (window.__wgSetZoomBadge) {
155
+ window.__wgSetZoomBadge(window.__wgZoomLevel || 1, window.__wgZoomSel || "");
156
+ }
157
+ };
158
+ window.__wgSetTypingBadge = (on, label) => {
159
+ let badge = document.getElementById("wg-typing-badge");
160
+ if (!on) {
161
+ if (badge) {
162
+ badge.classList.remove("wg-in");
163
+ setTimeout(() => {
164
+ const b = document.getElementById("wg-typing-badge");
165
+ if (b && !b.classList.contains("wg-in")) b.remove();
166
+ }, 220);
167
+ }
168
+ return;
169
+ }
170
+ if (!badge) {
171
+ badge = document.createElement("div");
172
+ badge.id = "wg-typing-badge";
173
+ badge.setAttribute("data-wg-ui", "1");
174
+ document.documentElement.appendChild(badge);
175
+ }
176
+ const text = label ? String(label).slice(0, 40) : "typing";
177
+ badge.innerHTML =
178
+ '<span class="wg-ty-dots">\u25cf\u25cf\u25cf</span><span>typing \u00b7 ' +
179
+ text.replace(/</g, "&lt;") +
180
+ "</span>";
181
+ void badge.offsetWidth;
182
+ badge.classList.add("wg-in");
183
+ };
184
+ /**
185
+ * Zoom HUD only - never CSS-scale the target or the device shell.
186
+ * Shell camera zoom fought device-theater fit-scale (broke ring follow
187
+ * + uneven border-radius). Element scale mangled cart rows. Emphasis
188
+ * is ring + focus veil; badge still shows authored zoom level.
189
+ */
190
+ window.__wgApplyZoom = (sel, scale) => {
191
+ document.querySelectorAll("[data-wg-zoomed=\"1\"]").forEach((el) => {
192
+ el.style.removeProperty("transform");
193
+ el.style.removeProperty("transform-origin");
194
+ el.style.removeProperty("transition");
195
+ el.style.removeProperty("z-index");
196
+ el.style.removeProperty("position");
197
+ delete el.dataset.wgZoomed;
198
+ });
199
+ const shell = document.getElementById("wg-device-shell");
200
+ if (shell && shell.dataset.wgCamBase != null) {
201
+ shell.style.transform = shell.dataset.wgCamBase;
202
+ shell.style.transformOrigin = shell.dataset.wgCamOrigin || "center center";
203
+ delete shell.dataset.wgCamBase;
204
+ delete shell.dataset.wgCamOrigin;
205
+ delete shell.dataset.wgCamZoom;
206
+ }
207
+ const n = Number(scale);
208
+ if (!Number.isFinite(n) || n <= 1.001) {
209
+ window.__wgZoomLevel = 1;
210
+ window.__wgZoomSel = "";
211
+ if (window.__wgSetZoomBadge) window.__wgSetZoomBadge(1, "");
212
+ return;
213
+ }
214
+ window.__wgZoomLevel = n;
215
+ window.__wgZoomSel = sel || "";
216
+ if (window.__wgSetZoomBadge) window.__wgSetZoomBadge(n, sel || "");
217
+ };
218
+ // Mount zoom HUD immediately so demos always show 1.00x.
219
+ if (window.__wgEnsureZoomBadge) window.__wgEnsureZoomBadge();
220
+ }