openvibers/openvibe.shared route-transition.js
route-transition.js
| 1 | /* |
| 2 | * openvibe-shared/route-transition.js — what a page move looks like (roadmap WS-P): web-runtime.js's rt.enter() fetches |
| 3 | * this on the first in-site move. A thin progress bar at the top of the window; after 150 ms a centred line in the |
| 4 | * page, "Loading Content…" with the current phase under it ("applying styles", "starting up", or the site's own |
| 5 | * rt.status("Loading clips…")); then the page fades in (opacity only: a transform would make the page the containing |
| 6 | * block of its position:fixed children, and a bottom sheet or a floating button would jump while it ran). Theme tokens colour it (--accent, --accent-light, |
| 7 | * --text-muted); reduced motion gets no animation. |
| 8 | * |
| 9 | * const h = OVRouteFx.start(section, { label, prefix }) → { status(text), done() }; follows <prefix>:phase events |
| 10 | * OVRouteFx.bar('run' | 'done') → the top bar alone (a site's own loads) |
| 11 | */ |
| 12 | (function (root) { |
| 13 | ; |
| 14 | if (typeof document === 'undefined' || root.OVRouteFx) return; |
| 15 | const css = `.ovrt-wait{position:fixed;z-index:40;pointer-events:none;display:grid;place-content:center;justify-items:center;gap:12px;padding:24px;color:var(--text-muted,#8a96a8);font:600 .84rem/1.35 system-ui,sans-serif;text-align:center;animation:ovrtIn .25s ease both} |
| 16 | .ovrt-wait i{display:block;width:min(200px,50vw);height:3px;border-radius:3px;overflow:hidden;position:relative;background:color-mix(in srgb,var(--accent,#3b82f6) 18%,transparent)} |
| 17 | .ovrt-wait i::after{content:'';position:absolute;inset:0;width:38%;border-radius:3px;background:var(--accent,#3b82f6);animation:ovrtSlide 1.05s cubic-bezier(.45,0,.2,1) infinite} |
| 18 | .ovrt-wait small{font-weight:500;font-size:.76rem;opacity:.85;min-height:1.2em;transition:opacity .2s} |
| 19 | .ovrt-bar{position:fixed;left:0;top:0;height:3px;width:100%;z-index:2147483000;pointer-events:none;transform-origin:0 50%;transform:scaleX(0);opacity:0;background:linear-gradient(90deg,var(--accent,#3b82f6),var(--accent-light,#60a5fa) 60%,var(--accent-2,#a78bfa));box-shadow:0 0 10px color-mix(in srgb,var(--accent,#3b82f6) 70%,transparent),0 0 3px var(--accent-light,#60a5fa);border-radius:0 3px 3px 0} |
| 20 | .ovrt-bar::after{content:'';position:absolute;right:0;top:-2px;width:90px;height:7px;border-radius:50%;background:var(--accent-light,#93c5fd);filter:blur(4px);opacity:.9} |
| 21 | .ovrt-bar.is-run{opacity:1;transform:scaleX(.88);transition:transform 3s cubic-bezier(.08,.7,.2,1),opacity .1s} |
| 22 | .ovrt-bar.is-done{opacity:0;transform:scaleX(1);transition:transform .2s ease,opacity .35s .15s} |
| 23 | .ovrt-enter{animation:ovrtEnter .28s cubic-bezier(.22,1,.36,1) both} |
| 24 | @keyframes ovrtEnter{from{opacity:0}to{opacity:1}} |
| 25 | @keyframes ovrtIn{from{opacity:0}to{opacity:1}} |
| 26 | @keyframes ovrtSlide{from{transform:translateX(-100%)}to{transform:translateX(265%)}} |
| 27 | html[data-ov-motion=off] .ovrt-bar::after{display:none}`; |
| 28 | const st = document.createElement('style'); |
| 29 | st.id = 'ovrt-css'; |
| 30 | st.textContent = css; |
| 31 | document.head.appendChild(st); |
| 32 | |
| 33 | let barEl = null, barTimer = 0; |
| 34 | function bar(state) { |
| 35 | if (!barEl) { barEl = document.createElement('div'); barEl.className = 'ovrt-bar'; barEl.setAttribute('aria-hidden', 'true'); document.body.appendChild(barEl); } |
| 36 | root.clearTimeout(barTimer); |
| 37 | if (state === 'run') { barEl.classList.remove('is-done', 'is-run'); void barEl.offsetWidth; barEl.classList.add('is-run'); } |
| 38 | else { barEl.classList.add('is-done'); barTimer = root.setTimeout(() => barEl.classList.remove('is-run', 'is-done'), 600); } |
| 39 | } |
| 40 | // Motion off (html[data-ov-motion=off], theme-loader.js) is the only setting that skips an effect; Calm plays it. |
| 41 | const reduced = () => document.documentElement.getAttribute('data-ov-motion') === 'off'; |
| 42 | |
| 43 | const PHASES = { styles: 'applying styles', code: 'starting up' }; |
| 44 | function start(section, { label, prefix = 'ov' } = {}) { |
| 45 | bar('run'); |
| 46 | let wait = null, open = true; |
| 47 | const onPhase = (e) => { if (wait && e.detail && PHASES[e.detail.phase]) wait.querySelector('small').textContent = PHASES[e.detail.phase]; }; |
| 48 | document.addEventListener(`${prefix}:phase`, onPhase); |
| 49 | const t = root.setTimeout(() => { |
| 50 | if (!open) return; |
| 51 | wait = document.createElement('div'); |
| 52 | wait.className = 'ovrt-wait'; |
| 53 | wait.setAttribute('role', 'status'); |
| 54 | wait.innerHTML = '<i></i><span></span><small></small>'; |
| 55 | wait.querySelector('span').textContent = `Loading ${label || 'page'}…`; |
| 56 | // Over the page's visible area, but outside it: a page script that re-renders its section while the |
| 57 | // route loads cannot take the line away. |
| 58 | const r = section.getBoundingClientRect(), top = Math.max(r.top, 0); |
| 59 | Object.assign(wait.style, { left: `${r.left}px`, width: `${r.width}px`, top: `${top}px`, height: `${Math.max(160, Math.min(r.bottom, root.innerHeight) - top)}px` }); |
| 60 | document.body.appendChild(wait); |
| 61 | }, 150); |
| 62 | return { |
| 63 | status(text) { if (wait) wait.querySelector('small').textContent = String(text || ''); }, |
| 64 | done() { |
| 65 | open = false; |
| 66 | root.clearTimeout(t); |
| 67 | document.removeEventListener(`${prefix}:phase`, onPhase); |
| 68 | if (wait) wait.remove(); |
| 69 | bar('done'); |
| 70 | if (reduced()) return; |
| 71 | section.classList.remove('ovrt-enter'); |
| 72 | void section.offsetWidth; |
| 73 | section.classList.add('ovrt-enter'); |
| 74 | root.setTimeout(() => section.classList.remove('ovrt-enter'), 400); |
| 75 | }, |
| 76 | }; |
| 77 | } |
| 78 | |
| 79 | root.OVRouteFx = { start, bar }; |
| 80 | })(typeof window !== 'undefined' ? window : globalThis); |
| 81 |