openvibers/openvibe.shared Commits
The seasonal fall lands on the bar, never over a page's heading (4.50.1) (#196)
Changes
@@@ -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@@@ -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 deployedconsumer 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` namesthe tag it pins.@@@ -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)",@@@ -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);@@@ -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": {@@@ -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, htmproto.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, htmassert.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();
@@ -6,6 +6,14 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,14 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## 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
## 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
- **Rich embeds in the agent session.** The owner, 2026-10-30: "When it generates or reads or analyzed images or visuals
@@ -2,7 +2,7 @@
@@ -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.
> 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
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
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.50.0",
+ "version": "4.50.1",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"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)",
"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)",
@@ -93,7 +93,7 @@ html[data-ov-season-mark=winter] .ovm-fall i{width:6px;height:6px;border-radius:
@@ -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 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 ovmWiggle{0%,100%{transform:none}25%{transform:rotate(-18deg)}60%{transform:rotate(12deg)}}
@keyframes ovmTwinkle{0%,100%{opacity:1}50%{opacity:.3}}
@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}}
@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}`;
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.
// 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}`;
@@ -177,14 +177,22 @@ html[data-ov-motion=reduced] .ov-mark .sw{display:none}`;
function fallOnce(el) {
function fallOnce(el) {
const id = season(); if (!id || reduced() || document.hidden || !el.isConnected) return;
const id = season(); if (!id || reduced() || document.hidden || !el.isConnected) return;
const day = id + ':' + new Date().toDateString();
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 r = el.getBoundingClientRect(); if (!r.width || !document.body) return;
const box = document.createElement('div'); box.className = 'ovm-fall'; box.setAttribute('aria-hidden', 'true');
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'];
const colors = id === 'winter' ? ['#f4fbff', '#d8f3ff', '#ffffff'] : ['#ff7a1a', '#f5a524', '#d9481f', '#7fd04e'];
for (let i = 0; i < 7; i++) {
for (let i = 0; i < 7; i++) {
const s = document.createElement('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);
box.appendChild(s);
}
}
document.body.appendChild(box);
document.body.appendChild(box);
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"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",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -15,8 +15,8 @@ function memoryStorage() {
@@ -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); } };
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 = [];
let now = start, timers = [];
const clock = {
const clock = {
now: () => now,
now: () => now,
@@ -29,7 +29,7 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, htm
@@ -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 setT = (fn, ms) => { const t = { fn, at: now + ms }; timers.push(t); return t; };
const clearT = (t) => { timers = timers.filter((x) => x !== t); };
const clearT = (t) => { timers = timers.filter((x) => x !== t); };
const observers = [];
const observers = [];
@@ -51,6 +51,14 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, htm
@@ -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.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.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); };
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);
const run = new Function('window', 'document', 'IntersectionObserver', 'MutationObserver', 'setTimeout', 'clearTimeout', 'Date', 'sessionStorage', SRC);
run(window, document, IO, MutationObserver, setT, clearT, FakeDate, session);
run(window, document, IO, MutationObserver, setT, clearT, FakeDate, session);
const io = observers[0];
const io = observers[0];
@@ -79,6 +87,39 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, htm
@@ -79,6 +87,39 @@ function setup({ reduce = false, session = memoryStorage(), start = 1000000, htm
assert.strictEqual(t.smilTime(), 1.6);
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 ─────────────────
// ── intro: runs once on screen, stops by itself after about 8 s ─────────────────
{
{
const t = setup();
const t = setup();