openvibers/openvibe.shared nav-slots.js

nav-slots.js

main 6.2 KB · 71 lines · javascript

1/**
2 * nav-slots.js (openvibe-shared 4.160.0): a site's own buttons in the navbar, drawn here so the bar stays small. The
3 * navbar loads this the first time a site calls OpenVibeNavbar.lead(spec) or .action(spec) and calls paint() after each
4 * render. Known icons by name, or 24-unit path data (letters, numbers and path commands only: no markup from a site).
5 */
6(function (root) {
7 'use strict';
8 const CSS = `.openvibe-navbar .ovnav-slot { all: unset; box-sizing: border-box; position: relative; display: inline-grid; place-items: center; flex: none; width: 34px; height: 34px; border-radius: 11px; cursor: pointer; color: var(--text-secondary, #a8b3c4); transition: background-color .15s, color .15s, transform .18s cubic-bezier(.2, 1.4, .3, 1); }
9.openvibe-navbar .ovnav-slot:hover, .openvibe-navbar .ovnav-slot:focus-visible { background: var(--accent-glow, rgba(59,130,246,.14)); color: var(--text-primary, #e6edf7); }
10.openvibe-navbar .ovnav-slot:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
11.openvibe-navbar .ovnav-slot:active { transform: scale(.9); }
12.openvibe-navbar .ovnav-slot[aria-expanded="true"], .openvibe-navbar .ovnav-slot[aria-pressed="true"] { color: var(--accent-light, #60a5fa); background: var(--accent-glow, rgba(59,130,246,.14)); }
13.openvibe-navbar .ovnav-slot svg { width: 21px; height: 21px; }
14.openvibe-navbar .ovnav-slot b { position: absolute; top: -1px; right: -3px; min-width: 16px; height: 16px; padding: 0 4px; box-sizing: border-box; border-radius: 999px; background: var(--live-red, #ef4444); color: #fff; font: 700 10px/16px Inter, system-ui, sans-serif; text-align: center; box-shadow: 0 0 0 2px var(--bg-secondary, #111826); }
15.openvibe-navbar .ovnav-slot b:empty { display: none; }
16.openvibe-navbar .ovnav-lead { margin-left: -2px; animation: ovnav-slot-in .22s cubic-bezier(.2, .9, .3, 1) both; }
17@keyframes ovnav-slot-in { from { opacity: 0; transform: translateX(-6px); } }
18@media (max-width: 400px) { .openvibe-navbar .ovnav-slot { width: 32px; height: 32px; } }
19/* 347: the site's own buttons match the menu button on the right: its size, its corner, its stroke. */
20@media (min-width: 761px) { .openvibe-navbar .ovnav-slot.ovnav-phone-only { display: none; } }`;
21 const SLOT_ICONS = {
22 sidebar: 'M4 5.5h16a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-11a1 1 0 0 1 1-1zM9.5 5.5v13',
23 back: 'M15 5l-7 7 7 7',
24 close: 'M6 6l12 12M18 6L6 18',
25 plus: 'M12 5v14M5 12h14',
26 search: 'M10.5 4a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13zM15.5 15.5L20 20',
27 settings: 'M12 9a3 3 0 1 1 0 6 3 3 0 0 1 0-6zM12 2.8v2.4M12 18.8v2.4M2.8 12h2.4M18.8 12h2.4M5.5 5.5l1.7 1.7M16.8 16.8l1.7 1.7M5.5 18.5l1.7-1.7M16.8 7.2l1.7-1.7',
28 filter: 'M4 6h16M7 12h10M10 18h4',
29 share: 'M12 15V4M8 8l4-4 4 4M5 13v6h14v-6',
30 star: 'M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8-5.2-2.7-5.2 2.7 1-5.8L3.5 9.7l5.9-.9z',
31 info: 'M12 3a9 9 0 1 1 0 18 9 9 0 0 1 0-18zM12 11v5.5M12 7.6v.4',
32 };
33 const PATH_OK = /^[MmLlHhVvCcSsQqTtAaZz0-9 ,.\-]{1,2000}$/;
34 const esc = (v) => String(v == null ? '' : v).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
35 function html(spec, cls, id) {
36 const label = esc(spec.label || 'Menu');
37 const ctl = spec.sidebar || spec.controls;
38 const state = (spec.sidebar ? ' data-ovk-sidebar-open' : '') + (ctl ? ` aria-controls="${esc(ctl)}" aria-expanded="${spec.expanded ? 'true' : 'false'}"` : spec.pressed != null ? ` aria-pressed="${spec.pressed ? 'true' : 'false'}"` : '');
39 if (spec.phoneOnly) cls += ' ovnav-phone-only';
40 const d = spec.path && PATH_OK.test(spec.path) ? spec.path : SLOT_ICONS[spec.icon] || SLOT_ICONS.info;
41 const svg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${d}"/></svg>`;
42 const badge = spec.badge != null ? `<b>${esc(String(spec.badge).slice(0, 3))}</b>` : '';
43 const href = spec.href && /^(https?:\/\/|\/)/.test(String(spec.href)) ? spec.href : null;
44 return href ? `<a class="ovnav-slot ${cls}" id="${esc(id)}" href="${esc(href)}" aria-label="${label}" title="${label}"${state}>${svg}${badge}</a>`
45 : `<button type="button" class="ovnav-slot ${cls}" id="${esc(id)}" aria-label="${label}" title="${label}"${state}>${svg}${badge}</button>`;
46 }
47 /** Draw the lead button before the brand and the actions where the bell was (before the picture or Sign in);
48 clicks go to the site's handlers. */
49 function paint(nav, lead, actions) {
50 if (!nav || !nav.querySelector) return;
51 if (!document.getElementById('ovnav-slots-styles')) { const st = document.createElement('style'); st.id = 'ovnav-slots-styles'; st.textContent = CSS; document.head.appendChild(st); }
52 nav.querySelectorAll('.ovnav-slot').forEach((el) => el.remove());
53 const brand = nav.querySelector('.openvibe-navbar-brand');
54 if (lead && brand) brand.insertAdjacentHTML('afterbegin', html(lead, 'ovnav-lead', 'ovnav-lead'));
55 // The bar's bell is gone (437): its mount is kept as an anchor when a page still draws one, else the actions
56 // sit before the picture (signed in) or Sign in, so a site's buttons stay on the bar's right.
57 const anchor = nav.querySelector('#openvibe-bell-mount') || nav.querySelector('.ovnav-avatar-wrap, #openvibe-login-btn');
58 if (anchor && actions) for (const a of actions.values()) anchor.insertAdjacentHTML('beforebegin', html(a, 'ovnav-act', `ovnav-act-${a.id}`));
59 nav.__ovSlots = { lead, actions };
60 if (nav.__ovSlotsBound) return;
61 nav.__ovSlotsBound = true;
62 nav.addEventListener('click', (e) => {
63 const b = e.target && e.target.closest ? e.target.closest('.ovnav-slot') : null;
64 if (!b || !nav.__ovSlots) return;
65 const s = b.id === 'ovnav-lead' ? nav.__ovSlots.lead : nav.__ovSlots.actions.get(b.id.replace(/^ovnav-act-/, ''));
66 if (s && typeof s.onClick === 'function') { if (!s.href) e.preventDefault(); try { s.onClick(e, b); } catch (err) { console.error(err); } }
67 });
68 }
69 root.OpenVibeNavSlots = { paint, icons: Object.keys(SLOT_ICONS) };
70})(typeof window !== 'undefined' ? window : globalThis);
71