openvibers/openvibe.shared nav-slots.js
nav-slots.js
| 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 | ; |
| 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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 |