/* ===================================================================== Alune dialog design board — interactions Vanilla JS, no dependencies. Every visual change is a class or attribute toggle; all motion lives in CSS (transform + opacity). ===================================================================== */ (function () { 'use strict'; const root = document.documentElement; const $ = (sel, ctx = document) => ctx.querySelector(sel); const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel)); const store = { get(key) { try { return localStorage.getItem(key); } catch { return null; } }, set(key, value) { try { localStorage.setItem(key, value); } catch { /* storage unavailable (private mode, file://) */ } }, }; const reduced = () => root.dataset.reducedMotion === 'true'; const nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const visible = (el) => !el.closest('[hidden]') && el.getClientRects().length > 0; const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; const FIELDS = 'input:not([type=hidden]):not([type=checkbox]):not([type=range]), select, textarea'; const THEME_KEY = 'alune-dialogs-theme'; const MOTION_KEY = 'alune-dialogs-motion'; /* ------------------------------------------------------------------- Theme + motion preference ------------------------------------------------------------------- */ function applyTheme(theme) { root.dataset.theme = theme; $$('[data-theme-toggle]').forEach((btn) => btn.setAttribute('aria-label', theme === 'dark' ? '切换到浅色主题' : '切换到深色主题'), ); } applyTheme(root.dataset.theme === 'dark' ? 'dark' : 'light'); $$('[data-theme-toggle]').forEach((btn) => btn.addEventListener('click', () => { const next = root.dataset.theme === 'dark' ? 'light' : 'dark'; store.set(THEME_KEY, next); if (document.startViewTransition && !reduced()) { document.startViewTransition(() => applyTheme(next)); } else { applyTheme(next); } }), ); function applyMotion(isReduced) { root.dataset.reducedMotion = String(isReduced); $$('[data-motion-toggle]').forEach((btn) => btn.setAttribute('aria-pressed', String(isReduced))); } applyMotion(reduced()); $$('[data-motion-toggle]').forEach((btn) => btn.addEventListener('click', () => { const next = !reduced(); store.set(MOTION_KEY, next ? 'reduced' : 'full'); applyMotion(next); }), ); /* ------------------------------------------------------------------- Scoped state engine A [data-scope] owns a Map of group -> value. Controls write to it; [data-case] and [data-enable] read from it. ------------------------------------------------------------------- */ const states = new WeakMap(); const within = (scope, sel) => $$(sel, scope).filter((el) => el.closest('[data-scope]') === scope); const isCheckbox = (el) => el.matches('input[type=checkbox]'); const isSwitch = (el) => el.getAttribute('role') === 'switch'; const isText = (el) => el.matches('input:not([type=checkbox]):not([type=radio]), textarea'); const isContainer = (el) => !isCheckbox(el) && !isSwitch(el) && !isText(el); const usesChecked = (el) => el.getAttribute('role') === 'radiogroup' || el.classList.contains('opt-grid'); const options = (el) => $$(':scope > [data-value]', el); const onValue = (el) => el.dataset.on || 'on'; const offValue = (el) => el.dataset.off || 'off'; function classifyText(el) { const value = el.value.trim(); if (!value) return 'empty'; if (el.dataset.original !== undefined && value === el.dataset.original) return 'same'; return 'filled'; } function readControl(el) { if (isCheckbox(el)) return el.checked ? onValue(el) : offValue(el); if (isSwitch(el)) return el.getAttribute('aria-checked') === 'true' ? onValue(el) : offValue(el); if (isText(el)) return classifyText(el); const attr = usesChecked(el) ? 'aria-checked' : 'aria-pressed'; const opts = options(el); const chosen = opts.find((opt) => opt.getAttribute(attr) === 'true') || opts[0]; return chosen ? chosen.dataset.value : ''; } function syncControl(el, value) { if (value === undefined) return; if (isCheckbox(el)) { el.checked = value === onValue(el); return; } if (isSwitch(el)) { el.setAttribute('aria-checked', String(value === onValue(el))); return; } if (isText(el)) { if (classifyText(el) === value) return; if (value === 'empty') el.value = ''; else if (value === 'same' && el.dataset.original !== undefined) el.value = el.dataset.original; else if (value === 'filled' && el.dataset.sample) el.value = el.dataset.sample; return; } const attr = usesChecked(el) ? 'aria-checked' : 'aria-pressed'; const radio = el.getAttribute('role') === 'radiogroup'; const opts = options(el); opts.forEach((opt, index) => { const on = opt.dataset.value === value; opt.setAttribute(attr, String(on)); if (radio) opt.tabIndex = on ? 0 : -1; if (on && el.classList.contains('seg')) el.style.setProperty('--i', String(index)); }); if (el.classList.contains('seg')) el.style.setProperty('--n', String(opts.length)); } function matches(expr, map) { return expr .trim() .split(/\s+/) .every((clause) => { const negate = clause.startsWith('!'); const body = negate ? clause.slice(1) : clause; const at = body.indexOf(':'); const hit = at > 0 && body.slice(at + 1).split('|').includes(map.get(body.slice(0, at))); return negate ? !hit : hit; }); } function setGroup(scope, name, value, depth = 0) { const map = states.get(scope); if (!map || depth > 4) return; map.set(name, value); within(scope, `[data-group="${name}"]`).forEach((ctrl) => { if (!isContainer(ctrl)) return; const opt = options(ctrl).find((o) => o.dataset.value === value && o.dataset.also); if (opt) assign(scope, opt.dataset.also, depth + 1); }); } function assign(scope, spec, depth = 0) { spec .trim() .split(/\s+/) .forEach((pair) => { const at = pair.indexOf(':'); if (at > 0) setGroup(scope, pair.slice(0, at), pair.slice(at + 1), depth); }); } function drive(scope, el, on) { const spec = el.dataset.drive; if (!spec) return; const at = spec.indexOf(':'); const group = spec.slice(0, at); const [off, onVal] = spec.slice(at + 1).split('|'); const current = states.get(scope).get(group); if (current === off || current === onVal) setGroup(scope, group, on ? onVal : off); } function setEnabled(el, ok) { if (el.hasAttribute('data-relock')) return; if (el.matches('button, input, select, textarea, fieldset')) { el.disabled = !ok; return; } el.classList.toggle('is-disabled', !ok); if (ok) el.removeAttribute('aria-disabled'); else el.setAttribute('aria-disabled', 'true'); $$('button, input, select, textarea', el).forEach((inner) => { inner.disabled = !ok; }); } function render(scope) { const map = states.get(scope); if (!map) return; within(scope, '[data-group]').forEach((ctrl) => syncControl(ctrl, map.get(ctrl.dataset.group))); // toggleAttribute also covers SVG glyph variants, which have no `hidden` property. within(scope, '[data-case]').forEach((el) => { el.toggleAttribute('hidden', !matches(el.dataset.case, map)); }); within(scope, '[data-enable]').forEach((el) => setEnabled(el, matches(el.dataset.enable, map))); } function initScope(scope) { const map = new Map(); within(scope, '[data-group]').forEach((ctrl) => { if (!map.has(ctrl.dataset.group)) map.set(ctrl.dataset.group, readControl(ctrl)); }); states.set(scope, map); render(scope); } $$('[data-scope]').forEach((scope) => initScope(scope)); function commit(scope, ctrl, value) { setGroup(scope, ctrl.dataset.group, value); if (!isContainer(ctrl)) drive(scope, ctrl, value === onValue(ctrl)); render(scope); } document.addEventListener('click', (event) => { const opt = event.target.closest('[data-value]'); const group = opt && opt.parentElement.closest('[data-group]'); if (group && opt.parentElement === group && isContainer(group)) { const scope = group.closest('[data-scope]'); if (!scope || opt.disabled || opt.getAttribute('aria-disabled') === 'true') return; setGroup(scope, group.dataset.group, opt.dataset.value); render(scope); return; } const sw = event.target.closest('[role=switch]'); if (sw) { const on = sw.getAttribute('aria-checked') !== 'true'; sw.setAttribute('aria-checked', String(on)); const scope = sw.closest('[data-scope]'); if (sw.dataset.group && scope) commit(scope, sw, on ? onValue(sw) : offValue(sw)); } }); document.addEventListener('change', (event) => { const el = event.target; if (!isCheckbox(el) || !el.dataset.group) return; const scope = el.closest('[data-scope]'); if (scope) commit(scope, el, el.checked ? onValue(el) : offValue(el)); }); document.addEventListener('input', (event) => { const el = event.target; if (el.dataset.group && isText(el)) { const scope = el.closest('[data-scope]'); if (scope) { setGroup(scope, el.dataset.group, classifyText(el)); render(scope); } } if (el.classList.contains('range')) paintRange(el); if (el.dataset.filter !== undefined) filterMenu(el); }); // Radio groups: roving tabindex + arrow keys. document.addEventListener('keydown', (event) => { const opt = event.target.closest('[role=radiogroup][data-group] > [data-value]'); if (!opt) return; const step = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[event.key]; if (!step) return; event.preventDefault(); const opts = options(opt.parentElement).filter((o) => !o.disabled); const next = opts[(opts.indexOf(opt) + step + opts.length) % opts.length]; next.focus(); next.click(); }); /* ------------------------------------------------------------------- App skeleton behind every stage ------------------------------------------------------------------- */ const tpl = $('#tpl-app'); if (tpl) { $$('.stage:not(.is-bare):not([data-bare]), .phone-core, .mlab-stage').forEach((host) => host.prepend(tpl.content.cloneNode(true)), ); } /* ------------------------------------------------------------------- Live preview: clone an artboard dialog into a real modal layer ------------------------------------------------------------------- */ const openLayers = []; let liveCount = 0; function syncInert() { const layered = openLayers.length > 0; const blocked = layered || root.classList.contains('is-menu-open'); $$('main, .foot').forEach((el) => { el.inert = blocked; }); const island = $('.island'); if (island) island.inert = layered; root.classList.toggle('is-locked', blocked); } function uniquify(clone) { const n = ++liveCount; const ids = new Set(); [clone, ...$$('[id]', clone)].forEach((el) => { if (!el.id) return; ids.add(el.id); el.id = `${el.id}-live${n}`; }); const fix = (value) => value .split(/\s+/) .map((id) => (ids.has(id) ? `${id}-live${n}` : id)) .join(' '); ['for', 'aria-labelledby', 'aria-describedby', 'aria-controls', 'list'].forEach((attr) => $$(`[${attr}]`, clone).forEach((el) => el.setAttribute(attr, fix(el.getAttribute(attr)))), ); $$('a[href^="#"]', clone).forEach((el) => { const id = el.getAttribute('href').slice(1); if (ids.has(id)) el.setAttribute('href', `#${id}-live${n}`); }); return n; } function label(clone, n, sel, attr, prefix) { const el = $(sel, clone); if (!el || clone.hasAttribute(attr)) return; if (!el.id) el.id = `${prefix}-live${n}`; clone.setAttribute(attr, el.id); } function initialFocus(layer, clone) { const pick = (list) => list.find((el) => visible(el) && !el.disabled); const critical = clone.dataset.level === '2' || clone.dataset.tone === 'danger'; const target = pick($$('[data-autofocus]', clone)) || (critical && pick($$('.dlg-actions [data-close]', clone))) || pick($$(`.dlg-body ${FIELDS.split(', ').join(', .dlg-body ')}`, clone)) || pick($$(FOCUSABLE, clone)) || clone; target.focus({ preventScroll: true }); } async function openPreview(artboard) { const source = $('.stage .dlg', artboard); if (!source) return; const clone = source.cloneNode(true); $$('.pin, .measure', clone).forEach((el) => el.remove()); clone.classList.remove('has-pin'); const n = uniquify(clone); const critical = clone.dataset.level === '2' || clone.dataset.tone === 'danger'; clone.setAttribute('role', critical ? 'alertdialog' : 'dialog'); clone.setAttribute('aria-modal', 'true'); clone.tabIndex = -1; label(clone, n, '.dlg-title', 'aria-labelledby', 'live-title'); label(clone, n, '.dlg-desc', 'aria-describedby', 'live-desc'); const layer = document.createElement('div'); layer.className = 'mn-layer'; layer.dataset.scope = ''; const scrim = document.createElement('div'); scrim.className = 'mn-scrim'; scrim.dataset.close = ''; layer.append(scrim, clone); states.set(layer, new Map(states.get(artboard) || [])); if (artboard.dataset.live) assign(layer, artboard.dataset.live); render(layer); document.body.append(layer); $$('.range', layer).forEach(paintRange); openLayers.push({ layer, trigger: document.activeElement }); syncInert(); await nextFrame(); layer.classList.add('is-open'); initialFocus(layer, clone); } function close(layer, confirmed) { const index = openLayers.findIndex((entry) => entry.layer === layer); if (index < 0) return; const [{ trigger }] = openLayers.splice(index, 1); if (confirmed) layer.classList.add('is-confirmed'); layer.classList.add('is-closing'); setTimeout( () => { layer.remove(); syncInert(); if (trigger && document.contains(trigger)) trigger.focus({ preventScroll: true }); }, reduced() ? 140 : 320, ); } document.addEventListener('click', (event) => { const trigger = event.target.closest('[data-preview]'); if (!trigger) return; const artboard = trigger.dataset.preview ? $(trigger.dataset.preview) : trigger.closest('[data-scope]'); if (artboard) void openPreview(artboard); }); document.addEventListener( 'keydown', (event) => { const top = openLayers[openLayers.length - 1]; if (!top) return; const { layer } = top; if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); if (!layer.dataset.busy) close(layer, false); return; } const submit = event.metaKey || event.ctrlKey || event.target.matches('input:not([type=checkbox])'); if (event.key === 'Enter' && submit && !event.isComposing) { const dlg = $('.dlg', layer); if (!dlg || dlg.dataset.level === '2') return; const buttons = $$('.dlg-actions .btn:not([data-close])', layer).filter( (btn) => visible(btn) && !btn.disabled && btn.getAttribute('aria-disabled') !== 'true', ); const primary = buttons[buttons.length - 1]; if (primary) { event.preventDefault(); primary.click(); } return; } if (event.key === 'Tab') { const items = $$(FOCUSABLE, layer).filter(visible); if (!items.length) { event.preventDefault(); return; } const first = items[0]; const last = items[items.length - 1]; const active = document.activeElement; const outside = !layer.contains(active) || active === $('.dlg', layer); if (event.shiftKey && (active === first || outside)) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && (active === last || outside)) { event.preventDefault(); first.focus(); } } }, true, ); /* ------------------------------------------------------------------- Actions: busy choreography, then close or advance state ------------------------------------------------------------------- */ const ACTION = '.dlg-actions .btn:not([data-close]), .pc-actions .btn:not([data-close]), [data-then]'; const PASSIVE = '[data-copy], [data-preview], [data-replay], [data-mlab], [data-eye]'; function busyLabel(btn) { return Array.from(btn.children).find( (el) => el.tagName === 'SPAN' && !el.className && !el.hidden, ); } async function runAction(btn) { const layer = btn.closest('.mn-layer'); const inline = Boolean(layer && layer.classList.contains('is-inline')); const scope = btn.closest('[data-scope]'); const container = btn.closest('.dlg, .pop') || scope || document.body; const text = busyLabel(btn); const orb = $('.btn-orb', btn); const saved = { text: text ? text.textContent : null, orb: orb ? orb.innerHTML : null }; let spinner = null; btn.classList.add('is-busy'); btn.setAttribute('aria-busy', 'true'); if (text && btn.dataset.busyLabel) text.textContent = btn.dataset.busyLabel; if (orb) { orb.innerHTML = ''; } else { spinner = document.createElement('span'); spinner.className = 'moonload'; spinner.setAttribute('aria-hidden', 'true'); btn.prepend(spinner); } const locked = $$('.dlg-actions .btn, .pc-actions .btn, .dlg-close', container).filter( (el) => el !== btn && !el.disabled, ); locked.forEach((el) => { el.disabled = true; el.dataset.relock = ''; }); if (layer && !inline) layer.dataset.busy = 'true'; if (scope && btn.dataset.during) { assign(scope, btn.dataset.during); render(scope); } await wait(reduced() ? 500 : 900); const restore = () => { btn.classList.remove('is-busy'); btn.removeAttribute('aria-busy'); if (text && saved.text !== null) text.textContent = saved.text; if (orb) orb.innerHTML = saved.orb; if (spinner) spinner.remove(); locked.forEach((el) => { el.disabled = false; delete el.dataset.relock; }); if (layer) delete layer.dataset.busy; if (scope) render(scope); }; if (btn.dataset.then) { restore(); if (scope) { assign(scope, btn.dataset.then); render(scope); } return; } if (inline) { restore(); mlabPlay('confirm'); return; } if (layer) { close(layer, true); return; } await wait(reduced() ? 160 : 520); restore(); } document.addEventListener('click', (event) => { const btn = event.target.closest(ACTION); if (!btn || btn.matches(PASSIVE)) return; if (btn.disabled || btn.getAttribute('aria-disabled') === 'true' || btn.classList.contains('is-busy')) return; event.preventDefault(); void runAction(btn); }); document.addEventListener('click', (event) => { const closer = event.target.closest('[data-close]'); if (!closer || closer.disabled) return; const layer = closer.closest('.mn-layer'); if (!layer) return; if (layer.classList.contains('is-inline')) { mlabPlay('exit'); return; } if (!layer.dataset.busy) close(layer, false); }); /* ------------------------------------------------------------------- Small component behaviours ------------------------------------------------------------------- */ document.addEventListener('click', (event) => { const eye = event.target.closest('[data-eye]'); if (eye) { const input = $('input', eye.closest('.inp') || eye.parentElement); if (!input) return; const show = input.type === 'password'; input.type = show ? 'text' : 'password'; const use = $('use', eye); if (use) use.setAttribute('href', show ? '#i-eye-off' : '#i-eye'); eye.setAttribute('aria-label', `${show ? '隐藏' : '显示'}${eye.dataset.eye}`); eye.setAttribute('aria-pressed', String(show)); return; } const copy = event.target.closest('[data-copy]'); if (copy) { void copyText(copy); return; } const chip = event.target.closest('.chip-btn[data-prefix]'); if (chip) { const chips = $$('.chip-btn[data-prefix]', chip.parentElement); chips.forEach((c) => c.setAttribute('aria-pressed', String(c === chip))); const input = $('input', chip.closest('.fld')); if (input) { let rest = input.value; chips.forEach((c) => { if (rest.startsWith(c.dataset.prefix)) rest = rest.slice(c.dataset.prefix.length); }); input.value = chip.dataset.prefix + rest; input.dispatchEvent(new Event('input', { bubbles: true })); } return; } const replay = event.target.closest('[data-replay]'); if (replay) replayPops(replay.closest('.ab, .pc-card')); }); async function copyText(btn) { const host = btn.closest('.dlg') || document; const source = $(btn.dataset.copy, host); const status = $('[data-copy-status]', host); const text = busyLabel(btn); const use = $('use', btn); try { if (!source || !navigator.clipboard) throw new Error('clipboard unavailable'); await navigator.clipboard.writeText(source.innerText.trim()); if (status) status.hidden = true; const before = { text: text && text.textContent, icon: use && use.getAttribute('href') }; btn.classList.add('fb-copied'); if (text) text.textContent = '已复制'; if (use) use.setAttribute('href', '#i-check'); setTimeout(() => { btn.classList.remove('fb-copied'); if (text) text.textContent = before.text; if (use) use.setAttribute('href', before.icon); }, 1600); } catch { if (status) { status.textContent = '无法复制,请选择提示文本手动复制。'; status.hidden = false; } } } function paintRange(input) { const min = Number(input.min || 0); const max = Number(input.max || 100); const pct = ((Number(input.value) - min) / (max - min)) * 100; input.style.setProperty('--fill', `${pct}%`); const out = $('output', input.closest('.fld') || input.parentElement); if (out) out.textContent = `${input.value} px`; } $$('.range').forEach(paintRange); function filterMenu(input) { const host = input.closest('.pop') || input.closest('[data-scope]') || document; const query = input.value.trim().toLowerCase(); let any = false; $$('.menu-item[data-name]', host).forEach((item) => { const hit = !query || item.dataset.name.toLowerCase().includes(query); item.hidden = !hit; any = any || hit; }); $$('[role=group]', host).forEach((group) => { const items = $$('.menu-item[data-name]', group); if (items.length) group.hidden = items.every((item) => item.hidden); }); const empty = $('[data-empty]', host); if (empty) empty.hidden = any; } function replayPops(host) { if (!host) return; $$('.pop.is-replay', host).forEach((pop) => { pop.classList.remove('is-replay'); void pop.offsetWidth; pop.classList.add('is-replay'); }); } // Anatomy toggles: redlines / pins. function syncToggle(box) { const target = $(box.dataset.target); if (!target) return; if (box.dataset.toggleOn) target.classList.toggle(box.dataset.toggleOn, box.checked); if (box.dataset.toggleOff) target.classList.toggle(box.dataset.toggleOff, !box.checked); } $$('input[type=checkbox][data-target]').forEach((box) => { syncToggle(box); box.addEventListener('change', () => syncToggle(box)); }); /* ------------------------------------------------------------------- Scroll reveals (IntersectionObserver only) ------------------------------------------------------------------- */ const reveals = $$('.reveal'); const byDocument = (a, b) => a.target.compareDocumentPosition(b.target) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1; if ('IntersectionObserver' in window) { const io = new IntersectionObserver( (entries) => { entries .filter((entry) => entry.isIntersecting) .sort(byDocument) .forEach((entry, index) => { const el = entry.target; const delay = Math.min(index, 6) * 80; el.style.setProperty('--d', `${delay}ms`); el.classList.add('is-in'); io.unobserve(el); setTimeout(() => el.style.removeProperty('--d'), delay + 1300); if (el.querySelector('.pop.is-replay')) setTimeout(() => replayPops(el), delay + 160); if (el.classList.contains('mlab') || el.querySelector('.mlab-stage')) { setTimeout(() => mlabPlay('enter'), delay + 240); } }); }, { threshold: 0, rootMargin: '0px 0px -12% 0px' }, ); reveals.forEach((el) => io.observe(el)); } else { reveals.forEach((el) => el.classList.add('is-in')); } /* ------------------------------------------------------------------- Island nav: active section + hamburger overlay ------------------------------------------------------------------- */ const navLinks = new Map( $$('.island-links a[href^="#"]').map((a) => [a.getAttribute('href').slice(1), a]), ); if ('IntersectionObserver' in window && navLinks.size) { const navIO = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; navLinks.forEach((a, id) => { if (id === entry.target.id) a.setAttribute('aria-current', 'true'); else a.removeAttribute('aria-current'); }); }); }, { rootMargin: '-45% 0px -50% 0px' }, ); $$('main > section[id]').forEach((section) => navIO.observe(section)); } const burger = $('.burger'); const menu = $('#menu'); function setMenu(open) { if (!burger || !menu) return; root.classList.toggle('is-menu-open', open); burger.setAttribute('aria-expanded', String(open)); burger.setAttribute('aria-label', open ? '关闭目录' : '打开目录'); menu.inert = !open; syncInert(); if (open) { const first = $('a', menu); if (first) setTimeout(() => first.focus({ preventScroll: true }), 80); } else if (menu.contains(document.activeElement)) { burger.focus({ preventScroll: true }); } } if (burger && menu) { menu.inert = true; burger.addEventListener('click', () => setMenu(!root.classList.contains('is-menu-open'))); menu.addEventListener('click', (event) => { if (event.target.closest('a')) setMenu(false); }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !openLayers.length && root.classList.contains('is-menu-open')) { setMenu(false); } }); } /* ------------------------------------------------------------------- Motion lab: replay the modal choreography inline ------------------------------------------------------------------- */ const lab = $('.mlab-stage .mn-layer.is-inline[data-scope]'); let labTimer = 0; async function mlabEnter(slow) { lab.classList.remove('is-open', 'is-closing', 'is-confirmed'); void lab.offsetWidth; await nextFrame(); lab.classList.add('is-open'); if (slow) { const dlg = $('.dlg', lab); if (dlg) void getComputedStyle(dlg).transform; if (lab.getAnimations) { lab.getAnimations({ subtree: true }).forEach((anim) => { anim.playbackRate = 0.25; }); } } } function mlabPlay(mode) { if (!lab) return; clearTimeout(labTimer); if (mode === 'enter' || mode === 'slow') { void mlabEnter(mode === 'slow'); return; } if (mode === 'confirm') lab.classList.add('is-confirmed'); lab.classList.add('is-closing'); labTimer = setTimeout(() => void mlabEnter(false), reduced() ? 900 : 1600); } if (lab) { lab.classList.add('is-open'); $$('[data-mlab]').forEach((btn) => btn.addEventListener('click', () => mlabPlay(btn.dataset.mlab))); } })();