openvibers/openvibe.design Commits

Forms: switch rows and segmented choices, live, from the kit's own builders (#227)

OP OpenVibers committed 4 days ago · fab025aa0f · 3 files, +56 −5

Recovered from an uncommitted worktree (design/forms-switch-segmented, 2026-11-09) whose job ran out of quota.
The demo now calls kit.switchRow and kit.segmented (the pinned Shared has them), so the page shows exactly what
sites get; the hand-drawn copies and stand-in icons are gone.

Changes

ddiff --git a/public/css/forms.css b/public/css/forms.css
iindex d7fa5ef..0728d4e 100644
---- a/public/css/forms.css
++++ b/public/css/forms.css
@@@ -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); }
ddiff --git a/public/js/forms-page.js b/public/js/forms-page.js
iindex 41d455b..3e90e7f 100644
---- a/public/js/forms-page.js
++++ b/public/js/forms-page.js
@@@ -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();
});
})();
ddiff --git a/server/views/forms.js b/server/views/forms.js
iindex fb959f8..a594a0b 100644
---- a/server/views/forms.js
++++ b/server/views/forms.js
@@@ -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>
</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">