openvibers/openvibe.shared Commits
openvibe-shared 4.230.0: OpenVibe's own controls on every site (#512)
Changes
@@@ -1,5 +1,10 @@# Changelog++## 4.230.0 — 2026-11-12++++- **OpenVibe's own controls on every site** (the owner, 11-12: "improve the selection boxes to use a more customized look integrated into openvibe.design and themed better with animations and effects for all inputs and buttons"). `ov-forms.css` now draws every checkbox and radio: a rounded box whose check pops in, and a ring whose dot grows. The mark is the box's own `::before` (a clip-path check, scaled), so no picture is fetched. Both lift their border on hover, squeeze on a press and glow with focus; the mixed state is a bar; disabled is faded. A switch (`role="switch"`) glows when on, its knob squeezes when pressed, and it has a focus ring. Buttons answer a press, and text fields, text areas and selects glow in the accent with focus. Everything is `:where()`, so a site's own rule wins; `data-ov-native` keeps the browser's control; motion follows the person (Calm slower, Off moves nothing).++- The bar loads `ov-forms.css` on any page with a text field, a text area or a button. The select kit's script still waits for a select, a box or a file input.++## 4.229.0 — 2026-11-12- **Back brings you back to the menu** (the owner, 11-12: "fix the history back button so it properly brings you back to this GUI rather than the page behind it"). Leaving through a link in the menu marks where it was on its history entry (its place, its door, its scroll); Back to that page opens the menu again there, whether the page comes back from the browser's cache, loads again, or a site swaps it in place (Boost). It reuses that entry, so Forward still works, and closing it then leaves the page as it was. Only for a Back or Forward within half an hour.@@@ -5,7 +5,7 @@**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt**Install and test:** `npm ci && npm test`--**Status:** alpha, v4.229.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.230.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,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.229.0",++ "version": "4.230.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)",@@@ -610,7 +610,7 @@* serves its own pinned copy at /shared/ gets its own), else the network's copy.*/// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.-- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': 'c96584376da1', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'ef289c4550b1', 'dock.js': '9f15df28050c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '3a88a5ba2565', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'b11405559831', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': 'ac7107f5d733' };++ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': 'c96584376da1', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'ef289c4550b1', 'dock.js': '9f15df28050c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '3a88a5ba2565', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'b11405559831', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': 'ac7107f5d733' };const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;// ov-tip.js loads at the first hover over a tooltip's owner.@@@ -684,9 +684,17 @@}return false;}++ // 4.230.0: the look of every field and button is in ov-forms.css, so a page with any text field, text area or button++ // gets the stylesheet once it settles; the select kit's script still waits for a select, a box or a file input.++ function formsCss() {++ if (document.documentElement.getAttribute('data-ov-forms') === 'off' || document.querySelector('link[href*="ov-forms.css"]')) return;++ if (!document.querySelector(FORM_CONTROLS + ',input,textarea,button')) return;++ try { const ln = document.createElement('link'); ln.rel = 'stylesheet'; ln.href = siblingV('ov-forms.css'); document.head.appendChild(ln); } catch { /* */ }++ }function loadForms() {if (root.OVForms || document.getElementById('ov-forms-loader')) return true;if (document.documentElement.getAttribute('data-ov-forms') === 'off') return true;++ formsCss();if (!formsWanted()) return false;try {if (!document.querySelector('link[href*="ov-forms.css"]')) {@@@ -1,9 +1,11 @@/* openvibe-shared/ov-forms.css — OpenVibe's own form controls, on every site (docs/forms.md). ov-forms.js draws the* select (a button, its menu on a computer, its sheet on a phone); the rest is this file alone:*-- * every page checkboxes, radios, sliders and progress take the site's accent; a checkbox with role="switch" is a-- * switch; a file input's button is a button; nothing here moves a site's layout (all of it is-- * :where(), so a site's own rule always wins)++ * every page checkboxes and radios are drawn (4.230.0: a box with a check that pops in, a ring with a dot; hover, a++ * press, a focus glow), sliders and progress take the site's accent; a checkbox with role="switch" is++ * a switch that glows when on and squeezes when pressed; buttons answer a press, text fields glow++ * with focus; a file input's button is a button. All of it is :where(), so a site's own rule always++ * wins; motion follows the person's setting (Calm slower, Off none)* opted in .ov-field (a label, a control, a hint, an error) and .ov-input: the full field look;* .ov-choices / .ov-choice: a choice drawn as a card (radio or checkbox inside)* opted out data-ov-native on a control or any ancestor, or <html data-ov-forms="off">@@@ -149,10 +151,61 @@ html.ov-sel-locked, html.ov-sel-locked body { overflow: hidden; overscroll-behav.ov-sel-group:first-child { padding-top: 4px; }.ov-sel-none { padding: 18px 12px; color: var(--ovf-muted); text-align: center; }--/* ── Every page: the browser's own boxes, in the site's colours (nothing here changes a size) ─────────────────── */++/* ── Every page: OpenVibe's own controls, in the site's colours ──────────────────────────────────────────────── */:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"], input[type="range"], progress, meter):where(:not([data-ov-native], [data-ov-native] *)) {accent-color: var(--accent, #3b82f6);}++/* 4.230.0 (the owner, 11-12: "improve the selection boxes … a more customized look … themed better with animations and++ effects for all inputs and buttons"): a checkbox is a rounded box whose check pops in, a radio a ring whose dot++ grows; both lift their border on hover, squeeze on a press and glow with focus. The mark is the box's own ::before++ (clip-path check, scaled), so no picture is fetched and no page's image policy is involved. */++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)) {++ -webkit-appearance: none; appearance: none; box-sizing: border-box; flex: none; vertical-align: middle;++ display: inline-grid; place-content: center; width: 18px; height: 18px; cursor: pointer;++ border: 1.5px solid color-mix(in srgb, var(--text-primary, CanvasText) 38%, transparent); border-radius: 6px;++ background: var(--bg-input, color-mix(in srgb, var(--text-primary, CanvasText) 5%, transparent)); color: var(--accent, #3b82f6);++ transition: background-color .16s ease, border-color .16s ease, box-shadow .2s ease, transform .12s ease;++}++:where(html:not([data-ov-forms="off"])) :where(input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)) { border-radius: 50%; }++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *))::before {++ content: ""; width: 10px; height: 10px; transform: scale(0); transform-origin: 50% 60%;++ box-shadow: inset 1em 1em var(--on-accent, #fff);++ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);++ transition: transform .18s cubic-bezier(.2, 1.5, .4, 1);++}++:where(html:not([data-ov-forms="off"])) :where(input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *))::before {++ width: 8px; height: 8px; border-radius: 50%; clip-path: none; box-shadow: inset 1em 1em var(--accent, #3b82f6); transform-origin: 50% 50%;++}++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]):checked, input[type="checkbox"]:not([role="switch"]):indeterminate):where(:not([data-ov-native], [data-ov-native] *)) {++ background: var(--accent, #3b82f6); border-color: var(--accent, #3b82f6);++}++:where(html:not([data-ov-forms="off"])) :where(input[type="radio"]:checked):where(:not([data-ov-native], [data-ov-native] *)) {++ border-color: var(--accent, #3b82f6); border-width: 2px;++}++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]):checked, input[type="radio"]:checked):where(:not([data-ov-native], [data-ov-native] *))::before { transform: scale(1); }++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]):indeterminate):where(:not([data-ov-native], [data-ov-native] *))::before {++ transform: scale(1); height: 2.5px; border-radius: 2px; clip-path: none;++}++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):hover {++ border-color: color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent);++ box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent);++}++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active { transform: scale(.88); }++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {++ box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 28%, transparent);++}++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)):disabled { opacity: .5; cursor: not-allowed; }++/* Buttons answer a press; text fields glow with focus (a site's own button or field rule wins over these). */++:where(html:not([data-ov-forms="off"])) :where(button, input[type="button"], input[type="submit"], input[type="reset"], a.btn, .btn, [role="button"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)) {++ transition: transform .12s ease, box-shadow .18s ease, background-color .16s ease, border-color .16s ease, color .16s ease;++}++:where(html:not([data-ov-forms="off"])) :where(button, input[type="button"], input[type="submit"], input[type="reset"], a.btn, .btn, [role="button"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active { transform: scale(.97); }++:where(html:not([data-ov-forms="off"])) :where(input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="hidden"], [type="button"], [type="submit"], [type="reset"]), textarea, select):where(:not([data-ov-native], [data-ov-native] *)) {++ transition: border-color .15s ease, box-shadow .2s ease, background-color .15s ease;++}++:where(html:not([data-ov-forms="off"])) :where(input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="hidden"], [type="button"], [type="submit"], [type="reset"]), textarea, select):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {++ outline: 2px solid var(--accent, #3b82f6); outline-offset: 0; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent);++}:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"], input[type="range"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px;}@@@ -167,7 +220,26 @@ html.ov-sel-locked, html.ov-sel-locked body { overflow: hidden; overscroll-behav}:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *)) {background-color: var(--accent, #3b82f6); background-position: calc(100% - 2px) 50%;++ box-shadow: inset 0 0 0 1px color-mix(in srgb, #000 12%, transparent), 0 0 14px color-mix(in srgb, var(--accent, #3b82f6) 38%, transparent);++}++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):hover {++ background-color: color-mix(in srgb, CanvasText 34%, transparent);++}++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *, :disabled)):hover {++ background-color: color-mix(in srgb, var(--accent, #3b82f6) 88%, #fff);++}++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active {++ transform: none; background-image: radial-gradient(ellipse 13px 8.5px, #fff 0 92%, transparent 100%); background-size: 26px 20px;++}++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active { background-position: calc(100% - 2px) 50%; }++:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {++ outline: none; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 30%, transparent);}++/* Motion follows the person: Calm takes its time, Off moves nothing (the state still changes). */++html[data-ov-motion="calm"] :where(input[type="checkbox"], input[type="radio"], button, .btn)::before { transition-duration: .32s; }++html[data-ov-motion="off"] :where(input[type="checkbox"], input[type="radio"], button, input[type="button"], input[type="submit"], .btn, [role="button"], input, textarea, select),++html[data-ov-motion="off"] :where(input[type="checkbox"], input[type="radio"])::before { transition: none !important; animation: none !important; }++html[data-ov-motion="off"] :where(input[type="checkbox"], input[type="radio"], button, .btn, [role="button"]):active { transform: none !important; }:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:disabled) { opacity: .5; cursor: not-allowed; }:where(html:not([data-ov-forms="off"])) :where(input[type="file"]:not([data-ov-native]))::file-selector-button {font: inherit; font-weight: 600; margin-inline-end: .75em; padding: .38em .85em; cursor: pointer;@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "4.229.0",++ "version": "4.230.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "4.229.0",++ "version": "4.230.0","license": "MIT","dependencies": {"clean-css": "^5.3.3",@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.229.0",++ "version": "4.230.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,58 @@++'use strict';++// OpenVibe's own controls (4.230.0; the owner, 11-12: "improve the selection boxes … a more customized look … themed++// better with animations and effects for all inputs and buttons"), in a real headless Chrome with ov-forms.css alone:++// a checkbox and a radio are drawn (no native look) and their mark scales in when checked; a switch stays a switch; a++// site's own rule wins (:where), data-ov-native keeps the browser's box, motion Off removes the transitions, and the++// mark needs no picture (no url() in the stylesheet, so no page's image policy is involved). Skipped without Chrome.++const assert = require('node:assert/strict');++const fs = require('node:fs');++const os = require('node:os');++const path = require('node:path');++const http = require('node:http');++const h = require('../browser-harness');++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) { console.log('ov-forms-controls-chrome: skipped (no Chrome; set CHROME_BIN)'); process.exit(0); }++const root = path.join(__dirname, '..');++const css = fs.readFileSync(path.join(root, 'ov-forms.css'), 'utf8');++assert.ok(!/url\(/.test(css.replace(/\/\*[\s\S]*?\*\//g, '')), 'no url() in ov-forms.css: the marks are drawn, not fetched');++++const page = (motion) => `<!doctype html><html lang="en"${motion ? ` data-ov-motion="${motion}"` : ''}><head><meta charset="utf-8"><link rel="stylesheet" href="/ov-forms.css">++<style>.mine { width: 30px; height: 30px; }</style></head><body>++<input type="checkbox" id="on" checked><input type="checkbox" id="off"><input type="radio" id="r" checked><input type="checkbox" role="switch" id="sw" checked>++<input type="checkbox" id="mine" class="mine"><span data-ov-native><input type="checkbox" id="native"></span><button id="b">B</button><input id="t"></body></html>`;++const server = http.createServer((req, res) => {++ const u = new URL(req.url, 'http://x');++ if (u.pathname === '/ov-forms.css') { res.writeHead(200, { 'content-type': 'text/css' }); return res.end(css); }++ res.writeHead(200, { 'content-type': 'text/html' }); res.end(page(u.searchParams.get('motion')));++});++(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-controls-'));++ const browser = await h.launch({ tmpDir });++ try {++ const p = await h.openPage(browser, await browser.newContext(), { width: 390, height: 400 });++ await p.goto(`${base}/`, { maxMs: 5000 });++ const cs = (id, prop, pseudo = null) => p.evaluate(`getComputedStyle(document.getElementById(${JSON.stringify(id)})${pseudo ? `, ${JSON.stringify(pseudo)}` : ''}).${prop}`);++ assert.equal(await cs('on', 'appearance'), 'none', 'a checkbox is drawn');++ assert.equal(await cs('on', 'width'), '18px');++ assert.equal(await cs('on', 'transform', '::before'), 'matrix(1, 0, 0, 1, 0, 0)', 'its check is shown when checked');++ assert.equal(await cs('off', 'transform', '::before'), 'matrix(0, 0, 0, 0, 0, 0)', 'and scaled away when not');++ assert.ok(/transform/.test(await cs('off', 'transitionProperty', '::before')), 'the check scales in');++ assert.equal(await cs('r', 'appearance'), 'none', 'a radio is drawn');++ assert.equal(await cs('r', 'borderTopLeftRadius'), '50%');++ assert.equal(await cs('sw', 'width'), '42px', 'a switch stays a switch');++ assert.equal(await cs('sw', 'transform', '::before'), 'none', 'a switch has no check mark');++ assert.equal(await cs('mine', 'width'), '30px', 'a site\'s own rule wins');++ assert.notEqual(await cs('native', 'appearance'), 'none', 'data-ov-native keeps the browser\'s box');++ assert.ok(/transform/.test(await cs('b', 'transitionProperty')), 'a button answers a press');++ await p.goto(`${base}/?motion=off`, { maxMs: 5000 });++ assert.equal(await cs('off', 'transitionDuration', '::before'), '0s', 'motion Off: the check does not move');++ assert.equal(await cs('b', 'transitionDuration'), '0s', 'motion Off: buttons do not move');++ await p.close();++ console.log('ov-forms-controls-chrome: drawn checkbox and radio, the check scales in, switch kept, site rules win, data-ov-native, no url(), motion Off');++ } finally {++ await browser.close();++ server.close();++ fs.rmSync(tmpDir, { recursive: true, force: true });++ }++})().catch((e) => { console.error(e); process.exit(1); });
@@ -1,5 +1,10 @@
@@ -1,5 +1,10 @@
# Changelog
# Changelog
+## 4.230.0 — 2026-11-12
+
+- **OpenVibe's own controls on every site** (the owner, 11-12: "improve the selection boxes to use a more customized look integrated into openvibe.design and themed better with animations and effects for all inputs and buttons"). `ov-forms.css` now draws every checkbox and radio: a rounded box whose check pops in, and a ring whose dot grows. The mark is the box's own `::before` (a clip-path check, scaled), so no picture is fetched. Both lift their border on hover, squeeze on a press and glow with focus; the mixed state is a bar; disabled is faded. A switch (`role="switch"`) glows when on, its knob squeezes when pressed, and it has a focus ring. Buttons answer a press, and text fields, text areas and selects glow in the accent with focus. Everything is `:where()`, so a site's own rule wins; `data-ov-native` keeps the browser's control; motion follows the person (Calm slower, Off moves nothing).
+- The bar loads `ov-forms.css` on any page with a text field, a text area or a button. The select kit's script still waits for a select, a box or a file input.
+
## 4.229.0 — 2026-11-12
## 4.229.0 — 2026-11-12
- **Back brings you back to the menu** (the owner, 11-12: "fix the history back button so it properly brings you back to this GUI rather than the page behind it"). Leaving through a link in the menu marks where it was on its history entry (its place, its door, its scroll); Back to that page opens the menu again there, whether the page comes back from the browser's cache, loads again, or a site swaps it in place (Boost). It reuses that entry, so Forward still works, and closing it then leaves the page as it was. Only for a Back or Forward within half an hour.
- **Back brings you back to the menu** (the owner, 11-12: "fix the history back button so it properly brings you back to this GUI rather than the page behind it"). Leaving through a link in the menu marks where it was on its history entry (its place, its door, its scroll); Back to that page opens the menu again there, whether the page comes back from the browser's cache, loads again, or a site swaps it in place (Boost). It reuses that entry, so Forward still works, and closing it then leaves the page as it was. Only for a Back or Forward within half an hour.
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt
**Install and test:** `npm ci && npm test`
**Install and test:** `npm ci && npm test`
-**Status:** alpha, v4.229.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.230.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,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.229.0",
+ "version": "4.230.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)",
@@ -610,7 +610,7 @@
@@ -610,7 +610,7 @@
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
* serves its own pinned copy at /shared/ gets its own), else the network's copy.
*/
*/
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
- const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': 'c96584376da1', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '06208fe89eac', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'ef289c4550b1', 'dock.js': '9f15df28050c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '3a88a5ba2565', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'b11405559831', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': 'ac7107f5d733' };
+ const SIBLING_REV = { 'ov-mark.js': 'f34f731c3a94', 'ov-glyphs.js': 'c96584376da1', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': 'dd84a30bb646', 'ov-fx.js': '947820d69097', 'ov-labels.js': 'd40f7ca14bc4', 'ov-forms.js': '2eb9e2604c3e', 'ov-forms.css': '07e8f59d9e6d', 'ov-share.js': 'd419508a797e', 'ov-share.css': 'd609a71f611a', 'search.js': 'ef289c4550b1', 'dock.js': '9f15df28050c', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '0cef1f895965', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': '3a88a5ba2565', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': 'b11405559831', 'nav-slots.js': '7b11e6916732', 'ov-wallet.js': '3fa6bb23b429', 'ov-wallet.css': '4923c808c9ad', 'purse.js': '9d3fea0dae02', 'ov-drop.js': '445689a5eb80', 'ov-tip.js': '1d918e7de69d', 'ov-arrange.js': 'ae24086bd2be', 'soft.js': '98ab9b8080b1', 'ov-run.js': '91be6059adcf', 'ov-composer.js': 'ac7107f5d733' };
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
const siblingV = (name) => `${sibling(name)}${SIBLING_REV[name] ? `?v=${SIBLING_REV[name]}` : ''}`;
// ov-tip.js loads at the first hover over a tooltip's owner.
// ov-tip.js loads at the first hover over a tooltip's owner.
@@ -684,9 +684,17 @@
@@ -684,9 +684,17 @@
}
}
return false;
return false;
}
}
+ // 4.230.0: the look of every field and button is in ov-forms.css, so a page with any text field, text area or button
+ // gets the stylesheet once it settles; the select kit's script still waits for a select, a box or a file input.
+ function formsCss() {
+ if (document.documentElement.getAttribute('data-ov-forms') === 'off' || document.querySelector('link[href*="ov-forms.css"]')) return;
+ if (!document.querySelector(FORM_CONTROLS + ',input,textarea,button')) return;
+ try { const ln = document.createElement('link'); ln.rel = 'stylesheet'; ln.href = siblingV('ov-forms.css'); document.head.appendChild(ln); } catch { /* */ }
+ }
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;
+ formsCss();
if (!formsWanted()) return false;
if (!formsWanted()) return false;
try {
try {
if (!document.querySelector('link[href*="ov-forms.css"]')) {
if (!document.querySelector('link[href*="ov-forms.css"]')) {
@@ -1,9 +1,11 @@
@@ -1,9 +1,11 @@
/* openvibe-shared/ov-forms.css — OpenVibe's own form controls, on every site (docs/forms.md). ov-forms.js draws the
/* openvibe-shared/ov-forms.css — OpenVibe's own form controls, on every site (docs/forms.md). ov-forms.js draws the
* select (a button, its menu on a computer, its sheet on a phone); the rest is this file alone:
* select (a button, its menu on a computer, its sheet on a phone); the rest is this file alone:
*
*
- * every page checkboxes, radios, sliders and progress take the site's accent; a checkbox with role="switch" is a
- * switch; a file input's button is a button; nothing here moves a site's layout (all of it is
- * :where(), so a site's own rule always wins)
+ * every page checkboxes and radios are drawn (4.230.0: a box with a check that pops in, a ring with a dot; hover, a
+ * press, a focus glow), sliders and progress take the site's accent; a checkbox with role="switch" is
+ * a switch that glows when on and squeezes when pressed; buttons answer a press, text fields glow
+ * with focus; a file input's button is a button. All of it is :where(), so a site's own rule always
+ * wins; motion follows the person's setting (Calm slower, Off none)
* opted in .ov-field (a label, a control, a hint, an error) and .ov-input: the full field look;
* opted in .ov-field (a label, a control, a hint, an error) and .ov-input: the full field look;
* .ov-choices / .ov-choice: a choice drawn as a card (radio or checkbox inside)
* .ov-choices / .ov-choice: a choice drawn as a card (radio or checkbox inside)
* opted out data-ov-native on a control or any ancestor, or <html data-ov-forms="off">
* opted out data-ov-native on a control or any ancestor, or <html data-ov-forms="off">
@@ -149,10 +151,61 @@ html.ov-sel-locked, html.ov-sel-locked body { overflow: hidden; overscroll-behav
@@ -149,10 +151,61 @@ html.ov-sel-locked, html.ov-sel-locked body { overflow: hidden; overscroll-behav
.ov-sel-group:first-child { padding-top: 4px; }
.ov-sel-group:first-child { padding-top: 4px; }
.ov-sel-none { padding: 18px 12px; color: var(--ovf-muted); text-align: center; }
.ov-sel-none { padding: 18px 12px; color: var(--ovf-muted); text-align: center; }
-/* ── Every page: the browser's own boxes, in the site's colours (nothing here changes a size) ─────────────────── */
+/* ── Every page: OpenVibe's own controls, in the site's colours ──────────────────────────────────────────────── */
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"], input[type="range"], progress, meter):where(:not([data-ov-native], [data-ov-native] *)) {
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"], input[type="range"], progress, meter):where(:not([data-ov-native], [data-ov-native] *)) {
accent-color: var(--accent, #3b82f6);
accent-color: var(--accent, #3b82f6);
}
}
+/* 4.230.0 (the owner, 11-12: "improve the selection boxes … a more customized look … themed better with animations and
+ effects for all inputs and buttons"): a checkbox is a rounded box whose check pops in, a radio a ring whose dot
+ grows; both lift their border on hover, squeeze on a press and glow with focus. The mark is the box's own ::before
+ (clip-path check, scaled), so no picture is fetched and no page's image policy is involved. */
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)) {
+ -webkit-appearance: none; appearance: none; box-sizing: border-box; flex: none; vertical-align: middle;
+ display: inline-grid; place-content: center; width: 18px; height: 18px; cursor: pointer;
+ border: 1.5px solid color-mix(in srgb, var(--text-primary, CanvasText) 38%, transparent); border-radius: 6px;
+ background: var(--bg-input, color-mix(in srgb, var(--text-primary, CanvasText) 5%, transparent)); color: var(--accent, #3b82f6);
+ transition: background-color .16s ease, border-color .16s ease, box-shadow .2s ease, transform .12s ease;
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)) { border-radius: 50%; }
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *))::before {
+ content: ""; width: 10px; height: 10px; transform: scale(0); transform-origin: 50% 60%;
+ box-shadow: inset 1em 1em var(--on-accent, #fff);
+ clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
+ transition: transform .18s cubic-bezier(.2, 1.5, .4, 1);
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *))::before {
+ width: 8px; height: 8px; border-radius: 50%; clip-path: none; box-shadow: inset 1em 1em var(--accent, #3b82f6); transform-origin: 50% 50%;
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]):checked, input[type="checkbox"]:not([role="switch"]):indeterminate):where(:not([data-ov-native], [data-ov-native] *)) {
+ background: var(--accent, #3b82f6); border-color: var(--accent, #3b82f6);
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="radio"]:checked):where(:not([data-ov-native], [data-ov-native] *)) {
+ border-color: var(--accent, #3b82f6); border-width: 2px;
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]):checked, input[type="radio"]:checked):where(:not([data-ov-native], [data-ov-native] *))::before { transform: scale(1); }
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]):indeterminate):where(:not([data-ov-native], [data-ov-native] *))::before {
+ transform: scale(1); height: 2.5px; border-radius: 2px; clip-path: none;
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):hover {
+ border-color: color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent);
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent);
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active { transform: scale(.88); }
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 28%, transparent);
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"]:not([role="switch"]), input[type="radio"]):where(:not([data-ov-native], [data-ov-native] *)):disabled { opacity: .5; cursor: not-allowed; }
+/* Buttons answer a press; text fields glow with focus (a site's own button or field rule wins over these). */
+:where(html:not([data-ov-forms="off"])) :where(button, input[type="button"], input[type="submit"], input[type="reset"], a.btn, .btn, [role="button"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)) {
+ transition: transform .12s ease, box-shadow .18s ease, background-color .16s ease, border-color .16s ease, color .16s ease;
+}
+:where(html:not([data-ov-forms="off"])) :where(button, input[type="button"], input[type="submit"], input[type="reset"], a.btn, .btn, [role="button"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active { transform: scale(.97); }
+:where(html:not([data-ov-forms="off"])) :where(input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="hidden"], [type="button"], [type="submit"], [type="reset"]), textarea, select):where(:not([data-ov-native], [data-ov-native] *)) {
+ transition: border-color .15s ease, box-shadow .2s ease, background-color .15s ease;
+}
+:where(html:not([data-ov-forms="off"])) :where(input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"], [type="color"], [type="hidden"], [type="button"], [type="submit"], [type="reset"]), textarea, select):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {
+ outline: 2px solid var(--accent, #3b82f6); outline-offset: 0; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent);
+}
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"], input[type="range"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"], input[type="radio"], input[type="range"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {
outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px;
outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px;
}
}
@@ -167,7 +220,26 @@ html.ov-sel-locked, html.ov-sel-locked body { overflow: hidden; overscroll-behav
@@ -167,7 +220,26 @@ html.ov-sel-locked, html.ov-sel-locked body { overflow: hidden; overscroll-behav
}
}
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *)) {
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *)) {
background-color: var(--accent, #3b82f6); background-position: calc(100% - 2px) 50%;
background-color: var(--accent, #3b82f6); background-position: calc(100% - 2px) 50%;
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, #000 12%, transparent), 0 0 14px color-mix(in srgb, var(--accent, #3b82f6) 38%, transparent);
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):hover {
+ background-color: color-mix(in srgb, CanvasText 34%, transparent);
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *, :disabled)):hover {
+ background-color: color-mix(in srgb, var(--accent, #3b82f6) 88%, #fff);
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active {
+ transform: none; background-image: radial-gradient(ellipse 13px 8.5px, #fff 0 92%, transparent 100%); background-size: 26px 20px;
+}
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:checked):where(:not([data-ov-native], [data-ov-native] *, :disabled)):active { background-position: calc(100% - 2px) 50%; }
+:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]):where(:not([data-ov-native], [data-ov-native] *)):focus-visible {
+ outline: none; box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 30%, transparent);
}
}
+/* Motion follows the person: Calm takes its time, Off moves nothing (the state still changes). */
+html[data-ov-motion="calm"] :where(input[type="checkbox"], input[type="radio"], button, .btn)::before { transition-duration: .32s; }
+html[data-ov-motion="off"] :where(input[type="checkbox"], input[type="radio"], button, input[type="button"], input[type="submit"], .btn, [role="button"], input, textarea, select),
+html[data-ov-motion="off"] :where(input[type="checkbox"], input[type="radio"])::before { transition: none !important; animation: none !important; }
+html[data-ov-motion="off"] :where(input[type="checkbox"], input[type="radio"], button, .btn, [role="button"]):active { transform: none !important; }
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:disabled) { opacity: .5; cursor: not-allowed; }
:where(html:not([data-ov-forms="off"])) :where(input[type="checkbox"][role="switch"]:disabled) { opacity: .5; cursor: not-allowed; }
:where(html:not([data-ov-forms="off"])) :where(input[type="file"]:not([data-ov-native]))::file-selector-button {
:where(html:not([data-ov-forms="off"])) :where(input[type="file"]:not([data-ov-native]))::file-selector-button {
font: inherit; font-weight: 600; margin-inline-end: .75em; padding: .38em .85em; cursor: pointer;
font: inherit; font-weight: 600; margin-inline-end: .75em; padding: .38em .85em; cursor: pointer;
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.229.0",
+ "version": "4.230.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.229.0",
+ "version": "4.230.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"clean-css": "^5.3.3",
"clean-css": "^5.3.3",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.229.0",
+ "version": "4.230.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,58 @@
@@ -0,0 +1,58 @@
+'use strict';
+// OpenVibe's own controls (4.230.0; the owner, 11-12: "improve the selection boxes … a more customized look … themed
+// better with animations and effects for all inputs and buttons"), in a real headless Chrome with ov-forms.css alone:
+// a checkbox and a radio are drawn (no native look) and their mark scales in when checked; a switch stays a switch; a
+// site's own rule wins (:where), data-ov-native keeps the browser's box, motion Off removes the transitions, and the
+// mark needs no picture (no url() in the stylesheet, so no page's image policy is involved). Skipped without Chrome.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const os = require('node:os');
+const path = require('node:path');
+const http = require('node:http');
+const h = require('../browser-harness');
+if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) { console.log('ov-forms-controls-chrome: skipped (no Chrome; set CHROME_BIN)'); process.exit(0); }
+const root = path.join(__dirname, '..');
+const css = fs.readFileSync(path.join(root, 'ov-forms.css'), 'utf8');
+assert.ok(!/url\(/.test(css.replace(/\/\*[\s\S]*?\*\//g, '')), 'no url() in ov-forms.css: the marks are drawn, not fetched');
+
+const page = (motion) => `<!doctype html><html lang="en"${motion ? ` data-ov-motion="${motion}"` : ''}><head><meta charset="utf-8"><link rel="stylesheet" href="/ov-forms.css">
+<style>.mine { width: 30px; height: 30px; }</style></head><body>
+<input type="checkbox" id="on" checked><input type="checkbox" id="off"><input type="radio" id="r" checked><input type="checkbox" role="switch" id="sw" checked>
+<input type="checkbox" id="mine" class="mine"><span data-ov-native><input type="checkbox" id="native"></span><button id="b">B</button><input id="t"></body></html>`;
+const server = http.createServer((req, res) => {
+ const u = new URL(req.url, 'http://x');
+ if (u.pathname === '/ov-forms.css') { res.writeHead(200, { 'content-type': 'text/css' }); return res.end(css); }
+ res.writeHead(200, { 'content-type': 'text/html' }); res.end(page(u.searchParams.get('motion')));
+});
+(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-controls-'));
+ const browser = await h.launch({ tmpDir });
+ try {
+ const p = await h.openPage(browser, await browser.newContext(), { width: 390, height: 400 });
+ await p.goto(`${base}/`, { maxMs: 5000 });
+ const cs = (id, prop, pseudo = null) => p.evaluate(`getComputedStyle(document.getElementById(${JSON.stringify(id)})${pseudo ? `, ${JSON.stringify(pseudo)}` : ''}).${prop}`);
+ assert.equal(await cs('on', 'appearance'), 'none', 'a checkbox is drawn');
+ assert.equal(await cs('on', 'width'), '18px');
+ assert.equal(await cs('on', 'transform', '::before'), 'matrix(1, 0, 0, 1, 0, 0)', 'its check is shown when checked');
+ assert.equal(await cs('off', 'transform', '::before'), 'matrix(0, 0, 0, 0, 0, 0)', 'and scaled away when not');
+ assert.ok(/transform/.test(await cs('off', 'transitionProperty', '::before')), 'the check scales in');
+ assert.equal(await cs('r', 'appearance'), 'none', 'a radio is drawn');
+ assert.equal(await cs('r', 'borderTopLeftRadius'), '50%');
+ assert.equal(await cs('sw', 'width'), '42px', 'a switch stays a switch');
+ assert.equal(await cs('sw', 'transform', '::before'), 'none', 'a switch has no check mark');
+ assert.equal(await cs('mine', 'width'), '30px', 'a site\'s own rule wins');
+ assert.notEqual(await cs('native', 'appearance'), 'none', 'data-ov-native keeps the browser\'s box');
+ assert.ok(/transform/.test(await cs('b', 'transitionProperty')), 'a button answers a press');
+ await p.goto(`${base}/?motion=off`, { maxMs: 5000 });
+ assert.equal(await cs('off', 'transitionDuration', '::before'), '0s', 'motion Off: the check does not move');
+ assert.equal(await cs('b', 'transitionDuration'), '0s', 'motion Off: buttons do not move');
+ await p.close();
+ console.log('ov-forms-controls-chrome: drawn checkbox and radio, the check scales in, switch kept, site rules win, data-ov-native, no url(), motion Off');
+ } finally {
+ await browser.close();
+ server.close();
+ fs.rmSync(tmpDir, { recursive: true, force: true });
+ }
+})().catch((e) => { console.error(e); process.exit(1); });