openvibers/openvibe.shared Commits
Arrange: Stop is an ordinary optional part (#501)
Changes
@@@ -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-12The menu keeps its search field and tabs (the owner, 11-12, two phone screenshots: "the gui gets messed up and loses@@@ -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.@@@ -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 };});@@@ -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);nnew file mode 100644@@@ -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');
@@ -1,5 +1,9 @@
@@ -1,5 +1,9 @@
# Changelog
# 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
## 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
The menu keeps its search field and tabs (the owner, 11-12, two phone screenshots: "the gui gets messed up and loses
@@ -604,7 +604,7 @@
@@ -604,7 +604,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': '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]}` : ''}`;
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.
@@ -219,8 +219,8 @@ html[data-ov-motion="reduced"] { --ov-arrange-dur: .12s; }
@@ -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 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: [] };
const b = { keys: (p.bindings && p.bindings.keys || []).slice(0, 0), gamepad: [] };
if (overlay) {
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 };
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 };
});
});
@@ -27,20 +27,20 @@ const catalog = {
@@ -27,20 +27,20 @@ const catalog = {
grid: { columns: { phone: 4, tablet: 8, computer: 12 }, cell_px: { phone: 64, tablet: 64, computer: 64 } },
grid: { columns: { phone: 4, tablet: 8, computer: 12 }, cell_px: { phone: 64, tablet: 64, computer: 64 } },
theme_tokens: ['danger', 'surface', 'accent'],
theme_tokens: ['danger', 'surface', 'accent'],
parts: [
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: [] } },
{ 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',
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.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();
const covered = layout();
covered.parts[1].place = { dock: 'right', order: 0, w: 2, h: 2 };
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');
const defaults = core.applyDefaults(catalog, 'phone');
assert.deepEqual(core.validate(catalog, defaults), { ok: true, reasons: [] }, 'the SDK defaults are valid');
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);
assert.equal(core.merge(layout(), { parts: [{ id: 'stick', style: { opacity: 0.5 } }] }).parts[1].style.opacity, 0.5);
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -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');