openvibers/openvibe.shared Commits

Arrange: Stop is an ordinary optional part (#501)

OP OpenVibers committed 21 h ago · d2af0d5a05 · 5 files, +39 −9

* Arrange: Stop docks like any other optional part

* Refresh SIBLING_REV

* Merge origin/main into plain-stop

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 1fbd34e..e787e0b 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,9 @@
# Changelog
++## Unreleased — a plain Stop
++
++- `ov-arrange.js`: a Stop part docks right like any other optional part; no special E-STOP size or position.
++
## 4.223.1 — 2026-11-12
The menu keeps its search field and tabs (the owner, 11-12, two phone screenshots: "the gui gets messed up and loses
ddiff --git a/navbar.js b/navbar.js
iindex deff4f2..d1201d2 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -604,7 +604,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': 'b5f1f966bae3', 'dock.js': '1fb16839cb0e', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4086b813ffda', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': '6215643437c0', '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': '898af091b3e1', 'soft.js': '98ab9b8080b1' };
++ 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': 'b5f1f966bae3', 'dock.js': '1fb16839cb0e', 'presence.js': '5f195d41f8c5', 'panels.js': 'd90bfe987d81', 'surfaces.js': 'a94e76bd1200', 'notification-ui.js': '4086b813ffda', 'history.js': 'f9fb9e096c65', 'bottom.js': '1671b6949137', 'appbar.js': 'c21485176ae9', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'aaa688bf6fc7', 'sso-client.js': 'f574ac3331a5', 'overlay.js': '2bc56b3cf726', 'ov-home.js': '6215643437c0', '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': 'c8da956d1e28', 'soft.js': '98ab9b8080b1' };
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.
ddiff --git a/ov-arrange.js b/ov-arrange.js
iindex 3eda528..c2a4f54 100644
---- a/ov-arrange.js
++++ b/ov-arrange.js
@@@ -219,8 +219,8 @@ html[data-ov-motion="reduced"] { --ov-arrange-dur: .12s; }
const st = { tint: (p.styles && p.styles.tints && p.styles.tints[0]) || (catalog.theme_tokens || ['surface'])[0], shape: (p.styles && p.styles.shapes && p.styles.shapes[0]) || 'square', opacity: 1, label: true };
const b = { keys: (p.bindings && p.bindings.keys || []).slice(0, 0), gamepad: [] };
if (overlay) {
-- const dock = p.id === 'pill' || p.id === 'stick' ? 'left' : p.id === 'slider' || p.id === 'estop' ? 'bottom' : 'right';
-- return { id: p.id, visible: true, place: { dock, order: 0, w: Math.max(p.min.w, p.id === 'stick' ? 2 : p.id === 'estop' ? 2 : 1), h: Math.max(p.min.h, p.id === 'stick' ? 2 : p.id === 'estop' ? 2 : 1) }, style: st, bindings: b };
++ const dock = p.id === 'pill' || p.id === 'stick' ? 'left' : p.id === 'slider' ? 'bottom' : 'right';
++ return { id: p.id, visible: true, place: { dock, order: 0, w: Math.max(p.min.w, p.id === 'stick' ? 2 : 1), h: Math.max(p.min.h, p.id === 'stick' ? 2 : 1) }, style: st, bindings: b };
}
return { id: p.id, visible: true, place: { x: 0, y: 0, w: Math.min(spanFor({ ...p, _cols: cols }, device).w, cols), h: Math.max(p.min.h, 2) }, style: st, bindings: b };
});
ddiff --git a/test/ov-arrange-core.test.js b/test/ov-arrange-core.test.js
iindex 5cf2212..c58d312 100644
---- a/test/ov-arrange-core.test.js
++++ b/test/ov-arrange-core.test.js
@@@ -27,20 +27,20 @@ const catalog = {
grid: { columns: { phone: 4, tablet: 8, computer: 12 }, cell_px: { phone: 64, tablet: 64, computer: 64 } },
theme_tokens: ['danger', 'surface', 'accent'],
parts: [
-- { id: 'estop', name: 'E-stop', kind: 'button', required: true, pressed: true, min: { w: 2, h: 2, px: 56 }, styles: { tints: ['danger'], shapes: ['round'], label: true }, bindings: { keys: [], gamepad: [] } },
++ { id: 'stop', name: 'Stop', kind: 'button', required: true, pressed: true, min: { w: 2, h: 2, px: 56 }, styles: { tints: ['danger'], shapes: ['round'], label: true }, bindings: { keys: [], gamepad: [] } },
{ id: 'stick', name: 'Stick', kind: 'stick', required: false, pressed: true, min: { w: 2, h: 2 }, styles: { tints: ['surface'], shapes: ['round'], label: false }, bindings: { keys: ['W'], gamepad: [] } },
],
};
--const part = (id, place) => ({ id, visible: true, place, style: { tint: id === 'estop' ? 'danger' : 'surface', shape: 'round', opacity: 1, label: id === 'estop' }, bindings: { keys: [], gamepad: [] } });
--const layout = (estop = { dock: 'right', order: 0, w: 2, h: 2 }) => ({
++const part = (id, place) => ({ id, visible: true, place, style: { tint: id === 'stop' ? 'danger' : 'surface', shape: 'round', opacity: 1, label: id === 'stop' }, bindings: { keys: [], gamepad: [] } });
++const layout = (stop = { dock: 'right', order: 0, w: 2, h: 2 }) => ({
surface: 'bot.overlay', surface_version: 1, device: 'phone', name: 'Mine', author: { name: 'Ana' }, sharing: 'private',
-- parts: [part('estop', estop), part('stick', { dock: 'left', order: 0, w: 2, h: 2 })],
++ parts: [part('stop', stop), part('stick', { dock: 'left', order: 0, w: 2, h: 2 })],
});
assert.deepEqual(core.validate(catalog, layout()), { ok: true, reasons: [] });
--assert.ok(core.validate(catalog, layout({ dock: 'right', order: 0, w: 1, h: 1 })).reasons.includes('E-stop has to be at least 2 × 2 cells.'));
++assert.ok(core.validate(catalog, layout({ dock: 'right', order: 0, w: 1, h: 1 })).reasons.includes('Stop has to be at least 2 × 2 cells.'));
const covered = layout();
covered.parts[1].place = { dock: 'right', order: 0, w: 2, h: 2 };
--assert.ok(core.validate(catalog, covered).reasons.some((r) => /Stick covers E-stop/.test(r)));
++assert.ok(core.validate(catalog, covered).reasons.some((r) => /Stick covers Stop/.test(r)));
const defaults = core.applyDefaults(catalog, 'phone');
assert.deepEqual(core.validate(catalog, defaults), { ok: true, reasons: [] }, 'the SDK defaults are valid');
assert.equal(core.merge(layout(), { parts: [{ id: 'stick', style: { opacity: 0.5 } }] }).parts[1].style.opacity, 0.5);
ddiff --git a/test/plain-stop-text.test.js b/test/plain-stop-text.test.js
nnew file mode 100644
iindex 0000000..6fe4a23
---- /dev/null
++++ b/test/plain-stop-text.test.js
@@@ -0,0 +1,26 @@
++'use strict';
++// These are small hobby robots: no page, doc or AGENTS.md says E-STOP, estop or emergency stop. Inline code
++// names a legacy protocol field and is skipped.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const root = path.join(__dirname, '..');
++const files = ['AGENTS.md', 'README.md', 'STATUS.json'].map((f) => path.join(root, f)).filter((f) => fs.existsSync(f));
++const walk = (dir, ext) => {
++ if (!fs.existsSync(dir)) return;
++ for (const name of fs.readdirSync(dir)) {
++ const p = path.join(dir, name);
++ if (fs.statSync(p).isDirectory()) walk(p, ext);
++ else if (p.endsWith(ext)) files.push(p);
++ }
++};
++walk(path.join(root, 'docs'), '.md');
++walk(path.join(root, 'public', 'fragments'), '.html');
++const bad = /(^|[^A-Za-z])e-?stop([^A-Za-z]|$)|emergency\s+stop/i;
++for (const file of files) {
++ let text = fs.readFileSync(file, 'utf8');
++ text = text.replace(/```[\s\S]*?```/g, '').replace(/`[^`]*`/g, '')
++ .replace(/<pre[\s\S]*?<\/pre>/gi, '').replace(/<code[\s\S]*?<\/code>/gi, '');
++ assert.ok(!bad.test(text), `${path.relative(root, file)} says E-STOP; write Stop`);
++}
++console.log('plain-stop-text: no page, doc or AGENTS.md says E-STOP');