/* Shared UI primitives for the demo forms: icons, escaping, the "?" help tooltip and the toast. Form-specific state stays in each form module. */ export const $ = (selector, scope = document) => scope.querySelector(selector); export const $$ = (selector, scope = document) => [...scope.querySelectorAll(selector)]; export const escape = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); export const clone = value => JSON.parse(JSON.stringify(value)); export const icons = { floor:'', sliders:'', palette:'', card:'', sun:'', close:'', check:'', layers:'', info:'', help:'', image:'', draw:'', grid:'', upload:'', download:'', folder:'', borders:'', type:'', zigbee:'', sofa:'', door:'', temperature:'', humidity:'', bulb:'', eyeOff:'', eye:'', more:'', copy:'', trash:'', compass:'', reset:'', chevron:'', link:'', radar:'', tooltip:'', wall:'', broom:'', wifi:'', refresh:'', }; export const icon = name => ``; export const hydrateIcons = (scope = document) => $$('[data-icon]', scope).forEach(el => { el.innerHTML = icon(el.dataset.icon); }); /* Toast: one shared status line at the bottom of the page. */ let toastTimer; export function showToast(message) { const toast = $('#toast'); clearTimeout(toastTimer); toast.textContent = message; toast.hidden = false; toastTimer = setTimeout(() => { toast.hidden = true; }, 4200); } /* Help: "?" trigger with a floating tooltip, mirrors the product's hp-help (hover, click or focus opens; Escape, outside click or leaving closes). One surface serves every form. */ const helpPopover = document.createElement('div'); helpPopover.id = 'help-popover'; helpPopover.className = 'help-popover'; helpPopover.setAttribute('role', 'tooltip'); helpPopover.hidden = true; document.body.append(helpPopover); let helpTrigger = null, helpPinned = false, helpText = () => ''; function positionHelp() { if (!helpTrigger) return; const margin = 8, gap = 6, a = helpTrigger.getBoundingClientRect(); helpPopover.style.maxWidth = `${Math.max(0, Math.min(320, innerWidth - margin * 2))}px`; const r = helpPopover.getBoundingClientRect(); const left = Math.min(Math.max(margin, a.left), innerWidth - margin - r.width); let top = a.bottom + gap, side = 'bottom'; if (top + r.height > innerHeight - margin && a.top - gap - r.height >= margin) { top = a.top - gap - r.height; side = 'top'; } helpPopover.style.left = `${Math.round(left)}px`; helpPopover.style.top = `${Math.round(top)}px`; helpPopover.dataset.side = side; } function openHelp(button, pinned) { if (helpTrigger && helpTrigger !== button) helpTrigger.setAttribute('aria-expanded', 'false'); helpTrigger = button; helpPinned = pinned || (helpPinned && helpTrigger === button); helpPopover.textContent = helpText(button.dataset.help); helpPopover.hidden = false; button.setAttribute('aria-expanded', 'true'); positionHelp(); } export function closeHelp() { if (!helpTrigger) return; helpTrigger.setAttribute('aria-expanded', 'false'); helpTrigger = null; helpPinned = false; helpPopover.hidden = true; } export const helpOpen = () => !!helpTrigger; document.addEventListener('pointerdown', e => { if (helpTrigger && !e.target.closest('button[data-help]') && !helpPopover.contains(e.target)) closeHelp(); }, true); addEventListener('resize', () => { if (helpTrigger) positionHelp(); }); /* Bind the triggers inside `scope` to a dictionary { key: [title, text] }; `scroller` keeps the tooltip anchored while the form scrolls. */ export function attachHelp(scope, scroller, copy) { const text = key => copy[key][1]; scope.addEventListener('click', e => { const button = e.target.closest('button[data-help]'); if (!button) return; helpText = text; if (helpTrigger === button && helpPinned) closeHelp(); else openHelp(button, true); }); scope.addEventListener('pointerover', e => { const button = e.target.closest('button[data-help]'); if (!button || e.pointerType === 'touch' || helpPinned) return; helpText = text; openHelp(button, false); }); scope.addEventListener('pointerout', e => { const button = e.target.closest('button[data-help]'); if (button && helpTrigger === button && !helpPinned && !button.contains(e.relatedTarget)) closeHelp(); }); scope.addEventListener('focusin', e => { const b = e.target.closest('button[data-help]'); if (b && !helpPinned) { helpText = text; openHelp(b, false); } }); scope.addEventListener('focusout', e => { const b = e.target.closest('button[data-help]'); if (b && helpTrigger === b && !helpPinned) closeHelp(); }); scroller.addEventListener('scroll', () => { if (helpTrigger) positionHelp(); }, { passive: true }); return { button: key => `` }; }