openvibers/openvibe.shared Commits
4.141.0: the forms kit loads when a form control is drawn (#329)
Changes
@@@ -1,5 +1,17 @@# Changelog++## 4.141.0 — 2026-11-07++++- **The forms kit loads when a form control is drawn, not merely present** (the owner, 11-07: "make things faster and++ more optimized").++ - A closed dialog's or a hidden box's select, checkbox or file input no longer counts.++ - Live's shell carries hidden modal forms, so every Live page fetched `ov-forms.js` and `.css` (17 KB) for++ controls nobody saw.++ - Hidden controls are watched with an IntersectionObserver, and the kit loads when one is drawn (a dialog opens, a++ fold unfolds).++ - A page with a drawn control loads it once it settles, as before.++ - Test: `test/forms-when-seen-chrome.test.js`.++## 4.140.0 — 2026-11-07- **Native: no more Material lavender.**@@@ -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.140.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.141.0 (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,13 +4,14 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.140.0",++ "version": "4.141.0","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)","contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)","sdk": "openvibe-sdk v0.94.0","features": [++ "the forms kit loads when a form control is drawn, not merely present: a closed dialog's select costs nothing (4.141.0)","the Compose export maps Material 3's container roles (cards, the navigation bar and its selected pill, dialogs) to Vibe surfaces, so native apps never show Material's lavender (4.140.0)","the menu's feed rows with a picture show the picture, not a site icon beside it; a row without one in that group keeps its icon in a picture-sized tile (4.139.0)","the footer's shipped line and brand logos load when the footer nears the screen (38 KB off most first visits); the footer, Boost and the glyphs load their files by content hash (4.138.0)",@@@ -584,10 +584,27 @@// first time one appears. <html data-ov-forms="off"> keeps the browser's own; data-ov-native keeps one control's.const FORM_CONTROLS = 'select,input[type="checkbox"],input[type="radio"],input[type="range"],input[type="file"]';let formWatch = null;++ // Only a control that is drawn counts (4.141.0): a closed dialog's or a hidden box's select does not, so a page whose++ // shell carries hidden modal forms (Live's) no longer fetches 17 KB it may never use. The hidden ones are watched,++ // and the kit loads when one is drawn (a dialog opens, a fold unfolds).++ let formSeen = null;++ function formsWanted() {++ const all = document.querySelectorAll(FORM_CONTROLS);++ if (!all.length) return false;++ for (let i = 0; i < all.length && i < 200; i++) if (all[i].getClientRects().length) return true;++ if (typeof IntersectionObserver === 'function') {++ formSeen = formSeen || new IntersectionObserver((es) => {++ if (!es.some((e) => e.isIntersecting || e.boundingClientRect.width)) return;++ formSeen.disconnect(); formSeen = null; loadForms();++ });++ for (let i = 0; i < all.length && i < 60; i++) formSeen.observe(all[i]);++ }++ return false;++ }function loadForms() {if (root.OVForms || document.getElementById('ov-forms-loader')) return true;if (document.documentElement.getAttribute('data-ov-forms') === 'off') return true;-- if (!document.querySelector(FORM_CONTROLS)) return false;++ if (!formsWanted()) return false;try {if (!document.querySelector('link[href*="ov-forms.css"]')) {const ln = document.createElement('link'); ln.rel = 'stylesheet'; ln.href = siblingV('ov-forms.css'); document.head.appendChild(ln);@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.140.0",++ "version": "4.141.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {nnew file mode 100644@@@ -0,0 +1,64 @@++'use strict';++// The forms kit loads when a form control is drawn, not merely present (4.141.0, the owner 11-07: "make things faster++// and more optimized"). Live's shell carries hidden modal forms, so every Live page fetched ov-forms.js and .css++// (17 KB) for selects nobody saw. A real headless Chrome; skipped without Chrome.++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++const http = require('http');++++const ROOT = path.join(__dirname, '..');++const h = require('../browser-harness');++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {++ console.log('forms-when-seen-chrome: skipped (no Chrome; set CHROME_BIN)');++ process.exit(0);++}++++const page = (body) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Forms</title><link rel="icon" href="data:,">++<script>window.fetch = () => Promise.resolve({ ok: false, status: 404, json: async () => ({}) });</script>++<script src="/shared/navbar.js" defer></script></head><body><div id="navbar-mount"></div>${body}++<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: null, links: [{ label: 'Home', href: '/' }], countViews: false, notifications: false }); });</script>++</body></html>`;++const HIDDEN = page('<main><h1>Home</h1><div id="modal" hidden><label>Voice <select id="voice"><option>One</option><option>Two</option></select></label></div></main>');++const SHOWN = page('<main><h1>Settings</h1><label>Voice <select id="voice"><option>One</option><option>Two</option></select></label></main>');++++const server = http.createServer((req, res) => {++ const u = new URL(req.url, 'http://x');++ if (u.pathname === '/hidden') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(HIDDEN); }++ if (u.pathname === '/shown') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(SHOWN); }++ const m = /^\/shared\/([a-z0-9-]+\.(js|css))$/.exec(u.pathname);++ if (m && fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(200, { 'content-type': m[2] === 'css' ? 'text/css' : 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, m[1]))); }++ res.writeHead(404); return res.end();++});++++(async () => {++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));++ const base = `http://127.0.0.1:${server.address().port}`;++ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-forms-seen-'));++ const browser = await h.launch({ tmpDir });++ const wait = (ms) => new Promise((r) => setTimeout(r, ms));++ try {++ const context = await browser.newContext();++ const asked = "performance.getEntriesByType('resource').some((e) => /ov-forms\\.js/.test(e.name))";++ const until = async (p, expr) => { const end = Date.now() + 4000; while (Date.now() < end) { if (await p.evaluate(expr)) return true; await wait(100); } return false; };++ const p = await h.openPage(browser, context, { width: 390 });++ await p.goto(`${base}/hidden`, { maxMs: 5000 });++ await wait(3000);++ assert.strictEqual(await p.evaluate(asked), false, 'a hidden dialog\'s select does not fetch the forms kit');++ assert.strictEqual(await p.evaluate('!!window.OVForms'), false);++++ await p.evaluate("(document.getElementById('modal').hidden = false, 1)");++ assert.ok(await until(p, '!!window.OVForms'), 'the kit loads when the dialog opens');++ assert.strictEqual(await p.evaluate(asked), true);++++ const q = await h.openPage(browser, context, { width: 390 });++ await q.goto(`${base}/shown`, { maxMs: 5000 });++ assert.ok(await until(q, '!!window.OVForms'), 'a drawn select loads it once the page settles, as before');++ console.log('forms-when-seen-chrome: hidden forms fetch nothing; the kit arrives when one is drawn');++ } finally {++ await browser.close();++ server.close();++ fs.rmSync(tmpDir, { recursive: true, force: true });++ }++})().catch((e) => { console.error(e); process.exit(1); });
@@ -1,5 +1,17 @@
@@ -1,5 +1,17 @@
# Changelog
# Changelog
+## 4.141.0 — 2026-11-07
+
+- **The forms kit loads when a form control is drawn, not merely present** (the owner, 11-07: "make things faster and
+ more optimized").
+ - A closed dialog's or a hidden box's select, checkbox or file input no longer counts.
+ - Live's shell carries hidden modal forms, so every Live page fetched `ov-forms.js` and `.css` (17 KB) for
+ controls nobody saw.
+ - Hidden controls are watched with an IntersectionObserver, and the kit loads when one is drawn (a dialog opens, a
+ fold unfolds).
+ - A page with a drawn control loads it once it settles, as before.
+ - Test: `test/forms-when-seen-chrome.test.js`.
+
## 4.140.0 — 2026-11-07
## 4.140.0 — 2026-11-07
- **Native: no more Material lavender.**
- **Native: no more Material lavender.**
@@ -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.140.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.141.0 (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,13 +4,14 @@
@@ -4,13 +4,14 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.140.0",
+ "version": "4.141.0",
"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)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"contracts": "openvibe-contracts v0.312.0 (devDependency: the release manifest contract)",
"sdk": "openvibe-sdk v0.94.0",
"sdk": "openvibe-sdk v0.94.0",
"features": [
"features": [
+ "the forms kit loads when a form control is drawn, not merely present: a closed dialog's select costs nothing (4.141.0)",
"the Compose export maps Material 3's container roles (cards, the navigation bar and its selected pill, dialogs) to Vibe surfaces, so native apps never show Material's lavender (4.140.0)",
"the Compose export maps Material 3's container roles (cards, the navigation bar and its selected pill, dialogs) to Vibe surfaces, so native apps never show Material's lavender (4.140.0)",
"the menu's feed rows with a picture show the picture, not a site icon beside it; a row without one in that group keeps its icon in a picture-sized tile (4.139.0)",
"the menu's feed rows with a picture show the picture, not a site icon beside it; a row without one in that group keeps its icon in a picture-sized tile (4.139.0)",
"the footer's shipped line and brand logos load when the footer nears the screen (38 KB off most first visits); the footer, Boost and the glyphs load their files by content hash (4.138.0)",
"the footer's shipped line and brand logos load when the footer nears the screen (38 KB off most first visits); the footer, Boost and the glyphs load their files by content hash (4.138.0)",
@@ -584,10 +584,27 @@
@@ -584,10 +584,27 @@
// first time one appears. <html data-ov-forms="off"> keeps the browser's own; data-ov-native keeps one control's.
// first time one appears. <html data-ov-forms="off"> keeps the browser's own; data-ov-native keeps one control's.
const FORM_CONTROLS = 'select,input[type="checkbox"],input[type="radio"],input[type="range"],input[type="file"]';
const FORM_CONTROLS = 'select,input[type="checkbox"],input[type="radio"],input[type="range"],input[type="file"]';
let formWatch = null;
let formWatch = null;
+ // Only a control that is drawn counts (4.141.0): a closed dialog's or a hidden box's select does not, so a page whose
+ // shell carries hidden modal forms (Live's) no longer fetches 17 KB it may never use. The hidden ones are watched,
+ // and the kit loads when one is drawn (a dialog opens, a fold unfolds).
+ let formSeen = null;
+ function formsWanted() {
+ const all = document.querySelectorAll(FORM_CONTROLS);
+ if (!all.length) return false;
+ for (let i = 0; i < all.length && i < 200; i++) if (all[i].getClientRects().length) return true;
+ if (typeof IntersectionObserver === 'function') {
+ formSeen = formSeen || new IntersectionObserver((es) => {
+ if (!es.some((e) => e.isIntersecting || e.boundingClientRect.width)) return;
+ formSeen.disconnect(); formSeen = null; loadForms();
+ });
+ for (let i = 0; i < all.length && i < 60; i++) formSeen.observe(all[i]);
+ }
+ return false;
+ }
function loadForms() {
function loadForms() {
if (root.OVForms || document.getElementById('ov-forms-loader')) return true;
if (root.OVForms || document.getElementById('ov-forms-loader')) return true;
if (document.documentElement.getAttribute('data-ov-forms') === 'off') return true;
if (document.documentElement.getAttribute('data-ov-forms') === 'off') return true;
- if (!document.querySelector(FORM_CONTROLS)) return false;
+ if (!formsWanted()) return false;
try {
try {
if (!document.querySelector('link[href*="ov-forms.css"]')) {
if (!document.querySelector('link[href*="ov-forms.css"]')) {
const ln = document.createElement('link'); ln.rel = 'stylesheet'; ln.href = siblingV('ov-forms.css'); document.head.appendChild(ln);
const ln = document.createElement('link'); ln.rel = 'stylesheet'; ln.href = siblingV('ov-forms.css'); document.head.appendChild(ln);
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.140.0",
+ "version": "4.141.0",
"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": {
new file mode 100644
new file mode 100644
@@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
+'use strict';
+// The forms kit loads when a form control is drawn, not merely present (4.141.0, the owner 11-07: "make things faster
+// and more optimized"). Live's shell carries hidden modal forms, so every Live page fetched ov-forms.js and .css
+// (17 KB) for selects nobody saw. A real headless Chrome; skipped without Chrome.
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const http = require('http');
+
+const ROOT = path.join(__dirname, '..');
+const h = require('../browser-harness');
+if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
+ console.log('forms-when-seen-chrome: skipped (no Chrome; set CHROME_BIN)');
+ process.exit(0);
+}
+
+const page = (body) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Forms</title><link rel="icon" href="data:,">
+<script>window.fetch = () => Promise.resolve({ ok: false, status: 404, json: async () => ({}) });</script>
+<script src="/shared/navbar.js" defer></script></head><body><div id="navbar-mount"></div>${body}
+<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: null, links: [{ label: 'Home', href: '/' }], countViews: false, notifications: false }); });</script>
+</body></html>`;
+const HIDDEN = page('<main><h1>Home</h1><div id="modal" hidden><label>Voice <select id="voice"><option>One</option><option>Two</option></select></label></div></main>');
+const SHOWN = page('<main><h1>Settings</h1><label>Voice <select id="voice"><option>One</option><option>Two</option></select></label></main>');
+
+const server = http.createServer((req, res) => {
+ const u = new URL(req.url, 'http://x');
+ if (u.pathname === '/hidden') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(HIDDEN); }
+ if (u.pathname === '/shown') { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(SHOWN); }
+ const m = /^\/shared\/([a-z0-9-]+\.(js|css))$/.exec(u.pathname);
+ if (m && fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(200, { 'content-type': m[2] === 'css' ? 'text/css' : 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, m[1]))); }
+ res.writeHead(404); return res.end();
+});
+
+(async () => {
+ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
+ const base = `http://127.0.0.1:${server.address().port}`;
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-forms-seen-'));
+ const browser = await h.launch({ tmpDir });
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
+ try {
+ const context = await browser.newContext();
+ const asked = "performance.getEntriesByType('resource').some((e) => /ov-forms\\.js/.test(e.name))";
+ const until = async (p, expr) => { const end = Date.now() + 4000; while (Date.now() < end) { if (await p.evaluate(expr)) return true; await wait(100); } return false; };
+ const p = await h.openPage(browser, context, { width: 390 });
+ await p.goto(`${base}/hidden`, { maxMs: 5000 });
+ await wait(3000);
+ assert.strictEqual(await p.evaluate(asked), false, 'a hidden dialog\'s select does not fetch the forms kit');
+ assert.strictEqual(await p.evaluate('!!window.OVForms'), false);
+
+ await p.evaluate("(document.getElementById('modal').hidden = false, 1)");
+ assert.ok(await until(p, '!!window.OVForms'), 'the kit loads when the dialog opens');
+ assert.strictEqual(await p.evaluate(asked), true);
+
+ const q = await h.openPage(browser, context, { width: 390 });
+ await q.goto(`${base}/shown`, { maxMs: 5000 });
+ assert.ok(await until(q, '!!window.OVForms'), 'a drawn select loads it once the page settles, as before');
+ console.log('forms-when-seen-chrome: hidden forms fetch nothing; the kit arrives when one is drawn');
+ } finally {
+ await browser.close();
+ server.close();
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ }
+})().catch((e) => { console.error(e); process.exit(1); });