openvibers/openvibe.tools Commits

Motion: Calm reduces, only Off removes (Shared 4.184.0) (#273)

OP OpenVibers committed 3 days ago · bcd7750480 · 15 files, +15 −21

Calm (the Motion setting, or a device that asks for less motion) used to stop
every effect here. Pin openvibe-shared 4.183.0 and drop the rules that stopped an
effect under prefers-reduced-motion or html[data-ov-motion=reduced]: Calm plays
every effect, smaller and slower, and only html[data-ov-motion=off] stops it.

Changes

ddiff --git a/apps/audio/public/css/style.css b/apps/audio/public/css/style.css
iindex 281377f..3400ec9 100644
---- a/apps/audio/public/css/style.css
++++ b/apps/audio/public/css/style.css
@@@ -591,4 +591,4 @@ a:hover { color: var(--accent-hover); }
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
--@media (prefers-reduced-motion: reduce) { .option-group, .result-panel, .toast { animation: none; } .spinner { animation: none; opacity: .7; } }
++html[data-ov-motion=off] .spinner{opacity: .7}
ddiff --git a/apps/docs/public/css/style.css b/apps/docs/public/css/style.css
iindex 549f09b..d162dd2 100644
---- a/apps/docs/public/css/style.css
++++ b/apps/docs/public/css/style.css
@@@ -481,4 +481,4 @@ body {
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
--@media (prefers-reduced-motion: reduce) { .spinner { animation: none; opacity: .7; } }
++html[data-ov-motion=off] .spinner{opacity: .7}
ddiff --git a/apps/food/public/css/style.css b/apps/food/public/css/style.css
iindex aa6cfbc..0eefac0 100644
---- a/apps/food/public/css/style.css
++++ b/apps/food/public/css/style.css
@@@ -247,4 +247,4 @@ body { min-height:100%; font-family:var(--font); background:var(--bg); color:var
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
--@media (prefers-reduced-motion: reduce) { .spinner { animation: none; opacity: .7; } }
++html[data-ov-motion=off] .spinner{opacity: .7}
ddiff --git a/apps/gateway/public/audio.html b/apps/gateway/public/audio.html
iindex 1c20ed6..c7a4257 100644
---- a/apps/gateway/public/audio.html
++++ b/apps/gateway/public/audio.html
@@@ -235,6 +235,5 @@
});
</script>
<script src="https://openvibe.live/live-notify.js" async></script>
-- <style>@media (prefers-reduced-motion: reduce) { .showcase-tool, .example-btn, .stat-card { animation: none; } }</style>
</body>
</html>
ddiff --git a/apps/gateway/public/codes.html b/apps/gateway/public/codes.html
iindex 27b305b..01bda3a 100644
---- a/apps/gateway/public/codes.html
++++ b/apps/gateway/public/codes.html
@@@ -141,7 +141,6 @@ select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,va
.facts span{display:block;font-size:.7rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--c-tx3)}
.dl{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}
@media (max-width:900px){.studio{grid-template-columns:minmax(0,1fr)}.preview{position:static;order:-1}.stage{min-height:220px}.stage .art,.stage .art svg{max-height:260px}}
--@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
/* ── Hub ── */
.smart{padding:18px;border-radius:calc(var(--r) + 4px);background:linear-gradient(135deg,color-mix(in srgb,var(--c-ac) 14%,var(--c-card)),var(--c-card));border:1px solid var(--c-brd2);display:grid;grid-template-columns:minmax(0,1fr) 220px;gap:18px;align-items:center;margin-bottom:22px}
ddiff --git a/apps/gateway/public/dev.html b/apps/gateway/public/dev.html
iindex d3d0d37..db26045 100644
---- a/apps/gateway/public/dev.html
++++ b/apps/gateway/public/dev.html
@@@ -549,6 +549,5 @@ function sample(){const t=T[ACT];if(t?.p){$('in1').value=t.p;if(t.p2&&$('in2'))$
function toast(msg){const t=$('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2000)}
</script>
<script src="https://openvibe.live/live-notify.js" async></script>
-- <style>@media (prefers-reduced-motion: reduce) { .bg-glow::before, .bg-glow::after { animation: none; } }</style>
</body>
</html>
ddiff --git a/apps/gateway/public/img.html b/apps/gateway/public/img.html
iindex 29832bf..d777551 100644
---- a/apps/gateway/public/img.html
++++ b/apps/gateway/public/img.html
@@@ -235,6 +235,5 @@
});
</script>
<script src="https://openvibe.live/live-notify.js" async></script>
-- <style>@media (prefers-reduced-motion: reduce) { .showcase-tool, .example-btn, .stat-card { animation: none; } }</style>
</body>
</html>
ddiff --git a/apps/gateway/public/js/site.js b/apps/gateway/public/js/site.js
iindex e7c1607..328303d 100644
---- a/apps/gateway/public/js/site.js
++++ b/apps/gateway/public/js/site.js
@@@ -172,12 +172,12 @@
})();
// The headline's phrases are links already in the page; this shows them in turn, once through, then rests on the
--// first. The button pauses, or plays it again. Reduced motion keeps the first. Never an aria-live region.
++// first. The button pauses, or plays it again. Motion off keeps the first; Calm rotates as usual. Never an aria-live region.
(function () {
const el = document.getElementById('tool-headline');
const items = el ? Array.from(el.querySelectorAll('.hm-ph')) : [];
if (items.length < 2) return;
-- const motion = window.matchMedia('(prefers-reduced-motion: reduce)');
++ const still = () => document.documentElement.getAttribute('data-ov-motion') === 'off';
const control = document.getElementById('headline-motion');
let i = 0, timer = 0, paused = false, rested = false;
const show = (n) => {
@@@ -190,8 +190,8 @@
};
const update = () => {
clearInterval(timer); timer = 0;
-- if (control) control.hidden = motion.matches;
-- if (motion.matches) { show(0); return; }
++ if (control) control.hidden = still();
++ if (still()) { show(0); return; }
if (!paused && !rested && !document.hidden) timer = setInterval(() => {
if (el.matches(':hover') || el.contains(document.activeElement)) return;
const next = (i + 1) % items.length;
@@@ -203,7 +203,7 @@
control.setAttribute('aria-label', say); control.title = say; control.classList.toggle('is-paused', paused || rested);
};
if (control) control.addEventListener('click', () => { if (rested) rested = paused = false; else paused = !paused; update(); });
-- motion.addEventListener('change', update);
++ window.addEventListener('ov:display', update);
document.addEventListener('visibilitychange', update);
update();
})();
ddiff --git a/apps/gateway/public/net.html b/apps/gateway/public/net.html
iindex 18f3d0c..f8c68c7 100644
---- a/apps/gateway/public/net.html
++++ b/apps/gateway/public/net.html
@@@ -1428,6 +1428,5 @@
}
</script>
<script src="https://openvibe.live/live-notify.js" async></script>
-- <style>@media (prefers-reduced-motion: reduce) { .bg-glow::before, .bg-glow::after, .toast, .showcase-tool, .stat-card { animation: none; } }</style>
</body>
</html>
ddiff --git a/apps/gateway/server/pages/site.js b/apps/gateway/server/pages/site.js
iindex dad8f76..4097b83 100644
---- a/apps/gateway/server/pages/site.js
++++ b/apps/gateway/server/pages/site.js
@@@ -127,7 +127,7 @@ details{border-bottom:1px solid var(--border,rgba(255,255,255,.08));padding:12px
transition:opacity var(--ov-dur-3,.32s) var(--ov-ease-out,ease-out),transform var(--ov-dur-3,.32s) var(--ov-ease-out,ease-out),visibility 0s linear var(--ov-dur-3,.32s)}
.hm-ph.is-on{opacity:1;visibility:visible;transform:none;transition-delay:0s}.hm-ph.is-out{transform:translateY(-.35em)}
.hm-ph:hover,.hm-ph:focus-visible{text-decoration:underline;text-decoration-thickness:.06em;text-underline-offset:.12em}
--@media (prefers-reduced-motion:reduce){.hm-ph{transition:none;transform:none}}
++html[data-ov-motion=off] .hm-ph{transform:none}
/* The strip of tools drifts, so its ends fade: a chip cut by a hard edge looked broken. */
.quick-strip{-webkit-mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 22px),transparent);mask-image:linear-gradient(90deg,transparent,#000 22px,#000 calc(100% - 22px),transparent);padding-inline:14px;margin-inline:-14px;max-width:calc(100% + 28px)}
/* The headline's pause: a small round icon beside the words, not a line of its own. */
@@@ -222,7 +222,6 @@ background:color-mix(in srgb,var(--text-primary,#e8ecf3) 8%,transparent);color:v
/* The home on phones (2026-10-29): the examples are one row that scrolls sideways (all four still there), and four of the most
opened tools show (the families below and the A to Z list hold the rest). */
@media (max-width:599px){.start-grid{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-inline:16px;scrollbar-width:none;gap:10px;margin:0 -16px;padding:0 16px 4px}.start-grid::-webkit-scrollbar{display:none}.start-grid>*{flex:0 0 84%;min-width:0;scroll-snap-align:start}section[aria-labelledby="top-h"] .grid>:nth-child(n+5){display:none}}
--@media (prefers-reduced-motion:reduce){.tool{transition:none}}
${icons.CSS}
/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */
@media (pointer: coarse) {
ddiff --git a/apps/gateway/test/home-headline.test.js b/apps/gateway/test/home-headline.test.js
iindex bc6f2f9..af840dd 100644
---- a/apps/gateway/test/home-headline.test.js
++++ b/apps/gateway/test/home-headline.test.js
@@@ -32,10 +32,11 @@ const site = require('../server/pages/site');
assert.strictEqual(new Set(links.map((m) => m[2])).size, links.length, 'each phrase is a different tool');
assert.ok(!home.includes('data-phrases='), 'the phrases are links in the page, not a data attribute');
-- // The swap is CSS on shared timing, still under reduced motion; the script goes once through, then rests.
++ // The swap is CSS on shared timing, played under Calm and held only with motion off; the script goes once through, then rests.
assert.match(home, /\.hm-rot\{display:grid\}\.hm-ph\{grid-area:1\/1;/, 'the phrases share one cell, so nothing below moves');
-- assert.match(home, /@media \(prefers-reduced-motion:reduce\)\{\.hm-ph\{transition:none;transform:none\}\}/);
++ assert.match(home, /html\[data-ov-motion=off\] \.hm-ph\{transform:none\}/);
const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'site.js'), 'utf8');
++ assert.ok(js.includes("getAttribute('data-ov-motion') === 'off'"), 'only motion off holds the first phrase');
assert.ok(js.includes('if (next === 0) { rested = true;'), 'once through, then it rests');
assert.ok(js.includes("el.matches(':hover') || el.contains(document.activeElement)"), 'it never changes under a pointer or the focus');
console.log(`home-headline: ${links.length} phrases, each a link to its tool; one shows; once through, then still`);
ddiff --git a/apps/img/public/css/style.css b/apps/img/public/css/style.css
iindex 4554c31..ff6963d 100644
---- a/apps/img/public/css/style.css
++++ b/apps/img/public/css/style.css
@@@ -484,4 +484,4 @@ body {
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
--@media (prefers-reduced-motion: reduce) { .spinner { animation: none; opacity: .7; } }
++html[data-ov-motion=off] .spinner{opacity: .7}
ddiff --git a/apps/maps/public/css/style.css b/apps/maps/public/css/style.css
iindex cad813b..95a7ccd 100644
---- a/apps/maps/public/css/style.css
++++ b/apps/maps/public/css/style.css
@@@ -574,4 +574,4 @@ html, body { height: auto; min-height: 100%; overflow: auto; }
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
#app { padding-top: calc(var(--ovk-nav-h, 52px) + 8px); }
.ovk-page { padding-top: 12px; }
--@media (prefers-reduced-motion: reduce) { .splash-content, .splash-icon, #splash-screen.fade-out, .welcome-icon, .user-marker, #detail-panel, .modal-content, .sp-item:not(.done):not(.error) { animation: none; } .search-progress .spinner { animation: none; opacity: .7; } }
++html[data-ov-motion=off] .search-progress .spinner{opacity: .7}
ddiff --git a/apps/text/public/css/shared.css b/apps/text/public/css/shared.css
iindex ac542f4..bf8c7c6 100644
---- a/apps/text/public/css/shared.css
++++ b/apps/text/public/css/shared.css
@@@ -912,4 +912,3 @@ textarea.large {
/* The page is the shared tool frame: its head starts under the navbar, not behind it. */
.ovk-tool-head { margin-top: calc(var(--ovk-nav-h, 52px) + 12px); }
--@media (prefers-reduced-motion: reduce) { .live-dot, .output-flash, .toast { animation: none; } }
ddiff --git a/apps/yt/public/css/style.css b/apps/yt/public/css/style.css
iindex c7bf903..828b9a8 100644
---- a/apps/yt/public/css/style.css
++++ b/apps/yt/public/css/style.css
@@@ -422,4 +422,4 @@ a:hover { text-decoration: underline; }
/* The page is the shared tool frame: it starts under the navbar, not behind it. */
.ovk-page { padding-top: calc(var(--ovk-nav-h, 52px) + 12px); }
--@media (prefers-reduced-motion: reduce) { .spinner, .progress-bar.indeterminate { animation: none; opacity: .7; } }
++html[data-ov-motion=off] .spinner,html[data-ov-motion=off] .progress-bar.indeterminate{opacity: .7}