openvibers/openvibe.shared route-transition.js

route-transition.js

main 5.6 KB · 81 lines · javascript

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 'use strict';
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%)}}
27html[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