openvibers/openvibe.shared ov-window.js

ov-window.js

main 23.3 KB · 320 lines · javascript

1/**
2 * ov-window.js (openvibe-shared 4.202.0): one window for every little interface. Asks 304 and 318, the owner: "a system
3 * that's used for all the windows [in] harmony … people can make their own or … custom … everything … feels connected".
4 * One frame (a head with the title and ×, a body, a foot) in three kinds, the same motion, the same keys:
5 * dialog: centred on a computer; a sheet from the bottom on a phone
6 * sheet: a sheet from the bottom on every screen
7 * panel: floats beside what opened it on a computer, draggable by its head (where you leave it is kept per id);
8 * a sheet on a phone
9 * It grows out of the button that opened it (the navbar's pop motion), closes with ×, Escape, a press outside (a dialog
10 * or a sheet; a panel stays until closed, and the page under it stays usable) or a swipe down on a phone, keeps the focus inside while open, and gives it back to that button. Windows stack: Escape closes the
11 * top one. The look follows the person's theme and their window style, <html data-ov-window="glass|solid|outline">
12 * (OVWindow.setStyle), on every window at once.
13 *
14 * const w = OVWindow.open({ id, title, body: node | text, foot: node, kind: 'dialog', size: 'md', from: button,
15 * label, onClose, bare, width }); bare: the content draws its own head (the title is still
16 * read); width: px, for content that wants its own
17 * w.el, w.body, w.foot, w.setTitle(text), w.close()
18 * A panel can be resizable (4.204.0, the owner: a robot's controls "can be popped out … and resized"): { resizable: true,
19 * minWidth, minHeight, onResize({ width, height }) } adds a grip at its corner that a mouse, a finger or a pen drags and
20 * the arrow keys move (Shift for bigger steps, Enter for the first size); the size is kept with its place per id.
21 * A site's content goes in as nodes (or text, set as text): the window never parses markup it is given.
22 */
23(function (root) {
24 'use strict';
25 if (typeof document === 'undefined' || root.OVWindow) return;
26 const HERE = document.currentScript && document.currentScript.src;
27 const WINDOW_REV = { 'ov-float.js': '1e087a0a11fd' };
28
29 const STYLES = ['glass', 'solid', 'outline'];
30 const CSS = `.ovw-scrim { position: fixed; inset: 0; z-index: var(--ovw-z, 10040); background: rgba(3, 7, 15, .5); animation: ovw-fade .2s ease-out both; }
31.ovw-scrim.is-clear { background: transparent; }
32.ovw { position: fixed; z-index: var(--ovw-z, 10040); box-sizing: border-box; display: flex; flex-direction: column; max-height: min(86vh, 760px); color: var(--text-primary, #e6edf7);
33 font: 500 15px/1.45 var(--font, Inter, system-ui, -apple-system, 'Segoe UI', sans-serif); border-radius: 20px; overflow: hidden;
34 background: var(--ovw-bg); border: 1px solid var(--ovw-line); box-shadow: var(--ovw-shadow); -webkit-backdrop-filter: var(--ovw-blur); backdrop-filter: var(--ovw-blur);
35 transform-origin: var(--ov-pop-x, 50%) var(--ov-pop-y, 0); animation: ovw-in .28s cubic-bezier(.2, 1.2, .3, 1) both; }
36:root { --ovw-bg: color-mix(in srgb, var(--bg-elevated, var(--bg-secondary, var(--bg-primary, #111826))) 92%, transparent); --ovw-line: var(--border, rgba(255, 255, 255, .12));
37 --ovw-shadow: 0 28px 70px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .2); --ovw-blur: blur(16px) saturate(1.3); }
38html[data-ov-window="solid"] { --ovw-bg: var(--bg-elevated, var(--bg-secondary, var(--bg-primary, #111826))); --ovw-blur: none; }
39html[data-ov-window="outline"] { --ovw-bg: var(--bg-primary, #0b1220); --ovw-blur: none; --ovw-line: color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); --ovw-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 20%, transparent), 0 18px 40px rgba(0, 0, 0, .35); }
40.ovw.is-out { animation: ovw-out .16s ease-in both; }
41.ovw-h { flex: none; display: flex; align-items: center; gap: 10px; min-height: 56px; padding: 6px 8px 6px 18px; border-bottom: 1px solid color-mix(in srgb, var(--ovw-line) 70%, transparent); }
42.ovw-t { flex: 1; min-width: 0; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16.5px; font-weight: 700; letter-spacing: -.01em; }
43.ovw-x { flex: none; display: grid; place-items: center; width: 40px; height: 40px; padding: 0; border: 0; border-radius: 12px; background: none; color: var(--text-secondary, #a8b3c4); cursor: pointer; }
44.ovw-x:hover, .ovw-x:focus-visible { background: color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); color: var(--text-primary, #e6edf7); }
45.ovw-x:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
46.ovw-b { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 16px 18px 18px; }
47.ovw-f { flex: none; display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 8px; padding: 12px 18px calc(12px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid color-mix(in srgb, var(--ovw-line) 70%, transparent); }
48.ovw-f:empty { display: none; }
49.ovw.is-dialog { left: 50%; top: 50%; width: min(var(--ovw-w), calc(100vw - 32px)); transform: translate(-50%, -50%); animation-name: ovw-in-c; }
50.ovw.is-dialog.is-out { animation-name: ovw-out-c; }
51.ovw.is-panel { width: min(var(--ovw-w), calc(100vw - 24px)); }
52.ovw.is-panel .ovw-h { cursor: grab; touch-action: none; }
53.ovw.is-panel.is-dragging { animation: none; transition: none; box-shadow: 0 34px 90px rgba(0, 0, 0, .55); }
54.ovw.is-panel.is-dragging .ovw-h { cursor: grabbing; }
55.ovw.is-sheet { left: 0; right: 0; bottom: 0; width: auto; max-height: 92vh; margin: 0 auto; max-width: 720px; border-radius: 22px 22px 0 0; border-bottom: 0; animation: ovw-up .32s cubic-bezier(.2, 1, .3, 1) both; }
56.ovw.is-sheet.is-out { animation: ovw-down .2s ease-in both; }
57.ovw.is-sheet .ovw-h { position: relative; padding-top: 16px; touch-action: none; }
58.ovw.is-sheet .ovw-h::before { content: ""; position: absolute; left: 50%; top: 6px; width: 38px; height: 4px; margin-left: -19px; border-radius: 4px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 28%, transparent); }
59.ovw.is-sheet.is-dragging { animation: none; }
60.ovw.is-resizable { max-height: none; }
61.ovw-grip { position: absolute; right: 2px; bottom: 2px; z-index: 2; width: 26px; height: 26px; padding: 0; border: 0; border-radius: 8px; background: none; cursor: nwse-resize; touch-action: none; color: var(--text-secondary, #a8b3c4); }
62.ovw-grip::before { content: ""; position: absolute; right: 6px; bottom: 6px; width: 10px; height: 10px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; border-radius: 0 0 3px 0; opacity: .55; }
63.ovw-grip:hover::before, .ovw-grip:focus-visible::before, .ovw.is-resizing .ovw-grip::before { opacity: 1; color: var(--accent, #3b82f6); }
64.ovw-grip:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; }
65.ovw.is-resizing { animation: none; transition: none; user-select: none; }
66.ovw { --ovw-w: 520px; }
67.ovw.is-sm { --ovw-w: 380px; }
68.ovw.is-lg { --ovw-w: 760px; }
69.ovw.is-full { --ovw-w: 1100px; }
70@keyframes ovw-in { from { opacity: 0; transform: translateY(calc(-6px * var(--ov-move, 1))) scale(.94); } }
71@keyframes ovw-out { to { opacity: 0; transform: scale(.96); } }
72@keyframes ovw-in-c { from { opacity: 0; transform: translate(-50%, calc(-50% + 10px * var(--ov-move, 1))) scale(.95); } }
73@keyframes ovw-out-c { to { opacity: 0; transform: translate(-50%, -50%) scale(.97); } }
74@keyframes ovw-up { from { transform: translateY(100%); } }
75@keyframes ovw-down { to { transform: translateY(100%); } }
76@keyframes ovw-fade { from { opacity: 0; } }
77html[data-ov-motion=off] .ovw, html[data-ov-motion=off] .ovw-scrim { animation: none !important; }
78html.ovw-locked { overflow: hidden; }
79/* bare: the content carries its own head (a card's banner); the title is for screen readers, × floats on top, and
80 the top strip is still the handle (drag a panel, swipe a sheet). */
81.ovw.is-bare .ovw-h { position: absolute; left: 0; right: 0; top: 0; z-index: 3; min-height: 52px; padding: 6px; justify-content: flex-end; border: 0; background: none; }
82.ovw.is-bare .ovw-t { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
83.ovw.is-bare .ovw-x { width: 36px; height: 36px; border-radius: 50%; color: #fff; background: rgba(4, 8, 16, .42); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
84.ovw.is-bare .ovw-x:hover, .ovw.is-bare .ovw-x:focus-visible { background: rgba(4, 8, 16, .62); color: #fff; }
85.ovw.is-bare .ovw-b { padding: 0; }
86.ovw.is-bare.is-sheet .ovw-h { padding-top: 14px; }`;
87
88 const stack = [];
89 let uid = 0;
90 function styles() { if (document.getElementById('ovw-styles')) return; const st = document.createElement('style'); st.id = 'ovw-styles'; st.textContent = CSS; document.head.appendChild(st); }
91
92 // The panel's move, tuck and place are OVFloat's (ov-float.js, loaded from beside this file the first time a panel
93 // opens). A page that already loaded it uses that.
94 let floatLoading = null;
95 const floatWait = [];
96 function withFloat(cb) {
97 if (root.OVFloat) { cb(root.OVFloat); return; }
98 if (!HERE || floatLoading === false) return;
99 if (floatLoading) { floatWait.push(cb); return; }
100 floatWait.push(cb);
101 const s = document.createElement('script');
102 const rev = WINDOW_REV['ov-float.js'];
103 s.src = HERE.replace(/ov-window\.js(?:\?.*)?$/, 'ov-float.js') + (rev ? `?v=${rev}` : '');
104 s.onload = () => {
105 floatLoading = null;
106 for (const fn of floatWait.splice(0)) { try { fn(root.OVFloat); } catch (e) { console.error(e); } }
107 };
108 s.onerror = () => { floatLoading = false; floatWait.length = 0; };
109 floatLoading = s;
110 (document.head || document.documentElement).appendChild(s);
111 }
112 const phone = () => !!(root.matchMedia && root.matchMedia('(max-width: 640px)').matches);
113 const focusables = (el) => [...el.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')].filter((x) => x.offsetParent !== null || x === document.activeElement);
114 const place = { get(id) { try { return JSON.parse(root.localStorage.getItem(`ov:window:${id}`) || 'null'); } catch { return null; } }, set(id, v) { try { root.localStorage.setItem(`ov:window:${id}`, JSON.stringify(v)); } catch { /* private mode */ } } };
115 function put(parent, v) { if (v == null) return; if (typeof v === 'string') parent.textContent = v; else parent.appendChild(v); }
116
117 function open(o = {}) {
118 styles();
119 const kind0 = ['dialog', 'sheet', 'panel'].includes(o.kind) ? o.kind : 'dialog';
120 const kind = phone() ? 'sheet' : kind0;
121 const id = o.id || `ovw-${++uid}`;
122 const opener = o.from || document.activeElement;
123 const modal = o.modal !== false && kind !== 'panel';
124 const z = 10040 + stack.length * 2;
125 const scrim = document.createElement('div');
126 scrim.className = `ovw-scrim${modal ? '' : ' is-clear'}`;
127 scrim.style.setProperty('--ovw-z', String(z));
128 const el = document.createElement('section');
129 el.className = `ovw is-${kind} is-${['sm', 'md', 'lg', 'full'].includes(o.size) ? o.size : 'md'}${o.bare ? ' is-bare' : ''}`;
130 el.style.setProperty('--ovw-z', String(z + 1));
131 if (Number(o.width) > 0) el.style.setProperty('--ovw-w', `${Math.round(Number(o.width))}px`);
132 el.setAttribute('role', 'dialog');
133 if (modal) el.setAttribute('aria-modal', 'true');
134 el.dataset.ovWindow = id;
135 const head = document.createElement('header'); head.className = 'ovw-h';
136 const title = document.createElement('h2'); title.className = 'ovw-t'; title.id = `${id}-t`; title.textContent = o.title || '';
137 const x = document.createElement('button'); x.type = 'button'; x.className = 'ovw-x'; x.setAttribute('aria-label', 'Close');
138 x.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';
139 head.append(title, x);
140 const body = document.createElement('div'); body.className = 'ovw-b'; put(body, o.body);
141 const foot = document.createElement('footer'); foot.className = 'ovw-f'; put(foot, o.foot);
142 el.append(head, body, foot);
143 if (o.title) el.setAttribute('aria-labelledby', title.id); else el.setAttribute('aria-label', o.label || 'Window');
144 if (modal) document.body.append(scrim, el); else document.body.append(el);
145 if (modal) document.documentElement.classList.add('ovw-locked');
146
147 // Where it opens: a panel beside its opener (or where it was left), a dialog centred, a sheet at the bottom.
148 let panelLeft = 0, panelTop = 0;
149 if (kind === 'panel') {
150 const saved = o.id ? place.get(o.id) : null;
151 const cs = document.documentElement;
152 const r = el.getBoundingClientRect(), vw = cs.clientWidth || root.innerWidth, vh = cs.clientHeight || root.innerHeight;
153 let left, top;
154 if (saved) { left = saved.x; top = saved.y; }
155 else if (opener && opener.getBoundingClientRect) { const f = opener.getBoundingClientRect(); left = f.left; top = f.bottom + 8; if (top + r.height > vh - 12) top = Math.max(12, f.top - r.height - 8); }
156 else { left = vw - r.width - 24; top = 72; }
157 left = Math.max(8, Math.min(left, vw - r.width - 8));
158 top = Math.max(8, Math.min(top, vh - 60));
159 panelLeft = left; panelTop = top;
160 el.style.left = `${left}px`;
161 el.style.top = `${top}px`;
162 }
163 if (opener && opener.getBoundingClientRect && kind !== 'sheet') {
164 const r = el.getBoundingClientRect(), f = opener.getBoundingClientRect();
165 el.style.setProperty('--ov-pop-x', `${Math.round(f.left + f.width / 2 - r.left)}px`);
166 el.style.setProperty('--ov-pop-y', `${Math.round(f.top + f.height / 2 - r.top)}px`);
167 }
168
169 let closed = false;
170 const w = {
171 id, el, body, foot,
172 setTitle(t) { title.textContent = t || ''; },
173 close(why) {
174 if (closed) return; closed = true;
175 if (w.float) { try { w.float.destroy(); } catch { /* */ } w.float = null; }
176 const i = stack.indexOf(w); if (i >= 0) stack.splice(i, 1);
177 if (!stack.some((s) => s.modal)) document.documentElement.classList.remove('ovw-locked');
178 const gone = () => { el.remove(); scrim.remove(); };
179 if (document.documentElement.getAttribute('data-ov-motion') === 'off') gone();
180 else { el.classList.add('is-out'); scrim.style.opacity = '0'; scrim.style.transition = 'opacity .16s'; let done = false; const end = () => { if (!done) { done = true; gone(); } }; el.addEventListener('animationend', end, { once: true }); setTimeout(end, 260); }
181 if (opener && opener.focus && document.contains(opener)) { try { opener.focus({ preventScroll: true }); } catch { /* */ } }
182 if (typeof o.onClose === 'function') { try { o.onClose(why || 'close'); } catch (e) { console.error(e); } }
183 },
184 modal,
185 float: null,
186 };
187 stack.push(w);
188 if (kind === 'panel') {
189 const panelId = o.id ? String(o.id) : '';
190 withFloat((OVFloat) => {
191 if (closed) return;
192 w.float = OVFloat.attach(el, {
193 frame: null, key: panelId ? `ov:window:float:${panelId}` : undefined,
194 defaults: {
195 x: Math.max(0, Math.min(1, panelLeft / Math.max(1, (document.documentElement.clientWidth || root.innerWidth)))),
196 y: Math.max(0, Math.min(1, panelTop / Math.max(1, (document.documentElement.clientHeight || root.innerHeight)))),
197 mode: 'float',
198 },
199 aspect: null, size: false, resize: false, doubleTap: false, activeBar: false,
200 viewport: 'client', handle: head, store: null, hintClass: 'ovw-dock-hint',
201 onChange(geom) {
202 if (!panelId) return;
203 // The fractions convert back to the same pixels without reading a rect mid-landing. (The old
204 // key stays as the absolute place older releases wrote, and for the size the grip keeps there.)
205 const vw = document.documentElement.clientWidth || root.innerWidth;
206 const vh = document.documentElement.clientHeight || root.innerHeight;
207 place.set(panelId, Object.assign(place.get(panelId) || {}, { x: Math.round(geom.x * vw), y: Math.round(geom.y * vh) }));
208 },
209 });
210 });
211 }
212 x.addEventListener('click', () => w.close('x'));
213 if (modal) scrim.addEventListener('pointerdown', () => w.close('outside'));
214 el.addEventListener('keydown', (e) => {
215 if (e.key === 'Tab') {
216 const f = focusables(el); if (!f.length) return;
217 const first = f[0], last = f[f.length - 1];
218 if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
219 else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
220 }
221 });
222
223 // A sheet goes down when swiped down on a phone; a panel's head is OVFloat's handle (with its tuck and place).
224 if (kind === 'sheet') {
225 head.addEventListener('pointerdown', (e) => {
226 if (e.target.closest('.ovw-x') || (e.button !== undefined && e.button !== 0)) return;
227 const r = el.getBoundingClientRect(), sx = e.clientX, sy = e.clientY;
228 let moved = false;
229 el.classList.add('is-dragging');
230 try { head.setPointerCapture(e.pointerId); } catch { /* */ }
231 const move = (ev) => {
232 const dx = ev.clientX - sx, dy = ev.clientY - sy;
233 if (!moved && Math.hypot(dx, dy) < 4) return;
234 moved = true;
235 if (kind === 'panel') {
236 el.style.left = `${Math.max(8, Math.min(r.left + dx, root.innerWidth - r.width - 8))}px`;
237 el.style.top = `${Math.max(8, Math.min(r.top + dy, root.innerHeight - 56))}px`;
238 } else el.style.transform = `translateY(${Math.max(0, dy)}px)`;
239 };
240 const up = (ev) => {
241 head.removeEventListener('pointermove', move); head.removeEventListener('pointerup', up); head.removeEventListener('pointercancel', up);
242 el.classList.remove('is-dragging');
243 if (kind === 'panel') { if (moved && o.id) { const b = el.getBoundingClientRect(); place.set(o.id, Object.assign(place.get(o.id) || {}, { x: Math.round(b.left), y: Math.round(b.top) })); } return; }
244 const dy = ev.clientY - sy;
245 if (moved && dy > Math.min(140, r.height * 0.28)) w.close('swipe');
246 else { el.style.transition = 'transform .2s cubic-bezier(.2, 1, .3, 1)'; el.style.transform = ''; setTimeout(() => { el.style.transition = ''; }, 220); }
247 };
248 head.addEventListener('pointermove', move); head.addEventListener('pointerup', up); head.addEventListener('pointercancel', up);
249 });
250 }
251
252 if (kind === 'panel' && o.resizable) resizable(el, o);
253
254 const first = o.focus ? body.querySelector(o.focus) : focusables(body)[0];
255 (first || x).focus({ preventScroll: true });
256 return w;
257 }
258
259 /** A panel's corner grip: pointer drag (mouse, finger, pen) and the arrow keys; the size is kept with its place. */
260 function resizable(el, o) {
261 el.classList.add('is-resizable');
262 const minW = Math.max(200, Number(o.minWidth) || 260), minH = Math.max(120, Number(o.minHeight) || 160);
263 const grip = document.createElement('button');
264 grip.type = 'button'; grip.className = 'ovw-grip';
265 grip.setAttribute('aria-label', 'Resize: drag, or use the arrow keys');
266 el.append(grip);
267 const first = { w: el.offsetWidth, h: el.offsetHeight };
268 const set = (w, h, save) => {
269 const r = el.getBoundingClientRect();
270 const W = Math.round(Math.max(minW, Math.min(w, root.innerWidth - r.left - 8)));
271 const H = Math.round(Math.max(minH, Math.min(h, root.innerHeight - r.top - 8)));
272 el.style.width = `${W}px`; el.style.height = `${H}px`;
273 if (save && o.id) place.set(o.id, Object.assign(place.get(o.id) || {}, { x: Math.round(r.left), y: Math.round(r.top), w: W, h: H }));
274 if (typeof o.onResize === 'function') { try { o.onResize({ width: W, height: H }); } catch { /* the page's own */ } }
275 };
276 const saved = o.id ? place.get(o.id) : null;
277 if (saved && saved.w && saved.h) set(saved.w, saved.h, false);
278 grip.addEventListener('pointerdown', (e) => {
279 if (e.button !== undefined && e.button !== 0) return;
280 e.preventDefault();
281 const sx = e.clientX, sy = e.clientY, w0 = el.offsetWidth, h0 = el.offsetHeight;
282 el.classList.add('is-resizing');
283 try { grip.setPointerCapture(e.pointerId); } catch { /* */ }
284 const move = (ev) => set(w0 + ev.clientX - sx, h0 + ev.clientY - sy, false);
285 const up = () => {
286 grip.removeEventListener('pointermove', move); grip.removeEventListener('pointerup', up); grip.removeEventListener('pointercancel', up);
287 el.classList.remove('is-resizing');
288 set(el.offsetWidth, el.offsetHeight, true);
289 };
290 grip.addEventListener('pointermove', move); grip.addEventListener('pointerup', up); grip.addEventListener('pointercancel', up);
291 });
292 grip.addEventListener('keydown', (e) => {
293 const step = e.shiftKey ? 80 : 24;
294 const d = { ArrowRight: [step, 0], ArrowLeft: [-step, 0], ArrowDown: [0, step], ArrowUp: [0, -step] }[e.key];
295 if (e.key === 'Enter' || e.key === 'Home') { e.preventDefault(); set(first.w, first.h, true); return; }
296 if (!d) return;
297 e.preventDefault();
298 set(el.offsetWidth + d[0], el.offsetHeight + d[1], true);
299 });
300 }
301
302 document.addEventListener('keydown', (e) => {
303 if (e.key !== 'Escape' || !stack.length || e.defaultPrevented) return;
304 e.preventDefault();
305 stack[stack.length - 1].close('escape');
306 });
307
308 // The style is the person's display setting (theme-loader: saved, synced to their account, painted at boot as
309 // <html data-ov-window>); setStyle saves through it, or straight onto the page where the theme loader is absent.
310 function setStyle(name) {
311 const v = STYLES.includes(name) ? name : 'glass';
312 const T = root.OpenVibeThemeLoader;
313 if (T && T.display && typeof T.display.set === 'function') { try { T.display.set({ window: v }); } catch { /* */ } }
314 if (v === 'glass') document.documentElement.removeAttribute('data-ov-window'); else document.documentElement.setAttribute('data-ov-window', v);
315 return v;
316 }
317
318 root.OVWindow = { open, setStyle, styles: STYLES.slice(), top: () => stack[stack.length - 1] || null, closeAll: () => [...stack].reverse().forEach((w) => w.close('all')) };
319})(typeof window !== 'undefined' ? window : globalThis);
320