openvibers/openvibe.design Commits

openvibe-shared v4.229.0 (was v4.228.0) (#308)

OP OpenVibers committed 14 h ago · b3608cbd9c · 8 files, +72 −9

* openvibe-shared v4.229.0 (was v4.228.0)

Opened by scripts/pin-bump.js in OpenVibers/OpenVibe.Shared after the v4.229.0 release.

* Rules 153-156 and the search field: menu addresses, one search field, a robot's manifest, a planned restart; /components/search-field searches Design's own components, rules and themes (GET /api/v1/suggest)

* Rule 157 and the Form controls page: every checkbox, radio, switch, field and button is OpenVibe's own (ov-forms.css, Shared 4.230.0)

* Components: the search field and the form controls in the registry's Writing and choosing

* Rule 158: an agent is a blueprint anyone can read, change, share and run anywhere

Ten parts, each fed by the product that knows it best; it asks and never grants; Vibe is one too.

Changes

ddiff --git a/README.md b/README.md
iindex 3dbc5c1..bb2ef69 100644
---- a/README.md
++++ b/README.md
@@@ -234,6 +234,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by
<!-- versions:start -->
- openvibe-contracts: v0.417.0
- openvibe-sdk: v0.161.0
--- openvibe-shared: v4.228.0
++- openvibe-shared: v4.229.0
- openvibe-publishing: v1.7.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 4b686d7..4d3f6af 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -10,12 +10,12 @@
"domain": "openvibe.design",
"contracts": "openvibe-contracts v0.417.0",
"sdk": "openvibe-sdk v0.161.0",
-- "shared": "openvibe-shared v4.228.0",
++ "shared": "openvibe-shared v4.229.0",
"packages": [
"openvibe-contracts v0.417.0",
"openvibe-publishing v1.7.0",
"openvibe-sdk v0.161.0",
-- "openvibe-shared v4.228.0"
++ "openvibe-shared v4.229.0"
],
"summary": "The service skeleton for openvibe.design: Network sign-in as OAuth client `design` (PKCE S256, httpOnly design_at/design_rt cookies) and a theme API proxy to OpenVibe.Network's /api/themes. The proxy serves the public catalogue (cached 5 minutes), one theme, the person's active theme and custom variables, saving a theme with source 'studio', its history and a restore by id, and a submission to the gallery. Writes need this site's own Origin; Network 4xx answers pass through and a Network failure is 502. The studio is designed: a gallery, previews on a Live channel, a Chat room and a shop's grid (with colour-vision views), a six-colour editor that derives the full token set, WCAG checks that gate Use, sharing to the gallery, and History with Restore. The Arena queues one prompt for three models through OpenVibe.AI (client credentials, ai.run.create), stores sanitized HTML with reported model/provider/cost/time/tokens, renders it only in sandboxed CSP-capped iframes, draws blind pairs, records one vote per person per round, and keeps Elo ratings per category plus median cost and time. The prompts, outputs and votes are an open JSONL dataset; rounds and votes follow ADR-033 export and deletion.",
"works": [
ddiff --git a/package-lock.json b/package-lock.json
iindex f17df21..68c3b04 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -19,7 +19,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.229.0",
"pg": "^8.23.1",
"subset-font": "^2.9.0",
"wawoff2": "^2.0.1"
@@@ -1476,9 +1476,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.228.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",
-- "integrity": "sha512-HR0Hac/K+qKmwMf2QZWCFxW+uVja36nyAtxVbNFgPGUDFdmWhmb6RMoP/CkU0+GSCPsC50k6aTS+oPx2czDgGg==",
++ "version": "4.229.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.229.0",
++ "integrity": "sha512-6QvmSc2eZa5Fos0e9kh4jDjx+bZ3gx+mI7P33dOUox34D7SrQ9BYmij0XTpex9+TTXfDbEUwkIlTz3SdetDSTg==",
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
ddiff --git a/package.json b/package.json
iindex 117e263..d85afe8 100644
---- a/package.json
++++ b/package.json
@@@ -25,7 +25,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.417.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.7.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.161.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.228.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.229.0",
"pg": "^8.23.1",
"subset-font": "^2.9.0",
"wawoff2": "^2.0.1"
ddiff --git a/server/components/registry.js b/server/components/registry.js
iindex b5f3b35..663f610 100644
---- a/server/components/registry.js
++++ b/server/components/registry.js
@@@ -23,6 +23,8 @@ const COMPONENTS = Object.freeze([
define('you-home', 'You home', 'The personal starting view inside the menu.', [['identity', 'avatar and name'], ['shortcuts', 'your places'], ['settings', 'display and account']], { person: 'person?', shortcuts: 'link[]' }, ['signed-in', 'signed-out', 'loading'], ['color.bg.raised', 'color.text.secondary', 'size.tap'], { keyboardAware: true }, ['navbar', 'Menu You panel'], ['SurfaceContainerHigh', 'Use a Column of ListItem shortcuts under the account identity.'], 'Use a List with account identity and destination sections.', { person: 'You' }),
define('dock', 'Dock', 'Persistent place for the current task, clear of the phone app bar and keyboard.', [['surface', 'bottom container'], ['content', 'current task'], ['action', 'send or confirm']], { content: 'string', action: 'action', busy: 'boolean' }, ['ready', 'busy', 'disabled'], ['color.bg.raised', 'color.line.strong', 'size.tap'], { keyboardAware: true, accessibility: { focus: 'Keep the action after the content in focus order' } }, ['kit', 'kit.dock({ action, name, placeholder, label })'], ['SurfaceContainer', 'Use Scaffold.bottomBar and imePadding; keep the primary action in a 48 dp control.'], 'Use safeAreaInset(edge: .bottom) with keyboard avoidance.', { content: 'Write a message', action: 'Send' }),
define('dock-tabs', 'Dock tabs', 'The phone destinations attached to the bottom app bar.', [['tab', 'icon and label'], ['selection', 'active destination']], { items: 'link[]', active: 'string' }, ['default', 'selected', 'pressed'], ['color.accent.strong', 'color.text.muted', 'size.tap'], { keyboardAware: true, accessibility: { name: 'Each tab announces its label and selected state' } }, ['appbar', 'OpenVibeAppbar from the site navigation'], ['SurfaceContainer', 'Reference: OvTabs in OpenVibe.Link\'s Android app (android/app/src/main/java/network/openvibe/kit/OvKit.kt). Use NavigationBar with NavigationBarItem and selected state.'], 'Use TabView with system tab items.', { active: 'Studio' }),
++ define('form-controls', 'Form controls', 'OpenVibe\'s own checkbox, radio, switch, field and button on every site: drawn by the system, in the site\'s colours, with motion that follows the person.', [['box', 'a checkbox: a rounded box whose check pops in'], ['ring', 'a radio: a ring whose dot grows'], ['switch', 'role="switch": a track that glows when on'], ['field', 'a text field that glows with focus'], ['button', 'a button that answers a press']], { checked: 'boolean', indeterminate: 'boolean', disabled: 'boolean', role: '"switch"?' }, ['off', 'on', 'mixed', 'hover', 'pressed', 'focused', 'disabled'], ['color.accent', 'color.bg.raised', 'size.tap'], { accessibility: { focus: 'Every control keeps a visible focus glow in the accent; a switch announces itself as a switch' } }, ['ov-forms', '<input type="checkbox" role="switch"> (ov-forms.css draws every box, radio, switch, field and button; data-ov-native keeps the browser\'s)'], ['Checkbox / RadioButton / Switch', 'Use Material 3 Checkbox, RadioButton and Switch with the theme\'s primary colour; animate the state change.'], 'Use Toggle with .switch or .checkbox style and the accent tint; keep the default state animation.', { checked: true }),
++ define('search-field', 'Search field', 'One search field for every site: a pill with suggestions, recent searches and a ghost completion.', [['mark', 'the search glyph'], ['field', 'the words'], ['ghost', 'the rest of the first suggestion'], ['clear', 'empties the field'], ['key', '"/" on a computer'], ['list', 'suggestions, and recent searches while empty']], { action: 'url', name: 'string', source: 'url', examples: 'string[]', recent: 'boolean' }, ['idle', 'focused', 'typing', 'suggesting', 'empty'], ['color.bg.raised', 'color.accent', 'size.tap'], { keyboardAware: true, outsideTapCloses: true, accessibility: { focus: '"/" puts the cursor in it; the arrows move in the list; Tab or → takes the ghost; Escape closes the list, then clears' } }, ['ov-searchbox', '<input name="q" data-ov-searchbox="/api/v1/suggest" data-ov-examples="streams|clips">'], ['SearchBar', 'Use SearchBar with SearchBarDefaults.InputField; suggestions in the expanded view and recent searches while it is empty.'], 'Use .searchable with searchSuggestions; recent searches as suggestions while the field is empty.', { examples: ['components', 'rules', 'themes'] }),
define('thread-composer', 'Thread composer', 'A multiline message field with Options, attachments and send or stop.', [['field', 'message text'], ['options', 'model and attachment choices'], ['send', 'submit or stop']], { action: 'url', placeholder: 'string', options: 'action[]', attach: 'boolean', busy: 'boolean' }, ['empty', 'composing', 'uploading', 'busy'], ['color.bg.raised', 'color.text.primary', 'size.tap'], { keyboardAware: true, accessibility: { focus: 'Enter sends when the field permits; Shift+Enter adds a line; Options has a spoken label' } }, ['thread', 'thread.composer({ action, placeholder, attach })'], ['SurfaceContainerHigh', 'Use an OutlinedTextField, Options icon button and send or stop button; imePadding on the dock.'], 'Use TextEditor with an Options Menu and send or stop Button above the keyboard.', { placeholder: 'Ask anything', attach: true }),
define('chat', 'Chat', 'A conversation: messages as room lines or bubbles, each with its actions in one cluster at its corner, and the box that sends, replies and edits.', [['messages', 'room lines or bubbles, grouped by person, with their time'], ['actions', 'reply, edit, translate, copy: one cluster at a message\'s corner, on hover, focus or a tap'], ['edited', 'one quiet word after the words, the time as its title'], ['bar', '"Replying to …" or "Editing your message" above the box'], ['composer', 'the chat preset: attach, emoji, voice and a round send']], { messages: 'message[]', view: '"room" | "bubbles"', me: 'id', actions: 'id[] | (message, { mine }) => id[]', onAction: 'function' }, ['default', 'hover', 'acting', 'replying', 'editing', 'edited'], ['color.bg.raised', 'color.text.muted', 'color.accent', 'size.tap'], { keyboardAware: true, outsideTapCloses: true, accessibility: { focus: 'Each action is a labelled button reached by Tab; Esc stops a reply or an edit' } }, ['messages', "OVMessages.render(list, { messages, view, me, actions, onAction }); OpenVibeComposer.mount(box, { preset: 'chat' }) with .reply() and .edit()"], ['SurfaceContainerHigh', 'A LazyColumn of messages; a long press opens the actions as a menu anchored at the message; the composer sits on the bottom bar with a reply or edit strip above it (imePadding).'], 'A List of messages with a contextMenu for the actions; the composer above the keyboard with a reply or edit strip.', { view: 'room', actions: ['reply', 'edit', 'copy'] }),
define('sheet', 'Sheet', 'A modal task surface: one heading, one close action and scrollable content.', [['handle', 'touch affordance'], ['header', 'title and close'], ['body', 'task controls']], { title: 'string', open: 'boolean', body: 'content' }, ['closed', 'opening', 'open', 'closing'], ['color.bg.raised', 'color.line.strong', 'radius.lg'], { keyboardAware: true, outsideTapCloses: true, scrollLock: true, accessibility: { focus: 'Trap focus while open; Escape closes and returns focus' } }, ['kit', 'kit.sheet({ id, title, body })'], ['SurfaceContainerHigh', 'Use ModalBottomSheet with a heading, close control and sheet state.'], 'Use .sheet with a NavigationStack and a toolbar close button.', { title: 'Add a site' }),
ddiff --git a/server/http/api.js b/server/http/api.js
iindex cf369f4..11c5d26 100644
---- a/server/http/api.js
++++ b/server/http/api.js
@@@ -135,6 +135,32 @@ function createApi(ctx) {
llms: 'https://openvibe.design/llms.txt',
});
});
++ // ── Suggestions for the one search field (ov-searchbox): components, rules and themes whose names match ──
++ r.get('/v1/suggest', async (req, res) => {
++ const q = String(req.query.q || '').trim().toLowerCase().slice(0, 80);
++ const limit = Math.max(1, Math.min(12, Number(req.query.limit) || 8));
++ res.set('Cache-Control', 'public, max-age=300');
++ if (!q) return res.json({ sections: [] });
++ const hit = (...words) => words.some((w) => String(w || '').toLowerCase().includes(q));
++ const starts = (w) => (String(w || '').toLowerCase().startsWith(q) ? 0 : 1);
++ const parts = legacyRegistry.COMPONENTS.filter((c) => hit(c.title, c.name, c.summary))
++ .sort((a, b) => starts(a.title) - starts(b.title)).slice(0, limit)
++ .map((c) => ({ label: c.title, url: `/components/${c.name}`, sub: c.summary.slice(0, 90), icon: 'page' }));
++ const { RULES } = require('../rules/register');
++ const rules = RULES.filter((x) => hit(x.rule, x.id)).slice(-limit).reverse()
++ .map((x) => ({ label: x.rule.length > 70 ? `${x.rule.slice(0, 68)}…` : x.rule, url: `/rules#${x.id}`, sub: `Rule ${x.n} · ${x.decided}`, icon: 'page' }));
++ let themes = [];
++ try {
++ const list = await catalog.list();
++ themes = ((list && list.themes) || []).filter((t) => hit(t.name, t.slug)).slice(0, 4)
++ .map((t) => ({ label: t.name, url: `/themes/${encodeURIComponent(t.slug)}`, sub: 'Theme', icon: 'place' }));
++ } catch { /* the themes catalogue is away: components and rules still answer */ }
++ return res.json({ sections: [
++ { id: 'components', label: 'Components', items: parts },
++ { id: 'rules', label: 'Rules', items: rules.slice(0, Math.max(2, limit - parts.length)) },
++ { id: 'themes', label: 'Themes', items: themes },
++ ].filter((s) => s.items.length) });
++ });
r.get('/v1/components/:name', async (req, res) => {
const entry = await registry.get(req.params.name);
if (entry) {
ddiff --git a/server/rules/register.js b/server/rules/register.js
iindex 74ad3f7..9f24d52 100644
---- a/server/rules/register.js
++++ b/server/rules/register.js
@@@ -565,6 +565,30 @@ const RULES = Object.freeze([
"Every composer takes a picture with no words, as any social app does. A picture's description is asked for, never required: ✨ on the picture writes a caption and alt text with AI on the person's own allowance (one tap, never on its own), and a post sent without a description gets one written as it goes when it can.",
'2026-11-12', "The owner, 11-12, on a comment refused for having only a picture: \"it shouldn't require you to type text if you just put a picture … like other social media … auto caption using OpenVibe.Exchange\".",
[{ label: 'The composer', href: '/components/thread-composer' }], ['openvibe-shared test: composer-caption-chrome', 'OpenVibe.Community test: comments']),
++ R("any-button-opens-a-place-in-the-menu", "navigation", "Any button, link or address can open a place in the menu.",
++ "A place in the menu has an address: a place, or place/part (you/notifications, you/apps, feed, here/live, or a door like vibe). An element with data-ov-menu=\"you/notifications\" opens it on a click on any page with the bar, a page address ending #ov-menu=you/notifications opens it as the page loads, and OpenVibeNavbar.openMenu takes the same words. A link that has an address still opens elsewhere with a modifier key, and the address leaves the page's own once used.",
++ '2026-11-12', "The owner, 11-12: \"other tools and buttons can open specific areas of the menu\".",
++ [{ label: 'The menu', href: '/components/menu' }], ['openvibe-shared test: menu-address-chrome']),
++ R("one-search-field-everywhere", "building", "Every site's search is the one search field, with suggestions, recent searches and a ghost completion.",
++ "A site's search box is openvibe-shared's ov-searchbox: a pill with the search mark, a clear button and a \"/\" key on a computer, its edge glowing with focus. Its placeholder names examples in turn while idle; as you type it suggests from the site, with the first suggestion's rest as a ghost that Tab or → takes; while empty it shows your recent searches here. Enter searches the words, the arrows choose a row, Escape closes then clears. Its list is the one dropdown every field uses.",
++ '2026-11-12', "The owner, 11-12: \"improve the search field … autocomplete, dropdown, animated effects … a shared, universal, reusable search input component across all sites\".",
++ [{ label: 'The search field', href: '/components/search-field' }], ['openvibe-shared test: ov-searchbox-chrome']),
++ R("a-robot-declares-its-controls", "products", "A robot says what it can do in a manifest its maker versions; the panel and the AI read only that.",
++ "A robot's driver publishes a manifest (bot.robot-manifest@1): each command's kind, class, ranges, lifetime, delivery window, what it does when released and how often it may be sent, and each reading's fields and source. The panel draws a control for each, people arrange them, and OpenVibe AI reads the same facts when it suggests a layout or writes a control, so nothing it makes can ask for more. A checked version is frozen; a change is a new version, not an edit, and needs no OpenVibe release.",
++ '2026-11-12', "Maticus's P7 command list for LOOI (11-12) became its first frozen manifest, with the owner's \"use this list for the manifest … and AI control generation\".",
++ [], ['openvibe-contracts: bot.robot-manifest fixtures']),
++ R("every-control-is-ours", "look", "Every checkbox, radio, switch, field and button is OpenVibe's own, drawn the same on every site.",
++ "openvibe-shared's ov-forms.css draws them all, in the site's colours: a box whose check pops in, a ring whose dot grows, a switch that glows when on, a field that glows with focus, a button that answers a press. A setting that is on or off is a switch, not a box. A site's own rule wins over the system's, data-ov-native keeps the browser's control where it must, and motion follows the person's setting.",
++ '2026-11-12', "The owner, 11-12, on the machines page's grey boxes: \"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\".",
++ [{ label: 'Form controls', href: '/components/form-controls' }], ['openvibe-shared test: ov-forms-controls-chrome']),
++ R("a-planned-restart-is-not-a-loss", "honest", "A service restarting on purpose says so, and nothing latches for it.",
++ "When Bot restarts for a deploy it closes every link with 1012 and waits for the replies, Link stops a robot's plugins with the cause link_restart (not link_lost), and a driver stops its motors without latching a stop of its own that a person would have to clear by hand. Every close is logged with its code and reason, or as a drop with no close frame.",
++ '2026-11-12', "Maticus, 11-12: every Bot deploy meant Clear STOP, ARM and Go Live by hand on LOOI, and the logs did not say why the link closed.",
++ [], ['OpenVibe.Link test: TestServerRestartStopsWithLinkRestart', 'OpenVibe.Bot test: restart-close']),
++ R("agents-are-blueprints", "agents", "An agent is a blueprint anyone can read, change, share and run anywhere.",
++ "What a person makes is one small document of ten parts (face, brain, knows, hands, places, memory, rules, when, makes, team), each part a slot fed by the product that knows it best: Design for the face, Exchange for the models and prices, Tools and every product for the hands, Link for the machines, Events and Sources for what starts it. Vibe is one too. A blueprint asks and never grants: installing shows every ask in words, a new version that asks for more waits for a yes, it carries no key or token, and money, passwords, deletions, new recipients and posting in a person's name always ask. Made on Design (Create, Agent), shared and remixed in Design's gallery, run from any chat, composer, schedule, event or machine.",
++ '2026-11-12', 'The owner: one agent system, "multi purpose reusable in a modular and API/sdk driven way", the same one other people use, with Exchange, Link, Design, Chat, Codes, Tools and shared hardware all part of it.',
++ [], ['openvibe-contracts test: agents.blueprint fixtures', 'openvibe-sdk test: agents-blueprint']),
]);
// The register's order is the rule's number: a person can cite "rule 7" and a link can carry it.
ddiff --git a/server/views/component-registry.js b/server/views/component-registry.js
iindex ccf5056..b1be98f 100644
---- a/server/views/component-registry.js
++++ b/server/views/component-registry.js
@@@ -10,6 +10,8 @@ const link = (name) => `/components/${name}`;
// A live demo built from openvibe-shared's own files loads them, as a site does.
const PAGE_EXTRA = {
'thread-composer': { shared: ['ov-thread.js'], sharedStyles: ['thread.css'] },
++ 'search-field': { shared: ['ov-searchbox.js'] },
++ 'form-controls': { sharedStyles: ['ov-forms.css'] },
chat: { shared: ['messages.js', 'ov-composer.js'], sharedStyles: ['messages.css'], scripts: ['js/component-chat.js'] },
};
const demo = {
@@@ -18,6 +20,15 @@ const demo = {
'you-home': () => kit.section({ title: 'You', note: 'Your places in one menu', body: kit.list([kit.row({ href: '/look', title: 'Your look', sub: 'Theme and comfort settings', mark: kit.mark({ name: 'You' }) }), kit.row({ href: '/themes', title: 'Theme studio', sub: 'Make it yours', mark: kit.mark({ name: 'Studio' }) })]) }),
dock: () => kit.dock({ action: '#', id: 'cr-dock', label: 'Message', placeholder: 'Write a message' }),
'dock-tabs': () => `<nav class="cr-dock-tabs" aria-label="Demo app tabs"><a href="/">${kit.icon('home')} Home</a><a href="/themes" aria-current="page">${kit.icon('palette')} Studio</a><a href="/look">${kit.icon('user')} You</a></nav>`,
++ // Every control as the sites draw it (openvibe-shared/ov-forms.css): press, hover and tab through them.
++ 'form-controls': () => `<div class="cr-controls" style="display:grid;gap:14px;max-width:560px">
++<div style="display:flex;flex-wrap:wrap;gap:16px"><label style="display:flex;gap:10px;align-items:center"><input type="checkbox" checked> Checked</label><label style="display:flex;gap:10px;align-items:center"><input type="checkbox"> Off</label><label style="display:flex;gap:10px;align-items:center"><input type="checkbox" disabled checked> Disabled</label></div>
++<div style="display:flex;flex-wrap:wrap;gap:16px" role="radiogroup" aria-label="A choice"><label style="display:flex;gap:10px;align-items:center"><input type="radio" name="cr-r" checked> One</label><label style="display:flex;gap:10px;align-items:center"><input type="radio" name="cr-r"> Two</label><label style="display:flex;gap:10px;align-items:center"><input type="radio" name="cr-r" disabled> Three</label></div>
++<div style="display:flex;flex-wrap:wrap;gap:16px"><label style="display:flex;gap:10px;align-items:center"><input type="checkbox" role="switch" checked> A switch, on</label><label style="display:flex;gap:10px;align-items:center"><input type="checkbox" role="switch"> Off</label></div>
++<label class="ov-field"><span>A field</span><input class="ov-input" placeholder="Type, and see the focus glow"></label>
++<div style="display:flex;flex-wrap:wrap;gap:10px"><button type="button" class="ovk-btn">A button</button><button type="button" class="ovk-btn ovk-btn--primary">Primary</button></div></div>`,
++ // The real field, searching this site's own components, rules and themes (GET /api/v1/suggest).
++ 'search-field': () => `<form class="cr-search" action="/components" role="search" style="max-width:560px"><input name="q" placeholder="Search" aria-label="Search OpenVibe.Design" data-ov-searchbox="/api/v1/suggest" data-ov-examples="components|rules|themes"></form><p class="cr-note">Type “me”, then Tab. Press / from anywhere on the page.</p>`,
'thread-composer': () => thread.composer({ action: '#', method: 'get', placeholder: 'Ask anything', tools: kit.sheetButton({ sheet: 'cr-options', label: 'Options', icon: 'more', iconOnly: false }), attach: { name: 'files' }, id: 'cr-composer' }) + kit.sheet({ id: 'cr-options', title: 'Options', body: kit.seg({ name: 'cr-model', label: 'Model', options: [['auto', 'Auto'], ['fast', 'Fast']], value: 'auto' }) }),
chat: () => `<div class="cr-chat"><div class="cr-chat-views" role="group" aria-label="How messages look"><button type="button" class="ovk-btn ovk-btn--quiet" data-cr-chat-view="room" aria-pressed="true">Room lines</button><button type="button" class="ovk-btn ovk-btn--quiet" data-cr-chat-view="bubbles" aria-pressed="false">Bubbles</button></div><ol class="ovm-messages" id="cr-chat-list" aria-label="Demo conversation"></ol><div id="cr-chat-box"></div><p class="ds-fine">Hover a message (on a phone, tap it): reply, edit, translate and copy are one cluster at its corner. Edit puts the words in the box; Enter saves, Esc puts back what was there. The demo keeps its messages on this page.</p></div>`,
sheet: () => kit.sheetButton({ sheet: 'cr-sheet', label: 'Open sheet', icon: 'plus', iconOnly: false }) + kit.sheet({ id: 'cr-sheet', title: 'Add a site', body: `<p>Pick a site and theme.</p>${kit.seg({ name: 'cr-theme', label: 'Look', options: [['vibe', 'Vibe'], ['daylight', 'Daylight']], value: 'vibe' })}` }),
@@@ -43,7 +54,7 @@ function registryLinks() {
const GROUPS = [
['Navigation', ['navbar', 'menu', 'you-home', 'dock', 'dock-tabs']],
-- ['Writing and choosing', ['chat', 'thread-composer', 'sheet', 'popover', 'keyboard']],
++ ['Writing and choosing', ['chat', 'thread-composer', 'search-field', 'form-controls', 'sheet', 'popover', 'keyboard']],
['Content', ['kit-row', 'kit-card', 'kit-section', 'empty-state']],
['Settings', ['switch-row', 'segmented-control']],
['Files and AI', ['upload-progress', 'ai-choice']],