waygraph 0.15.28 → 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.
@@ -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
+ }
@@ -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
+ }