openvibers/openvibe.shared Commits

The seasonal fall lands on the bar, never over a page's heading (4.50.1) (#196)

OP OpenVibers committed 14 days ago · 67605d1afb · 6 files, +67 −10

Once a day the mark's leaves fell 120-280 px from the brand, across a page's
heading on phones (Codes' conductor). They now drift to the bottom edge of
the bar the mark sits in, settle there and fade, inside the screen; a mark
outside a bar gets a short fall. The test covers the landing, the short
fall, once a day and no fall without a season.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 07d368c..e93f0fd 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -6,6 +6,14 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
++## 4.50.1 — 2026-10-30
++
++- **The seasonal fall lands on the bar.** Once a day the mark's leaves (snow in winter) fell 120-280 px from the brand,
++ over a page's heading on phones (Codes' conductor, 10-30). They now drift down to the bottom edge of the bar the mark
++ sits in (`.openvibe-navbar`, `header` or `nav`), settle there and fade, inside the screen's width; a mark outside a
++ bar gets a short fall of 36-64 px. `test/ov-mark.test.js` covers the landing, the short fall, once a day and no fall
++ without a season.
++
## 4.50.0 — 2026-10-30
- **Rich embeds in the agent session.** The owner, 2026-10-30: "When it generates or reads or analyzed images or visuals
ddiff --git a/README.md b/README.md
iindex cb11966..93ac50b 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.50.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.50.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex 299b9a9..00b295a 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.50.0",
++ "version": "4.50.1",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
ddiff --git a/ov-mark.js b/ov-mark.js
iindex 3a2dcd5..6d73dc7 100644
---- a/ov-mark.js
++++ b/ov-mark.js
@@@ -93,7 +93,7 @@ html[data-ov-season-mark=winter] .ovm-fall i{width:6px;height:6px;border-radius:
@keyframes ovmLeaf{from{transform:scale(0) rotate(-50deg)}to{transform:none}}
@keyframes ovmWiggle{0%,100%{transform:none}25%{transform:rotate(-18deg)}60%{transform:rotate(12deg)}}
@keyframes ovmTwinkle{0%,100%{opacity:1}50%{opacity:.3}}
--@keyframes ovmFall{0%{opacity:0;transform:none}12%{opacity:1}80%{opacity:.9}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}
++@keyframes ovmFall{0%{opacity:0;transform:none}12%{opacity:1}72%{opacity:1;transform:translate(var(--x),var(--y)) rotate(var(--r))}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}
@media (prefers-reduced-motion:reduce){.ov-mark svg,.ov-mark .g,.ov-mark .lp,.ov-mark .sw,.ov-mark .d,.ov-mark .sl,.ov-mark .sd{animation:none!important}.ov-mark .sw,.ovm-fall{display:none}}
html[data-ov-motion=reduced] .ov-mark .sw{display:none}`;
// The open loop: from the opening's left end round the bottom to its right end, then down the V and back up.
@@@ -177,14 +177,22 @@ html[data-ov-motion=reduced] .ov-mark .sw{display:none}`;
function fallOnce(el) {
const id = season(); if (!id || reduced() || document.hidden || !el.isConnected) return;
const day = id + ':' + new Date().toDateString();
-- try { if (localStorage.getItem(FALL_KEY) === day) return; localStorage.setItem(FALL_KEY, day); } catch { return; }
++ try { const ls = window.localStorage; if (ls.getItem(FALL_KEY) === day) return; ls.setItem(FALL_KEY, day); } catch { return; }
const r = el.getBoundingClientRect(); if (!r.width || !document.body) return;
const box = document.createElement('div'); box.className = 'ovm-fall'; box.setAttribute('aria-hidden', 'true');
-- box.style.left = (r.left + r.width / 2) + 'px'; box.style.top = (r.top + r.height * 0.6) + 'px';
++ const x0 = r.left + r.width / 2, y0 = r.top + r.height * 0.6;
++ box.style.left = x0 + 'px'; box.style.top = y0 + 'px';
++ // The leaves land on the bottom edge of the bar the mark sits in and fade there, so they never cross a page's
++ // heading (they fell 120-280 px over it on phones, 10-30). A mark outside a bar gets a short fall.
++ const bar = el.closest('.openvibe-navbar,header,nav');
++ const rb = bar && bar.getBoundingClientRect();
++ const floor = rb && rb.bottom > y0 + 8 ? rb.bottom - y0 - 4 : 0;
const colors = id === 'winter' ? ['#f4fbff', '#d8f3ff', '#ffffff'] : ['#ff7a1a', '#f5a524', '#d9481f', '#7fd04e'];
for (let i = 0; i < 7; i++) {
const s = document.createElement('i');
-- s.style.cssText = `--x:${Math.round((Math.random() - 0.3) * 140)}px;--y:${Math.round(120 + Math.random() * 160)}px;--r:${Math.round(Math.random() * 540 - 270)}deg;animation-delay:${(i * 0.18 + Math.random() * 0.2).toFixed(2)}s;animation-duration:${(2.4 + Math.random() * 1.2).toFixed(2)}s;background:${colors[i % colors.length]}`;
++ const dx = Math.max(8 - x0, Math.min((window.innerWidth || 0) - 8 - x0, Math.round((Math.random() - 0.3) * 200)));
++ const dy = floor ? Math.round(floor - Math.random() * 3) : Math.round(36 + Math.random() * 28);
++ s.style.cssText = `--x:${dx}px;--y:${dy}px;--r:${Math.round(Math.random() * 540 - 270)}deg;animation-delay:${(i * 0.18 + Math.random() * 0.2).toFixed(2)}s;animation-duration:${(2.4 + Math.random() * 1.2).toFixed(2)}s;background:${colors[i % colors.length]}`;
box.appendChild(s);
}
document.body.appendChild(box);
ddiff --git a/package.json b/package.json
iindex 834f71c..a31c232 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.50.0",
++ "version": "4.50.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/ov-mark.test.js b/test/ov-mark.test.js
iindex f2c7161..89aa5c8 100644
---- a/test/ov-mark.test.js
++++ b/test/ov-mark.test.js
@@@ -15,8 +15,8 @@ function memoryStorage() {
return { getItem: (k) => (m.has(k) ? m.get(k) : null), setItem: (k, v) => { m.set(k, String(v)); }, removeItem: (k) => { m.delete(k); } };
}
--function setup({ reduce = false, session = memoryStorage(), start = 1000000, html = '<a href="/" class="brand"><span class="ov-mark" data-size="28"></span></a><span class="ov-mark" id="foot" data-static="1"></span>' } = {}) {
-- const { window, document, MutationObserver, Event } = parseHTML(`<!doctype html><html><head></head><body>${html}</body></html>`);
++function setup({ reduce = false, session = memoryStorage(), start = 1000000, rootAttrs = '', html = '<a href="/" class="brand"><span class="ov-mark" data-size="28"></span></a><span class="ov-mark" id="foot" data-static="1"></span>' } = {}) {
++ const { window, document, MutationObserver, Event } = parseHTML(`<!doctype html><html${rootAttrs}><head></head><body>${html}</body></html>`);
let now = start, timers = [];
const clock = {
now: () => now,
@@@ -29,7 +29,7 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, htm
}
},
};
-- const FakeDate = { now: () => now };
++ const FakeDate = class extends Date { constructor(...a) { super(...(a.length ? a : [now])); } static now() { return now; } };
const setT = (fn, ms) => { const t = { fn, at: now + ms }; timers.push(t); return t; };
const clearT = (t) => { timers = timers.filter((x) => x !== t); };
const observers = [];
@@@ -51,6 +51,14 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, htm
proto.unpauseAnimations = function () { const s = smil.get(this) || { t: 0 }; s.paused = false; smil.set(this, s); };
proto.getCurrentTime = function () { return (smil.get(this) || { t: 0 }).t; };
proto.setCurrentTime = function (t) { const s = smil.get(this) || {}; s.t = t; smil.set(this, s); };
++ if (rootAttrs) {
++ // The seasonal fall measures the page: a phone's width, the mark in the bar and the bar's bottom edge.
++ window.innerWidth = 390;
++ window.localStorage = session.local || memoryStorage();
++ const rect = (r) => () => ({ left: r[0], top: r[1], width: r[2], height: r[3], right: r[0] + r[2], bottom: r[1] + r[3] });
++ for (const m of document.querySelectorAll('.ov-mark')) m.getBoundingClientRect = rect([20, 12, 28, 28]);
++ for (const b of document.querySelectorAll('.openvibe-navbar')) b.getBoundingClientRect = rect([0, 0, 390, 53]);
++ }
const run = new Function('window', 'document', 'IntersectionObserver', 'MutationObserver', 'setTimeout', 'clearTimeout', 'Date', 'sessionStorage', SRC);
run(window, document, IO, MutationObserver, setT, clearT, FakeDate, session);
const io = observers[0];
@@@ -79,6 +87,39 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, htm
assert.strictEqual(t.smilTime(), 1.6);
}
++ // ── the seasonal fall lands on the bar's bottom edge, never over the page's heading (4.50.1) ─────────
++ {
++ const leaves = (t) => [...t.document.querySelectorAll('.ovm-fall i')].map((i) => {
++ const st = i.getAttribute('style') || '';
++ return { x: parseFloat((/--x:\s*(-?[\d.]+)px/.exec(st) || [])[1]), y: parseFloat((/--y:\s*(-?[\d.]+)px/.exec(st) || [])[1]) };
++ });
++ const session = memoryStorage(); session.local = memoryStorage();
++ const t = setup({ session, rootAttrs: ' data-ov-season-mark="halloween"', html: '<div class="openvibe-navbar"><a href="/" class="brand"><span class="ov-mark" data-size="28"></span></a></div><main><h1>Conductor</h1></main>' });
++ t.io.fire(t.mark, true);
++ t.clock.advance(1000);
++ const inBar = leaves(t);
++ assert.strictEqual(inBar.length, 7, 'seven leaves fall');
++ const y0 = 12 + 28 * 0.6, edge = 53 - y0;
++ for (const l of inBar) {
++ assert.ok(l.y <= edge - 4 && l.y >= edge - 8, `a leaf lands on the bar's edge (${l.y}px from the mark, the edge at ${edge.toFixed(1)}px)`);
++ assert.ok(34 + l.x >= 8 && 34 + l.x <= 390 - 8, 'and on the screen');
++ }
++ const again = setup({ session, start: 1000000 + 60000, rootAttrs: ' data-ov-season-mark="halloween"', html: '<div class="openvibe-navbar"><a href="/" class="brand"><span class="ov-mark" data-size="28"></span></a></div>' });
++ again.io.fire(again.mark, true); again.clock.advance(1000);
++ assert.strictEqual(leaves(again).length, 0, 'once a day');
++
++ const loose = memoryStorage(); loose.local = memoryStorage();
++ const f = setup({ session: loose, rootAttrs: ' data-ov-season-mark="winter"', html: '<div class="foot"><a href="/" class="brand"><span class="ov-mark" data-size="28"></span></a></div>' });
++ f.io.fire(f.mark, true); f.clock.advance(1000);
++ const out = leaves(f);
++ assert.strictEqual(out.length, 7, 'a mark outside a bar still snows');
++ for (const l of out) assert.ok(l.y >= 36 && l.y <= 64, `a short fall (${l.y}px)`);
++
++ const none = setup({ session: memoryStorage(), html: '<div class="openvibe-navbar"><a href="/" class="brand"><span class="ov-mark" data-size="28"></span></a></div>' });
++ none.io.fire(none.mark, true); none.clock.advance(1000);
++ assert.strictEqual(none.document.querySelectorAll('.ovm-fall').length, 0, 'no season, no fall');
++ }
++
// ── intro: runs once on screen, stops by itself after about 8 s ─────────────────
{
const t = setup();