openvibers/openvibe.design Commits
Forms: switch rows and segmented choices, live, from the kit's own builders (#227)
Changes
@@@ -21,6 +21,18 @@.fm-row { display: flex; flex-wrap: wrap; gap: 14px 28px; padding: 20px; border-radius: 16px; border: 1px solid var(--border); background: var(--bg-secondary); }.fm-check { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; font-size: 15px; }.fm-range input { width: 160px; }++/* The switch-row / segmented demo (markup of kit 4.164; this site's pin catches up on the next round). */++.fm .ovk-set-switch { min-height: 56px; }++.fm .ovk-set-ic { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: var(--ov-radius-1); background: color-mix(in srgb, var(--ovk-accent) 12%, transparent); color: var(--ovk-accent); }++.fm .ovk-set-ic .ovk-i { width: 19px; height: 19px; }++.fm .ovk-set-row .ovk-switch { margin-left: auto; }++.fm .ovk-set-seg { flex-wrap: wrap; }++.fm .ovk-set-seg .ovk-set-text { flex: 1 1 100%; }++.fm .ovk-seg--fill { display: flex; flex: 1 1 100%; flex-wrap: nowrap; width: 100%; min-width: 0; }++.fm .ovk-seg--fill .ovk-seg-item { flex: 1 1 0; min-width: 0; }++.fm .ovk-seg--fill span { display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 38px; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }++.fm .ovk-seg--fill .ovk-i { flex: none; width: 16px; height: 16px; }++.fm #fm-rows .ovk-set-row:first-child { border-top: 0; }.fm-table-wrap { overflow-x: auto; border-radius: 14px; border: 1px solid var(--border); }.fm-table { width: 100%; border-collapse: collapse; font-size: 14px; }.fm-table th, .fm-table td { padding: 11px 14px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--border); }@@@ -1,11 +1,17 @@--// /forms: say what the first demo's select holds, so a visitor sees the hidden select follow OpenVibe's list.++// /forms: say what a demo holds, so a visitor sees the hidden control follow OpenVibe's own (a select, or a set of++// segmented radios).(function () {'use strict';document.querySelectorAll('[data-fm-out]').forEach(function (out) {-- var select = document.getElementById(out.getAttribute('data-fm-out'));++ var key = out.getAttribute('data-fm-out');++ var select = document.getElementById(key) || document.querySelector('input[name="' + key + '"]');if (!select) return;-- var say = function () { out.textContent = select.value || '(none)'; };-- select.addEventListener('change', say);++ var group = select.name ? document.querySelectorAll('input[name="' + select.name + '"]') : [select];++ var say = function () {++ var checked = select.type === 'radio' ? document.querySelector('input[name="' + select.name + '"]:checked') : select;++ out.textContent = (checked && checked.value) || '(none)';++ };++ group.forEach(function (el) { el.addEventListener('change', say); });say();});})();@@@ -22,6 +22,23 @@ const AGENT_SELECT = `<select id="fm-agent" name="agent" data-ov-title="Fallbackdata-href="#fm-attrs" data-href-label="Sign in">MiniMax Code</option></select>`;++const ROWS_SNIPPET = `// The kit's builders, server or browser (openvibe-shared/kit, docs/kit.md)++kit.switchRow({++ name: 'burn_after_read', icon: 'flame',++ title: 'Burn after reading', line: 'Gone once it is opened',++})++kit.segmented({++ name: 'visibility', value: 'public', label: 'Visibility',++ items: [++ { value: 'public', label: 'Public', icon: 'globe' },++ { value: 'unlisted', label: 'Unlisted', icon: 'link' },++ { value: 'private', label: 'Private', icon: 'lock' },++ ],++})`;++++// The demo is the kit's own output: kit.switchRow and kit.segmented (openvibe-shared, pinned here).++const VISIBILITY = [{ value: 'public', label: 'Public', icon: 'globe' }, { value: 'unlisted', label: 'Unlisted', icon: 'link' }, { value: 'private', label: 'Private', icon: 'lock' }];++const MODELS = [['OpenAI', ['gpt-5.5', 'gpt-5.5-mini', 'gpt-5.4-codex']],['Anthropic', ['claude-opus-5-5', 'claude-sonnet-5-5', 'claude-haiku-5-5']],@@@ -67,7 +84,7 @@ function formsPage({ base = 'https://openvibe.design' } = {}) {<p class="fm-eye">Forms</p><h1 class="fm-h1">Controls that look like OpenVibe, on every site</h1><p class="fm-lede">Every select is OpenVibe’s own: a menu on a computer, a sheet from the bottom on a phone. A choice can say whether it is ready, how much is used, and how to set it up. The form underneath is the browser’s, so everything that worked still works, and a page without JavaScript keeps its plain controls.</p>-- <nav class="fm-jump" aria-label="On this page"><a href="#fm-select">The select</a><a href="#fm-more">Several, search, groups</a><a href="#fm-controls">Switches and boxes</a><a href="#fm-fields">Fields and choices</a><a href="#fm-attrs">What it reads</a><a href="#fm-use">Use it</a></nav>++ <nav class="fm-jump" aria-label="On this page"><a href="#fm-select">The select</a><a href="#fm-more">Several, search, groups</a><a href="#fm-controls">Switches and boxes</a><a href="#fm-rows">Switch rows and segments</a><a href="#fm-fields">Fields and choices</a><a href="#fm-attrs">What it reads</a><a href="#fm-use">Use it</a></nav></header><section class="fm-sec" id="fm-select" aria-labelledby="fm-select-h">@@@ -102,6 +119,22 @@ function formsPage({ base = 'https://openvibe.design' } = {}) {</div></section>++<section class="fm-sec" id="fm-rows" aria-labelledby="fm-rows-h">++ <div class="fm-sec-head"><h2 id="fm-rows-h">Switch rows and segmented choices</h2><p>The kit builds the two rows a settings form is made of. A switch row is 56 px and the whole row is the target: an icon tile, a title, one line, and an iOS-style switch that is the native checkbox underneath, so it works without JavaScript. A segmented control puts three or four choices in one 44 px control, icons before the words.</p></div>++ <div class="fm-demo">++ <div class="fm-live">++ <div class="ovk-form">++ ${kit.switchRow({ name: 'fm-burn', icon: 'flame', title: 'Burn after reading', line: 'Gone once it is opened' })}++ ${kit.switchRow({ name: 'fm-nsfw', icon: 'eye-off', title: 'Mark as NSFW', line: 'Blurred until it is opened', checked: true })}++ ${kit.switchRow({ name: 'fm-lock', icon: 'lock', title: 'Lock it', line: 'OpenVibe can’t read what’s inside' })}++ ${kit.segmented({ name: 'fm-visibility', value: 'public', label: 'Visibility', items: VISIBILITY })}++ </div>++ <p class="fm-out" aria-live="polite">Chosen: <b data-fm-out="fm-visibility">public</b></p>++ </div>++ <div class="fm-code">${kit.snippet({ code: ROWS_SNIPPET, id: 'fm-rows-code', label: 'Copy' })}</div>++ </div>++</section>++<section class="fm-sec" id="fm-fields" aria-labelledby="fm-fields-h"><div class="fm-sec-head"><h2 id="fm-fields-h">Fields and choices</h2><p>Opt in with <code>.ov-field</code> for the whole field: a label, a control 44 px tall (16 px text on a phone, so it never zooms), a hint, an error. <code>.ov-choice</code> draws a choice as a card.</p></div><div class="fm-demo">
@@ -21,6 +21,18 @@
@@ -21,6 +21,18 @@
.fm-row { display: flex; flex-wrap: wrap; gap: 14px 28px; padding: 20px; border-radius: 16px; border: 1px solid var(--border); background: var(--bg-secondary); }
.fm-row { display: flex; flex-wrap: wrap; gap: 14px 28px; padding: 20px; border-radius: 16px; border: 1px solid var(--border); background: var(--bg-secondary); }
.fm-check { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; font-size: 15px; }
.fm-check { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; font-size: 15px; }
.fm-range input { width: 160px; }
.fm-range input { width: 160px; }
+/* The switch-row / segmented demo (markup of kit 4.164; this site's pin catches up on the next round). */
+.fm .ovk-set-switch { min-height: 56px; }
+.fm .ovk-set-ic { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: var(--ov-radius-1); background: color-mix(in srgb, var(--ovk-accent) 12%, transparent); color: var(--ovk-accent); }
+.fm .ovk-set-ic .ovk-i { width: 19px; height: 19px; }
+.fm .ovk-set-row .ovk-switch { margin-left: auto; }
+.fm .ovk-set-seg { flex-wrap: wrap; }
+.fm .ovk-set-seg .ovk-set-text { flex: 1 1 100%; }
+.fm .ovk-seg--fill { display: flex; flex: 1 1 100%; flex-wrap: nowrap; width: 100%; min-width: 0; }
+.fm .ovk-seg--fill .ovk-seg-item { flex: 1 1 0; min-width: 0; }
+.fm .ovk-seg--fill span { display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 38px; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.fm .ovk-seg--fill .ovk-i { flex: none; width: 16px; height: 16px; }
+.fm #fm-rows .ovk-set-row:first-child { border-top: 0; }
.fm-table-wrap { overflow-x: auto; border-radius: 14px; border: 1px solid var(--border); }
.fm-table-wrap { overflow-x: auto; border-radius: 14px; border: 1px solid var(--border); }
.fm-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.fm-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.fm-table th, .fm-table td { padding: 11px 14px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--border); }
.fm-table th, .fm-table td { padding: 11px 14px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--border); }
@@ -1,11 +1,17 @@
@@ -1,11 +1,17 @@
-// /forms: say what the first demo's select holds, so a visitor sees the hidden select follow OpenVibe's list.
+// /forms: say what a demo holds, so a visitor sees the hidden control follow OpenVibe's own (a select, or a set of
+// segmented radios).
(function () {
(function () {
'use strict';
'use strict';
document.querySelectorAll('[data-fm-out]').forEach(function (out) {
document.querySelectorAll('[data-fm-out]').forEach(function (out) {
- var select = document.getElementById(out.getAttribute('data-fm-out'));
+ var key = out.getAttribute('data-fm-out');
+ var select = document.getElementById(key) || document.querySelector('input[name="' + key + '"]');
if (!select) return;
if (!select) return;
- var say = function () { out.textContent = select.value || '(none)'; };
- select.addEventListener('change', say);
+ var group = select.name ? document.querySelectorAll('input[name="' + select.name + '"]') : [select];
+ var say = function () {
+ var checked = select.type === 'radio' ? document.querySelector('input[name="' + select.name + '"]:checked') : select;
+ out.textContent = (checked && checked.value) || '(none)';
+ };
+ group.forEach(function (el) { el.addEventListener('change', say); });
say();
say();
});
});
})();
})();
@@ -22,6 +22,23 @@ const AGENT_SELECT = `<select id="fm-agent" name="agent" data-ov-title="Fallback
@@ -22,6 +22,23 @@ const AGENT_SELECT = `<select id="fm-agent" name="agent" data-ov-title="Fallback
data-href="#fm-attrs" data-href-label="Sign in">MiniMax Code</option>
data-href="#fm-attrs" data-href-label="Sign in">MiniMax Code</option>
</select>`;
</select>`;
+const ROWS_SNIPPET = `// The kit's builders, server or browser (openvibe-shared/kit, docs/kit.md)
+kit.switchRow({
+ name: 'burn_after_read', icon: 'flame',
+ title: 'Burn after reading', line: 'Gone once it is opened',
+})
+kit.segmented({
+ name: 'visibility', value: 'public', label: 'Visibility',
+ items: [
+ { value: 'public', label: 'Public', icon: 'globe' },
+ { value: 'unlisted', label: 'Unlisted', icon: 'link' },
+ { value: 'private', label: 'Private', icon: 'lock' },
+ ],
+})`;
+
+// The demo is the kit's own output: kit.switchRow and kit.segmented (openvibe-shared, pinned here).
+const VISIBILITY = [{ value: 'public', label: 'Public', icon: 'globe' }, { value: 'unlisted', label: 'Unlisted', icon: 'link' }, { value: 'private', label: 'Private', icon: 'lock' }];
+
const MODELS = [
const MODELS = [
['OpenAI', ['gpt-5.5', 'gpt-5.5-mini', 'gpt-5.4-codex']],
['OpenAI', ['gpt-5.5', 'gpt-5.5-mini', 'gpt-5.4-codex']],
['Anthropic', ['claude-opus-5-5', 'claude-sonnet-5-5', 'claude-haiku-5-5']],
['Anthropic', ['claude-opus-5-5', 'claude-sonnet-5-5', 'claude-haiku-5-5']],
@@ -67,7 +84,7 @@ function formsPage({ base = 'https://openvibe.design' } = {}) {
@@ -67,7 +84,7 @@ function formsPage({ base = 'https://openvibe.design' } = {}) {
<p class="fm-eye">Forms</p>
<p class="fm-eye">Forms</p>
<h1 class="fm-h1">Controls that look like OpenVibe, on every site</h1>
<h1 class="fm-h1">Controls that look like OpenVibe, on every site</h1>
<p class="fm-lede">Every select is OpenVibe’s own: a menu on a computer, a sheet from the bottom on a phone. A choice can say whether it is ready, how much is used, and how to set it up. The form underneath is the browser’s, so everything that worked still works, and a page without JavaScript keeps its plain controls.</p>
<p class="fm-lede">Every select is OpenVibe’s own: a menu on a computer, a sheet from the bottom on a phone. A choice can say whether it is ready, how much is used, and how to set it up. The form underneath is the browser’s, so everything that worked still works, and a page without JavaScript keeps its plain controls.</p>
- <nav class="fm-jump" aria-label="On this page"><a href="#fm-select">The select</a><a href="#fm-more">Several, search, groups</a><a href="#fm-controls">Switches and boxes</a><a href="#fm-fields">Fields and choices</a><a href="#fm-attrs">What it reads</a><a href="#fm-use">Use it</a></nav>
+ <nav class="fm-jump" aria-label="On this page"><a href="#fm-select">The select</a><a href="#fm-more">Several, search, groups</a><a href="#fm-controls">Switches and boxes</a><a href="#fm-rows">Switch rows and segments</a><a href="#fm-fields">Fields and choices</a><a href="#fm-attrs">What it reads</a><a href="#fm-use">Use it</a></nav>
</header>
</header>
<section class="fm-sec" id="fm-select" aria-labelledby="fm-select-h">
<section class="fm-sec" id="fm-select" aria-labelledby="fm-select-h">
@@ -102,6 +119,22 @@ function formsPage({ base = 'https://openvibe.design' } = {}) {
@@ -102,6 +119,22 @@ function formsPage({ base = 'https://openvibe.design' } = {}) {
</div>
</div>
</section>
</section>
+<section class="fm-sec" id="fm-rows" aria-labelledby="fm-rows-h">
+ <div class="fm-sec-head"><h2 id="fm-rows-h">Switch rows and segmented choices</h2><p>The kit builds the two rows a settings form is made of. A switch row is 56 px and the whole row is the target: an icon tile, a title, one line, and an iOS-style switch that is the native checkbox underneath, so it works without JavaScript. A segmented control puts three or four choices in one 44 px control, icons before the words.</p></div>
+ <div class="fm-demo">
+ <div class="fm-live">
+ <div class="ovk-form">
+ ${kit.switchRow({ name: 'fm-burn', icon: 'flame', title: 'Burn after reading', line: 'Gone once it is opened' })}
+ ${kit.switchRow({ name: 'fm-nsfw', icon: 'eye-off', title: 'Mark as NSFW', line: 'Blurred until it is opened', checked: true })}
+ ${kit.switchRow({ name: 'fm-lock', icon: 'lock', title: 'Lock it', line: 'OpenVibe can’t read what’s inside' })}
+ ${kit.segmented({ name: 'fm-visibility', value: 'public', label: 'Visibility', items: VISIBILITY })}
+ </div>
+ <p class="fm-out" aria-live="polite">Chosen: <b data-fm-out="fm-visibility">public</b></p>
+ </div>
+ <div class="fm-code">${kit.snippet({ code: ROWS_SNIPPET, id: 'fm-rows-code', label: 'Copy' })}</div>
+ </div>
+</section>
+
<section class="fm-sec" id="fm-fields" aria-labelledby="fm-fields-h">
<section class="fm-sec" id="fm-fields" aria-labelledby="fm-fields-h">
<div class="fm-sec-head"><h2 id="fm-fields-h">Fields and choices</h2><p>Opt in with <code>.ov-field</code> for the whole field: a label, a control 44 px tall (16 px text on a phone, so it never zooms), a hint, an error. <code>.ov-choice</code> draws a choice as a card.</p></div>
<div class="fm-sec-head"><h2 id="fm-fields-h">Fields and choices</h2><p>Opt in with <code>.ov-field</code> for the whole field: a label, a control 44 px tall (16 px text on a phone, so it never zooms), a hint, an error. <code>.ov-choice</code> draws a choice as a card.</p></div>
<div class="fm-demo">
<div class="fm-demo">