/**
* openvibe-shared/kit.js — the OpenVibe Kit: the pieces an app page is made of, one look on every OpenVibe site (and on
* anyone's: the package is MIT), phone first. Draw with /shared/kit.css. docs/kit.md describes every piece and how it
* behaves on a phone, a tablet and a computer.
*
* The owner, 2026-10-28: "the pages are so dang long… use tabs or collapsible areas… fixed position elements… on a
* phone it should act like an app, not a huge long web page… one app ecosystem across all our systems." So a page is:
* a sticky head (back, title, one muted line, icon actions) with its tabs, content in lists of rows and cards that fold,
* a dock pinned at the bottom for the one thing you type, and the navbar's app bar under it on phones.
*
* The same file runs on the server (require('openvibe-shared/kit'): builders return HTML strings, every text escaped,
* so pages are complete without JavaScript) and in the browser (window.OpenVibeKit: the same builders for pages that
* draw themselves). The behaviour (copy buttons, menus that close, Enter to send, sheets, confirms) is kit-web.js, a
* small file of its own: a server-rendered site loads only that; a page that draws itself loads both and calls
* OpenVibeKit.enhance(container) after adding markup.
*
* const kit = require('openvibe-shared/kit');
* kit.head({ title: 'Settings', sub: 'Saved to your account', back: { href: '/', label: 'Home' },
* actions: [kit.iconButton({ href: '/help', icon: 'help', label: 'Help' })],
* tabs: kit.tabs({ base: '/settings', items: [{ key: 'look', label: 'Look' }, …], active }) })
* kit.tabOf(req.query, ['look', 'sound']) → the open tab (the first when none or unknown)
* kit.list([kit.row({ href, mark: kit.mark({ name }), title, meta: kit.when(ts).text, sub, unread: 2 }), …])
* kit.section({ title: 'Messages look', body: kit.switchRow({ name, icon, title, line, checked }) + kit.rangeRow(…) + … })
* kit.segmented({ name, items: [{ value, label, icon }] }) three or four equal choices in one control
* kit.fold({ id, title, meta, open, body }) a card that opens: setup, extras, danger
* kit.find({ action, name, placeholder, icon: 'search' }) a search or "add by name" row
* kit.dock({ action, name, placeholder, label }) the composer pinned at the bottom
* kit.stats([[1234, 'messages this week'], …]) a strip of numbers (scrolls sideways on phones)
* kit.empty({ title, text, action: { href, label } }) · kit.notice({ kind, text }) · kit.more({ label, items })
* kit.button({ href | type, label, icon, kind }) · kit.share({ title, url, kind, id }) · kit.askAgent({ text }) · kit.iconButton(…) · kit.chips(…) · kit.seg(…) · kit.segmented(…) · kit.icon(name)
* kit.mark({ name, src, hue, square }) · kit.when(ts) → { iso, text } · kit.esc(text) · kit.ICONS (names)
* kit.mediaW(url, width) a Media object at a resize width (64/96/160/320/480/640)
* kit.sidebar({ id, label, action, find, nav, sections, foot }) · kit.sidebarButton({ id }) · kit.withSidebar({ sidebar,
* body }) the left sidebar: vertical navigation, a drawer on phones
*
* The tool frame, one order for every tool page: kit.tool({ head: kit.toolHead({ icon, hue, crumb, title, task, facts }),
* input: kit.toolInput({ name, label, placeholder, button, tries: { items } }) | kit.drop(…) | an editor,
* answer: kit.answer({ title, badge, rows: [[label, value, note]], next }), family: kit.family({ title, more, items }),
* folds: [kit.fold(…)], related: kit.list(…) })
*
* Text arguments are escaped; arguments named body, html, before, after, actions, trail, badge, extra and mark take
* trusted markup (usually another builder's output). URLs are kept only when they are http(s) or same-site paths.
*/
(function (root, factory) {
const api = factory(root);
if (typeof module === 'object' && module.exports) { module.exports = api; return; }
root.OpenVibeKit = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function (root) {
'use strict';
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
/** A URL kept only when it is http(s), a same-site path, a query or a fragment; anything else becomes '#'. */
const url = (u) => { const s = String(u == null ? '' : u).trim(); return /^(https?:\/\/|\/(?!\/)|\?|#)/i.test(s) ? s : '#'; };
// A Media object (openvibe.media/o/med_…) at a width from Media's resize ladder (64, 96, 160, 320, 480, 640); any other
// URL as url() gives it. Avatars and pictures are stored as uploaded: Report and Community drew a 3072 px, 3 MB
// avatar at 34 px (4.87.0).
const mediaW = (u, w) => { const s = url(u); return /^https:\/\/openvibe\.media\/o\/med_[0-9A-Za-z]+$/.test(s) ? `${s}?w=${Number(w) || 160}` : s; };
const attrs = (o) => Object.keys(o || {}).filter((k) => o[k] != null && o[k] !== false && /^[a-z][a-z0-9-]*$/i.test(k)).map((k) => (o[k] === true ? ` ${k}` : ` ${k}="${esc(o[k])}"`)).join('');
// ── Icons: 24-unit line drawings in the text's colour (stroke 2, round), one set for every site ──
const P = {
back: '', forward: '', chevron: '',
up: '', down: '', close: '',
plus: '', minus: '', check: '',
send: '', search: '',
more: '',
menu: '', sidebar: '', filter: '',
home: '',
chat: '', mail: '',
bell: '',
user: '',
members: '',
invite: '',
gear: '',
pencil: '', trash: '',
copy: '',
open: '',
link: '',
download: '', upload: '',
lock: '',
key: '',
ban: '', help: '',
info: '', alert: '',
clock: '', calendar: '',
spark: '',
star: '', heart: '',
play: '', pause: '', stop: '',
live: '',
video: '', mic: '',
image: '',
file: '', folder: '',
code: '', terminal: '',
computer: '', phone: '',
globe: '',
tag: '', cart: '',
coin: '',
gift: '',
trophy: '',
flag: '', eye: '',
'eye-off': '',
flame: '',
map: '', book: '',
game: '',
leave: '',
retry: '',
task: '', steps: '',
hand: '',
shield: '', zap: '',
cloud: '', server: '',
sun: '',
moon: '', grid: '',
list: '',
chart: '', swap: '',
wave: '',
shoe: '',
palette: '', robot: '',
};
// ── The glyphs the kit does not draw itself, generated from glyphs.js (scripts/build-kit-glyphs.js) ──
// kit.icon has to draw every glyph a page can name: a page that asks for compass or binoculars gets the same
// registry's drawing, not an empty