ferro-json-ui 0.3.7

JSON-based server-driven UI schema types for Ferro
Documentation
pub(super) const SOURCE: &str = r#"
    // ── Command palette ──────────────────────────────────────────────────────
    //
    // Intercepts the open-command-palette custom event and ⌘K / Ctrl+K keydown.
    // Renders grouped search results via GET /dashboard/search?q=.
    // Recents stored in localStorage; quick actions from static config.
    // ARIA: role="combobox" on input, role="listbox" on results, stable
    // option ids ("fjui-palette-opt-{n}"), aria-activedescendant updated
    // on every arrow-key press (Pitfall D-02 baseline).
    //
    // Persistent-frame: inits exactly once via idempotent guard on element.
    // ES5 only: var, named function declarations, C-style for loops.
    // No arrow functions, no const/let, no template literals.

    function setupCommandPalette() {
        // Persistent-frame: init exactly once. Guard on element presence.
        if (document.getElementById('fjui-palette')) return;

        // ── Config ──────────────────────────────────────────────────────────
        var cfg = window.fjuiPaletteConfig || {};
        var searchUrl = cfg.searchUrl || '/dashboard/search';
        var quickActions = cfg.quickActions || [];

        // ── Recents ─────────────────────────────────────────────────────────
        var RECENTS_KEY = 'fjui-palette-recents';
        var RECENTS_MAX = 8;

        function getRecents() {
            try {
                var raw = localStorage.getItem(RECENTS_KEY);
                return raw ? JSON.parse(raw) : [];
            } catch (_) { return []; }
        }

        function recordRecentFromUrl(url) {
            try {
                // Only record dashboard entity-detail URLs (simple path filter).
                if (!url || url.indexOf('/dashboard/') === -1) return;
                var list = getRecents();
                list = [url].concat(list.filter(function(u) { return u !== url; })).slice(0, RECENTS_MAX);
                localStorage.setItem(RECENTS_KEY, JSON.stringify(list));
            } catch (_) {}
        }

        document.addEventListener('fjui:navigated', function() {
            recordRecentFromUrl(window.location.href);
        });

        // ── Build dialog DOM ─────────────────────────────────────────────────
        var dlg = document.createElement('dialog');
        dlg.id = 'fjui-palette';
        dlg.className = 'fjui-palette';
        dlg.setAttribute('role', 'dialog');
        dlg.setAttribute('aria-modal', 'true');
        dlg.setAttribute('aria-label', 'Cerca');

        var input = document.createElement('input');
        input.type = 'text';
        input.className = 'fjui-palette__input';
        input.setAttribute('role', 'combobox');
        input.setAttribute('aria-expanded', 'false');
        input.setAttribute('aria-controls', 'fjui-palette-listbox');
        input.setAttribute('aria-label', 'Cerca nel dashboard');
        input.setAttribute('autocomplete', 'off');
        input.setAttribute('spellcheck', 'false');
        input.placeholder = 'Cerca clienti, ordini, prenotazioni…';

        var listbox = document.createElement('ul');
        listbox.id = 'fjui-palette-listbox';
        listbox.className = 'fjui-palette__results';
        listbox.setAttribute('role', 'listbox');
        listbox.setAttribute('aria-label', 'Risultati');

        var hint = document.createElement('div');
        hint.className = 'fjui-palette__hint';
        hint.textContent = 'ESC per chiudere';
        hint.setAttribute('aria-hidden', 'true');

        dlg.appendChild(input);
        dlg.appendChild(listbox);
        dlg.appendChild(hint);
        document.body.appendChild(dlg);

        // ── Listbox engine ───────────────────────────────────────────────────
        var optionIndex = 0;

        var engine = createListboxEngine(input, listbox, function(optionEl) {
            var url = optionEl.getAttribute('data-url');
            if (!url) return;
            try { dlg.close(); } catch (_) {}
            input.setAttribute('aria-expanded', 'false');
            input.removeAttribute('aria-activedescendant');
            // Navigate via 248 nav runtime. The nav runtime intercepts
            // same-origin assigns and link clicks automatically. Use
            // window.location.assign so same-origin navigations are
            // picked up by the click/popstate interceptors in nav.rs.
            try { window.location.assign(url); } catch (_) {}
        });

        input.addEventListener('keydown', engine.handleKey);

        // Close clears aria state.
        dlg.addEventListener('close', function() {
            input.setAttribute('aria-expanded', 'false');
            input.removeAttribute('aria-activedescendant');
            input.value = '';
            clearTimeout(paletteTimer);
            if (paletteController) { try { paletteController.abort(); } catch (_) {} }
        });

        // ── Abort controller + debounce ──────────────────────────────────────
        var paletteController = null;
        var paletteTimer = null;

        function fetchResults(q) {
            if (paletteController) { try { paletteController.abort(); } catch (_) {} }
            clearTimeout(paletteTimer);
            paletteTimer = setTimeout(function() {
                paletteController = new AbortController();
                try {
                    fetch(searchUrl + '?q=' + encodeURIComponent(q), {
                        credentials: 'same-origin',
                        signal: paletteController.signal
                    }).then(function(r) { return r.json(); })
                      .then(function(data) {
                          renderResults(data.results || [], q);
                      })
                      .catch(function() { /* silently swallow abort and network errors */ });
                } catch (_) {}
            }, 120);
        }

        // ── Render grouped results ───────────────────────────────────────────
        var GROUP_ORDER = ['clienti', 'prenotazioni', 'ordini', 'prodotti'];
        var GROUP_LABELS = {
            clienti: 'CLIENTI',
            prenotazioni: 'PRENOTAZIONI',
            ordini: 'ORDINI',
            prodotti: 'PRODOTTI'
        };

        function makeOption(url, title, subtitle) {
            var li = document.createElement('li');
            li.setAttribute('role', 'option');
            li.id = 'fjui-palette-opt-' + optionIndex;
            li.className = 'fjui-palette__option';
            li.setAttribute('aria-selected', 'false');
            li.setAttribute('data-url', url);
            optionIndex++;

            var titleEl = document.createElement('span');
            titleEl.className = 'fjui-palette__option-title';
            titleEl.textContent = title;

            li.appendChild(titleEl);

            if (subtitle) {
                var subtitleEl = document.createElement('span');
                subtitleEl.className = 'fjui-palette__option-subtitle';
                subtitleEl.textContent = subtitle;
                li.appendChild(subtitleEl);
            }

            li.addEventListener('click', function() {
                engine.updateOptions();
                var url = li.getAttribute('data-url');
                if (!url) return;
                try { dlg.close(); } catch (_) {}
                input.setAttribute('aria-expanded', 'false');
                input.removeAttribute('aria-activedescendant');
                try { window.location.assign(url); } catch (_) {}
            });

            return li;
        }

        function renderResults(results, q) {
            optionIndex = 0;
            while (listbox.firstChild) { listbox.removeChild(listbox.firstChild); }

            if (results.length === 0) {
                var empty = document.createElement('li');
                empty.className = 'fjui-palette__empty';
                if (q && q.length > 0) {
                    // textContent only — no user strings in markup (T-249-02-01).
                    empty.textContent = 'Nessun risultato per "' + q + '"';
                } else {
                    empty.textContent = 'Inizia a digitare per cercare';
                }
                listbox.appendChild(empty);
                input.setAttribute('aria-expanded', 'false');
                return;
            }

            // Bucket by group in fixed order.
            var buckets = {};
            for (var g = 0; g < GROUP_ORDER.length; g++) {
                buckets[GROUP_ORDER[g]] = [];
            }
            for (var i = 0; i < results.length; i++) {
                var item = results[i];
                if (buckets[item.group]) {
                    buckets[item.group].push(item);
                }
            }

            var hasItems = false;
            for (var gi = 0; gi < GROUP_ORDER.length; gi++) {
                var group = GROUP_ORDER[gi];
                var items = buckets[group];
                if (!items || items.length === 0) continue;
                hasItems = true;

                var groupLi = document.createElement('li');
                groupLi.setAttribute('role', 'group');
                groupLi.setAttribute('aria-label', GROUP_LABELS[group] || group.toUpperCase());

                var header = document.createElement('div');
                header.className = 'fjui-palette__group-label';
                header.textContent = GROUP_LABELS[group] || group.toUpperCase();
                groupLi.appendChild(header);

                var innerUl = document.createElement('ul');
                innerUl.setAttribute('role', 'presentation');

                for (var j = 0; j < items.length; j++) {
                    innerUl.appendChild(makeOption(items[j].url, items[j].title, items[j].subtitle || ''));
                }

                groupLi.appendChild(innerUl);
                listbox.appendChild(groupLi);
            }

            if (hasItems) {
                input.setAttribute('aria-expanded', 'true');
            }
            engine.updateOptions();
        }

        // ── Empty state: recents + quick actions ─────────────────────────────
        function renderEmptyState() {
            optionIndex = 0;
            while (listbox.firstChild) { listbox.removeChild(listbox.firstChild); }

            var recents = getRecents();
            var hasContent = false;

            if (recents.length > 0) {
                hasContent = true;
                var recentsLi = document.createElement('li');
                recentsLi.setAttribute('role', 'group');
                recentsLi.setAttribute('aria-label', 'RECENTI');

                var recentsHeader = document.createElement('div');
                recentsHeader.className = 'fjui-palette__group-label';
                recentsHeader.textContent = 'RECENTI';
                recentsLi.appendChild(recentsHeader);

                var recentsUl = document.createElement('ul');
                recentsUl.setAttribute('role', 'presentation');

                for (var r = 0; r < recents.length; r++) {
                    var recentUrl = recents[r];
                    // Derive a display label from the URL path segments.
                    var segments = recentUrl.replace(/\/$/, '').split('/');
                    var label = segments[segments.length - 1] || recentUrl;
                    recentsUl.appendChild(makeOption(recentUrl, label, recentUrl));
                }

                recentsLi.appendChild(recentsUl);
                listbox.appendChild(recentsLi);
            }

            if (quickActions.length > 0) {
                hasContent = true;
                var actionsLi = document.createElement('li');
                actionsLi.setAttribute('role', 'group');
                actionsLi.setAttribute('aria-label', 'AZIONI RAPIDE');

                var actionsHeader = document.createElement('div');
                actionsHeader.className = 'fjui-palette__group-label';
                actionsHeader.textContent = 'AZIONI RAPIDE';
                actionsLi.appendChild(actionsHeader);

                var actionsUl = document.createElement('ul');
                actionsUl.setAttribute('role', 'presentation');

                for (var a = 0; a < quickActions.length; a++) {
                    var action = quickActions[a];
                    actionsUl.appendChild(makeOption(action.url, action.label, ''));
                }

                actionsLi.appendChild(actionsUl);
                listbox.appendChild(actionsLi);
            }

            if (!hasContent) {
                var emptyEl = document.createElement('li');
                emptyEl.className = 'fjui-palette__empty';
                emptyEl.textContent = 'Inizia a digitare per cercare';
                listbox.appendChild(emptyEl);
            }

            if (hasContent) {
                input.setAttribute('aria-expanded', 'true');
                engine.updateOptions();
            }
        }

        // ── Input event ──────────────────────────────────────────────────────
        input.addEventListener('input', function() {
            var q = input.value.trim();
            if (q === '') {
                clearTimeout(paletteTimer);
                if (paletteController) { try { paletteController.abort(); } catch (_) {} }
                renderEmptyState();
            } else {
                fetchResults(q);
            }
        });

        // ── Open function ────────────────────────────────────────────────────
        function openPalette() {
            try {
                dlg.showModal();
            } catch (_) {}
            try { input.focus(); } catch (_) {}
            renderEmptyState();
        }

        // ── Open triggers ────────────────────────────────────────────────────
        // Header 247 seam.
        document.addEventListener('fjui:open-command-palette', function() {
            openPalette();
        });

        // ⌘K / Ctrl+K global shortcut.
        document.addEventListener('keydown', function(e) {
            if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
                e.preventDefault();
                openPalette();
            }
        });
    }
"#;