/* ============================================================
   PPphp Designer — Panels (left panel, workbar, statusbar)
   Pure DOM wiring driven by the store. No rendering of the
   canvas/inspector here — those subscribe to store events.
   ============================================================ */
(function () {
    'use strict';
    var PPD = window.PPD = window.PPD || {};
    var store = PPD.store, model = PPD.model, reg = PPD.registry;
    var esc = reg.esc, title = reg.title, icon = reg.icon;

    function qs(s, r) { return (r || document).querySelector(s); }
    function qsa(s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); }

    var COMMON = { menu: 'Menu', login: 'Login', profile: 'Profile', register: 'Register', forgot_password: 'Forgot Password', change_password: 'Change Password', reset_password: 'Reset Password', central_dashboard: 'Central Dashboard', calendar: 'Calendar', admin_area: 'Admin Area', two_factor: 'Two-Factor' };
    // Reserved pseudo-table for custom pages not bound to a real table (mirrors
    // DesignerController::STANDALONE_TABLE).
    var STANDALONE = '__standalone__';

    // Template galleries per common page. Adding a gallery for another page
    // (register, dashboard, …) is one entry here + variants in its
    // core/Designer/Templates/*PageTemplate.php — the picker UI is generic.
    var GALLERIES = {
        login: {
            title: 'Login template',
            hint: 'Choosing a template replaces the current Login page. Every detail stays editable.',
            activeSetting: 'loginTemplate',
            templates: [
                { id: 'flat', name: 'Theme Flat', note: 'Matches your app theme', icon: 'bi-square-half' },
                { id: 'aurora', name: 'Aurora Split', note: 'Bold product story', icon: 'bi-stars' },
                { id: 'halo', name: 'Glass Halo', note: 'Immersive and focused', icon: 'bi-circle-half' },
                { id: 'executive', name: 'Executive', note: 'Clean enterprise SSO', icon: 'bi-buildings' },
                { id: 'verdant', name: 'Verdant Glass', note: 'Scenic frosted focus', icon: 'bi-tree' }
            ]
        },
        register: {
            title: 'Register template',
            hint: 'Choosing a template replaces the current Register page. Every detail stays editable.',
            activeSetting: 'registerTemplate',
            templates: [
                { id: 'flat', name: 'Theme Flat', note: 'Matches your app theme', icon: 'bi-square-half' },
                { id: 'aurora', name: 'Aurora Split', note: 'Bold product story', icon: 'bi-stars' },
                { id: 'halo', name: 'Glass Halo', note: 'Immersive and focused', icon: 'bi-circle-half' },
                { id: 'executive', name: 'Executive', note: 'Enterprise SSO sign-up', icon: 'bi-buildings' },
                { id: 'verdant', name: 'Verdant Glass', note: 'Scenic frosted sign-up', icon: 'bi-tree' }
            ]
        },
        profile: {
            title: 'Profile template',
            hint: 'Choosing a template replaces the current Profile page. Every detail stays editable.',
            activeSetting: 'profileTemplate',
            templates: [
                { id: 'card', name: 'Identity Card', note: 'Clean centered card', icon: 'bi-person-vcard' },
                { id: 'cover', name: 'Cover Hero', note: 'Banner with avatar', icon: 'bi-image' },
                { id: 'split', name: 'Studio Split', note: 'Summary + edit form', icon: 'bi-layout-sidebar' },
                { id: 'verdant', name: 'Verdant Glass', note: 'Scenic frosted profile', icon: 'bi-tree' }
            ]
        },
        forgot_password: {
            title: 'Forgot-password template',
            hint: 'Choosing a template replaces the current Forgot Password page. Every detail stays editable.',
            activeSetting: 'forgotPasswordTemplate',
            templates: [
                { id: 'flat', name: 'Theme Flat', note: 'Matches your app theme', icon: 'bi-square-half' },
                { id: 'aurora', name: 'Aurora Split', note: 'Bold product story', icon: 'bi-stars' },
                { id: 'halo', name: 'Glass Halo', note: 'Immersive and focused', icon: 'bi-circle-half' },
                { id: 'executive', name: 'Executive', note: 'Clean enterprise look', icon: 'bi-buildings' },
                { id: 'verdant', name: 'Verdant Glass', note: 'Calm scenic recovery', icon: 'bi-tree' }
            ]
        },
        change_password: {
            title: 'Change-password template',
            hint: 'Choosing a template replaces the current Change Password page. Every detail stays editable.',
            activeSetting: 'changePasswordTemplate',
            templates: [
                { id: 'guard', name: 'Security Guard', note: 'Shield card + checklist', icon: 'bi-shield-lock' },
                { id: 'coach', name: 'Coach', note: 'Tips panel + form', icon: 'bi-lightbulb' },
                { id: 'compact', name: 'Compact', note: 'Minimal quick form', icon: 'bi-input-cursor' },
                { id: 'verdant', name: 'Verdant Glass', note: 'Scenic secure update', icon: 'bi-tree' }
            ]
        },
        reset_password: {
            title: 'Reset-password template',
            hint: 'Choosing a template replaces the current Reset Password page. Every detail stays editable.',
            activeSetting: 'resetPasswordTemplate',
            templates: [
                { id: 'flat', name: 'Theme Flat', note: 'Matches your app theme', icon: 'bi-square-half' },
                { id: 'aurora', name: 'Aurora Split', note: 'Bold product story', icon: 'bi-stars' },
                { id: 'halo', name: 'Glass Halo', note: 'Immersive and focused', icon: 'bi-circle-half' },
                { id: 'executive', name: 'Executive', note: 'Clean enterprise look', icon: 'bi-buildings' },
                { id: 'verdant', name: 'Verdant Glass', note: 'Calm scenic reset', icon: 'bi-tree' }
            ]
        },
        two_factor: {
            title: '2FA challenge template',
            hint: 'Choosing a template replaces the two-factor challenge shown at sign-in. The in-app 2FA setup page is separate.',
            activeSetting: 'twoFactorTemplate',
            templates: [
                { id: 'flat', name: 'Theme Flat', note: 'Matches your app theme', icon: 'bi-square-half' },
                { id: 'aurora', name: 'Aurora Split', note: 'Bold product story', icon: 'bi-stars' },
                { id: 'halo', name: 'Glass Halo', note: 'Immersive and focused', icon: 'bi-circle-half' },
                { id: 'executive', name: 'Executive', note: 'Clean enterprise look', icon: 'bi-buildings' },
                { id: 'verdant', name: 'Verdant Glass', note: 'Calm scenic verify', icon: 'bi-tree' }
            ]
        }
    };

    // Left-panel sections remember their open/closed state per browser.
    var SECTIONS_KEY = 'ppd.left.sections';
    function sectionStates() {
        try { return JSON.parse(localStorage.getItem(SECTIONS_KEY)) || {}; } catch (e) { return {}; }
    }
    function saveSectionState(key, open) {
        var s = sectionStates(); s[key] = !!open;
        try { localStorage.setItem(SECTIONS_KEY, JSON.stringify(s)); } catch (e) { /* private mode */ }
    }

    /* ---------------- LEFT PANEL ---------------- */
    function buildLeft() {
        // tables
        var tl = qs('#ppdTableList');
        if (tl) tl.innerHTML = store.tables().map(function (t) {
            var types = store.pageTypesFor(t.name);
            return '<button class="ppd-list-item" data-table="' + esc(t.name) + '">' +
                icon(t.type === 'VIEW' ? 'bi-eye' : 'bi-table') + '<span>' + esc(t.name) + '</span><em>' + types.length + '</em></button>';
        }).join('') || '<div class="ppd-muted">No tables.</div>';

        // common pages
        var cl = qs('#ppdCommonList');
        if (cl) cl.innerHTML = Object.keys(COMMON).map(function (k) {
            var gallery = GALLERIES[k] ? '<em title="' + GALLERIES[k].templates.length + ' templates">' + GALLERIES[k].templates.length + '</em>' : '';
            return '<button class="ppd-list-item" data-common="' + esc(k) + '">' + icon('bi-window') + '<span>' + esc(COMMON[k]) + '</span>' + gallery + '</button>';
        }).join('');
        var cc = qs('#ppdCommonCount');
        if (cc) cc.textContent = Object.keys(COMMON).length;

        // components palette
        var pl = qs('#ppdCompList');
        if (pl) {
            var groups = reg.palette();
            pl.innerHTML = Object.keys(groups).map(function (cat) {
                return '<div class="ppd-cat-label">' + esc(cat) + '</div><div class="ppd-comp-grid">' +
                    groups[cat].map(function (c) {
                        return '<button class="ppd-comp-card' + (c.hot ? ' is-hot' : '') + '" draggable="true" data-comp="' + esc(c.type) + '">' + (c.hot ? '<span class="ppd-comp-hot">Hot</span>' : '') + icon(c.icon) + '<span>' + esc(c.label) + '</span></button>';
                    }).join('') + '</div>';
            }).join('');
        }
        renderCustomPages();
        bindLeft();
    }

    // Every user-made custom page (custom_<slug>) across all tables, grouped in
    // the sidebar so they're findable without hunting through each table's ribbon.
    function renderCustomPages() {
        var xl = qs('#ppdCustomList'); if (!xl) return;
        var items = [];
        store.tables().forEach(function (t) {
            store.pageTypesFor(t.name).forEach(function (pt) {
                if (pt.indexOf('custom_') === 0) items.push({ table: t.name, pt: pt, label: pageTabLabel(pt, t.name) });
            });
        });
        // Standalone (table-less) pages, read straight from the designer state.
        var stdPages = (store.designer().pages || {})[STANDALONE] || {};
        Object.keys(stdPages).forEach(function (pt) {
            if (pt.indexOf('custom_') === 0) items.push({ table: STANDALONE, pt: pt, label: pageTabLabel(pt, STANDALONE), standalone: true });
        });
        var rows = items.length
            ? items.map(function (it) {
                var badge = it.standalone ? '<em class="ppd-badge-std" title="Standalone — not bound to a table">◇ standalone</em>' : '<em title="on table ' + esc(it.table) + '">' + esc(it.table) + '</em>';
                return '<div class="ppd-list-item ppd-custom-item" data-custom-table="' + esc(it.table) + '" data-custom-pt="' + esc(it.pt) + '" role="button" tabindex="0" title="' + esc(it.label) + (it.standalone ? ' — standalone page' : ' — on ' + esc(it.table)) + '">'
                    + icon(it.standalone ? 'bi-file-earmark-break' : 'bi-file-earmark-richtext') + '<span>' + esc(it.label) + '</span>'
                    + badge
                    + '<span class="ppd-item-actions">'
                    + '<button type="button" class="ppd-item-act" data-act="rename" data-ppd-tip="Rename this page"><i class="bi bi-pencil"></i></button>'
                    + '<button type="button" class="ppd-item-act danger" data-act="delete" data-ppd-tip="Delete this page"><i class="bi bi-trash"></i></button>'
                    + '</span></div>';
            }).join('')
            : '<div class="ppd-muted" style="padding:6px 6px;font-size:11.5px;line-height:1.5">No custom pages yet — create one below to sketch a report, chart or mobile screen.</div>';
        // A "New custom page" action is always available at the foot of the group.
        rows += '<button type="button" class="ppd-list-item ppd-newpage" data-new-custom data-ppd-tip="Create a blank custom page under any table">' + icon('bi-plus-lg') + '<span>New custom page</span></button>';
        // Any project can drop a full starter layout onto the current CUSTOM page.
        // Standalone pages get the full page frame; a DB-project custom page keeps
        // the app shell and the marketing sections drop in as its content.
        rows += '<button type="button" class="ppd-list-item ppd-newpage" data-apply-layout data-ppd-tip="Apply a starter layout to the current custom page (SaaS, Agency, Wedding…)">' + icon('bi-grid-1x2') + '<span>Apply a layout</span></button>';
        xl.innerHTML = rows;
        var xc = qs('#ppdCustomCount'); if (xc) xc.textContent = items.length || '';
        qsa('#ppdCustomList [data-custom-pt]').forEach(function (el) {
            el.addEventListener('click', function (e) {
                var act = e.target.closest('[data-act]');
                var table = el.getAttribute('data-custom-table'), pt = el.getAttribute('data-custom-pt');
                if (act) {
                    e.stopPropagation();
                    if (act.getAttribute('data-act') === 'rename') renameCustomPage(pt, table);
                    else deleteCustomPage(pt, table);
                    return;
                }
                selectCustomPage(table, pt);
            });
            el.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectCustomPage(el.getAttribute('data-custom-table'), el.getAttribute('data-custom-pt')); } });
        });
        var addBtn = qs('#ppdCustomList [data-new-custom]');
        if (addBtn) addBtn.addEventListener('click', addCustomPageFromSidebar);
        var layoutBtn = qs('#ppdCustomList [data-apply-layout]');
        if (layoutBtn) layoutBtn.addEventListener('click', applyStarterToCurrentPage);
    }

    function bindLeft() {
        qsa('[data-ppd-tab]').forEach(function (b) {
            b.addEventListener('click', function () {
                var tab = b.getAttribute('data-ppd-tab');
                store.setUi({ leftTab: tab });
                qsa('[data-ppd-tab]').forEach(function (x) { x.classList.toggle('active', x === b); });
                qsa('[data-ppd-section]').forEach(function (s) { s.classList.toggle('active', s.getAttribute('data-ppd-section') === tab); });
                syncFootVisibility(tab);
            });
        });
        bindFooter();
        syncFootVisibility(store.ui('leftTab'));
        var saved = sectionStates();
        qsa('[data-ppd-toggle]').forEach(function (b) {
            var key = b.getAttribute('data-ppd-toggle');
            var body = qs('[data-ppd-panel="' + key + '"]');
            if (body && typeof saved[key] === 'boolean') {
                body.classList.toggle('open', saved[key]);
                b.setAttribute('aria-expanded', saved[key] ? 'true' : 'false');
            }
            b.addEventListener('click', function () {
                if (!body) return;
                var open = body.classList.toggle('open');
                b.setAttribute('aria-expanded', open ? 'true' : 'false');
                saveSectionState(key, open);
            });
        });
        var ts = qs('#ppdTableSearch');
        if (ts) ts.addEventListener('input', function () { filter('#ppdTableList .ppd-list-item', ts.value); });
        var cs = qs('#ppdCompSearch');
        if (cs) cs.addEventListener('input', function () { filter('#ppdCompList .ppd-comp-card', cs.value); });

        qsa('[data-table]').forEach(function (b) { b.addEventListener('click', function () { selectTable(b.getAttribute('data-table')); }); });
        qsa('[data-common]').forEach(function (b) { b.addEventListener('click', function () { selectCommon(b.getAttribute('data-common')); }); });
        qsa('[data-template]').forEach(function (b) { b.addEventListener('click', function () { selectTemplate(b.getAttribute('data-template')); }); });
        qsa('[data-comp]').forEach(function (b) {
            b.addEventListener('click', function () { var id = model.add(b.getAttribute('data-comp')); store.setUi({ selectedId: id }); });
            b.addEventListener('dragstart', function (e) { b.classList.add('dragging'); e.dataTransfer.effectAllowed = 'copy'; e.dataTransfer.setData('text/ppd-type', b.getAttribute('data-comp')); });
            b.addEventListener('dragend', function () { b.classList.remove('dragging'); });
        });
    }

    /* ---------------- FOOTER (Common + Templates popovers) ---------------- */
    // The footer belongs to the Pages tab only — Components/Fields don't navigate
    // pages, so hide it there to keep those tabs clean.
    function syncFootVisibility(tab) {
        var foot = qs('#ppdLeftFoot');
        // Standalone projects have no database, so Common Pages (login/auth…) and
        // Default Templates (per-table page designs) don't apply — hide the footer.
        if (foot) foot.hidden = isStandaloneProject() || (tab || 'pages') !== 'pages';
        if (foot && foot.hidden) closeFootPops();
    }
    function closeFootPops() {
        qsa('.ppd-foot-pop').forEach(function (p) { p.hidden = true; });
        qsa('.ppd-foot-chip').forEach(function (c) { c.classList.remove('open'); c.setAttribute('aria-expanded', 'false'); });
    }
    // When the page currently open in the canvas belongs to a footer group
    // (a Common page, or a Default Template), flag that chip as "current": it
    // expands to show the open page's name and the sibling compresses, so the
    // footer doubles as a "you are here" marker for those groups.
    function syncFootActive() {
        var foot = qs('#ppdLeftFoot'); if (!foot) return;
        var u = store.ui();
        var curKey = u.scope === 'common' ? 'common' : (u.scope === 'template' ? 'templates' : '');
        var nowLabel = '';
        if (curKey === 'common') nowLabel = COMMON[u.commonPage] || title(u.commonPage);
        else if (curKey === 'templates') nowLabel = title(u.pageType);
        qsa('.ppd-foot-chip').forEach(function (chip) {
            var key = chip.getAttribute('data-foot-pop');
            var cur = key === curKey;
            chip.classList.toggle('is-current', cur);
            var now = qs('.ppd-foot-now', chip);
            if (now) now.textContent = cur ? nowLabel : '';
        });
        foot.classList.toggle('has-current', !!curKey);
    }
    function openFootPop(key) {
        var foot = qs('#ppdLeftFoot'); if (foot && foot.hidden) return;
        var pop = qs('.ppd-foot-pop[data-foot-panel="' + key + '"]');
        var chip = qs('.ppd-foot-chip[data-foot-pop="' + key + '"]');
        closeFootPops();
        if (pop) pop.hidden = false;
        if (chip) { chip.classList.add('open'); chip.setAttribute('aria-expanded', 'true'); }
    }
    function bindFooter() {
        qsa('.ppd-foot-chip').forEach(function (chip) {
            chip.addEventListener('click', function (e) {
                e.stopPropagation();
                var key = chip.getAttribute('data-foot-pop');
                var pop = qs('.ppd-foot-pop[data-foot-panel="' + key + '"]');
                var willOpen = pop && pop.hidden;
                closeFootPops();
                if (pop && willOpen) {
                    pop.hidden = false;
                    chip.classList.add('open'); chip.setAttribute('aria-expanded', 'true');
                    // Opening the Common chip always starts on the page list (browse
                    // mode), even if a gallery page is the active one.
                    if (key === 'common') setCommonPopView(false, '');
                }
            });
        });
        qsa('[data-foot-close]').forEach(function (x) { x.addEventListener('click', function (e) { e.stopPropagation(); closeFootPops(); }); });
        // Back arrow in the Common popover: leave the template gallery, show the page list.
        qsa('[data-foot-back]').forEach(function (b) { b.addEventListener('click', function (e) { e.stopPropagation(); setCommonPopView(false, ''); }); });
        // Click-away + Esc close the popovers.
        if (!bindFooter._doc) {
            bindFooter._doc = true;
            document.addEventListener('click', function (e) {
                if (!e.target.closest('.ppd-left-foot')) closeFootPops();
            });
            document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeFootPops(); });
        }
    }

    /* ---------------- RIBBON scroll nav ---------------- */
    // Compact ‹ › buttons flanking the page-type ribbon; shown only when the
    // ribbon overflows, so a table with many page-types stays fully reachable
    // without stretching the toolbar or wrapping.
    function bindRibbonNav() {
        var ribbon = qs('#ppdRibbon');
        var prev = qs('[data-ribbon-nav="prev"]'), next = qs('[data-ribbon-nav="next"]');
        if (!ribbon || !prev || !next) return;
        var by = function (dir) { ribbon.scrollBy({ left: dir * Math.max(120, ribbon.clientWidth * 0.7), behavior: 'smooth' }); };
        if (!bindRibbonNav._bound) {
            bindRibbonNav._bound = true;
            prev.addEventListener('click', function () { by(-1); });
            next.addEventListener('click', function () { by(1); });
            ribbon.addEventListener('scroll', syncRibbonNav);
            window.addEventListener('resize', syncRibbonNav);
        }
        syncRibbonNav();
    }
    function syncRibbonNav() {
        var ribbon = qs('#ppdRibbon'); if (!ribbon) return;
        var prev = qs('[data-ribbon-nav="prev"]'), next = qs('[data-ribbon-nav="next"]');
        var overflow = ribbon.scrollWidth - ribbon.clientWidth > 2;
        var x = ribbon.scrollLeft;
        if (prev) prev.hidden = !overflow || x <= 1;
        if (next) next.hidden = !overflow || x >= (ribbon.scrollWidth - ribbon.clientWidth - 1);
        var wrap = qs('#ppdRibbonWrap'); if (wrap) wrap.classList.toggle('has-nav', overflow);
    }

    /* ---------------- last-page memory (per project, per browser) --------- */
    function lastPageKey() {
        var b = window.PpDesignerBootstrap || {};
        return 'ppd.lastpage.' + (b.projectSlug || 'default');
    }
    function saveLastPage() {
        var u = store.ui();
        try {
            localStorage.setItem(lastPageKey(), JSON.stringify({
                scope: u.scope, table: u.table, pageType: u.pageType, commonPage: u.commonPage
            }));
        } catch (e) { /* private mode */ }
    }
    function readLastPage() {
        try { return JSON.parse(localStorage.getItem(lastPageKey())) || null; } catch (e) { return null; }
    }
    // Reopen the page the user last worked on (this project, this browser), if it
    // still exists. Returns true when it restored something. Called on boot.
    function restoreLastPage() {
        var a = readLastPage(); if (!a || !a.scope) return false;
        if (a.scope === 'common' && a.commonPage && COMMON[a.commonPage]) { selectCommon(a.commonPage); return true; }
        if (a.scope === 'template' && a.pageType) { selectTemplate(a.pageType); return true; }
        if (a.scope === 'table' && a.table) {
            var exists = store.tables().some(function (t) { return t.name === a.table; });
            if (!exists) return false;
            var pt = a.pageType, avail = store.pageTypesFor(a.table);
            if (avail.indexOf(pt) < 0) pt = avail[0] || 'list';
            if (pt.indexOf('custom_') === 0) selectCustomPage(a.table, pt); else { selectTable(a.table); if (pt !== store.ui('pageType')) setPageType(pt); }
            return true;
        }
        return false;
    }

    function filter(sel, q) {
        q = (q || '').toLowerCase();
        var visible = 0;
        qsa(sel).forEach(function (el) {
            var hit = el.textContent.toLowerCase().indexOf(q) > -1;
            el.style.display = hit ? '' : 'none';
            if (hit) visible++;
        });
        var empty = qs('#ppdTableEmpty');
        if (empty && sel.indexOf('ppdTableList') > -1) empty.hidden = visible > 0;
    }

    // generic per-page template gallery (see GALLERIES above)
    function renderGallery(pageKey) {
        var host = qs('#ppdPageTemplateList'), g = GALLERIES[pageKey];
        if (!host || !g) return;
        var page = (store.designer().common || {})[pageKey];
        var active = page && page.settings && page.settings[g.activeSetting];
        var titleEl = qs('#ppdPageTemplateTitle'), hintEl = qs('#ppdPageTemplateHint');
        if (titleEl) titleEl.textContent = g.title;
        if (hintEl) hintEl.textContent = g.hint;
        host.innerHTML = g.templates.map(function (tpl) {
            return '<button type="button" class="ppd-login-template' + (tpl.id === active ? ' active' : '') + '" data-gallery-template="' + esc(tpl.id) + '">' +
                '<span class="ppd-login-template-preview is-' + esc(tpl.id) + '"><i class="bi ' + esc(tpl.icon) + '"></i><b></b></span>' +
                '<span><strong>' + esc(tpl.name) + '</strong><small>' + esc(tpl.note) + '</small></span><i class="bi bi-check-circle-fill"></i></button>';
        }).join('');
        qsa('[data-gallery-template]', host).forEach(function (b) {
            b.addEventListener('click', function () { chooseCommonTemplate(pageKey, b.getAttribute('data-gallery-template')); });
        });
    }

    function chooseCommonTemplate(pageKey, variant) {
        var g = GALLERIES[pageKey];
        var d = store.designer();
        var current = d.common && d.common[pageKey];
        var active = g && current && current.settings && current.settings[g.activeSetting];
        if (active === variant) return;
        var label = COMMON[pageKey] || pageKey;
        var apply = function () {
            qsa('[data-gallery-template]').forEach(function (b) { b.disabled = true; });
            PPD.api.template('', pageKey, variant).then(function (res) {
                if (!res || !res.success || !res.page) throw new Error((res && res.message) || 'Template could not be loaded.');
                model.installTemplatedPage('', pageKey, res.page);
                store.setUi({ selectedId: '', selectedPart: '' });
                afterContextChange();
                if (PPD.toast) PPD.toast(label + ' template applied');
            }).catch(function (err) {
                if (PPD.toast) PPD.toast(err.message || 'Template could not be loaded.', 'error');
            }).then(function () {
                qsa('[data-gallery-template]').forEach(function (b) { b.disabled = false; });
            });
        };
        if (!current) { apply(); return; }
        if (window.Swal) {
            Swal.fire({
                icon: 'question',
                title: 'Switch template?',
                html: 'Replace the current <b>' + esc(label) + '</b> page with this template.<br>You can undo this change.',
                showCancelButton: true,
                confirmButtonText: 'Switch template',
                cancelButtonText: 'Cancel',
                confirmButtonColor: '#6d5dfc',
                reverseButtons: true,
                focusCancel: true
            }).then(function (r) { if (r.isConfirmed) apply(); });
        } else if (window.confirm('Replace the current ' + label + ' page with this template? You can undo this change.')) {
            apply();
        }
    }

    function renderFields(table) {
        var host = qs('#ppdFieldList');
        if (!host) return;
        var fields = store.fieldsFor(table);
        if (!table) { host.innerHTML = '<div class="ppd-muted">Select a table to view fields.</div>'; return; }
        if (table === STANDALONE) { host.innerHTML = '<div class="ppd-muted">Standalone page — it isn’t tied to a table. Each component brings its own data (set a table on the component itself).</div>'; return; }
        if (!fields.length) { host.innerHTML = '<div class="ppd-muted">No fields for ' + esc(table) + '.</div>'; return; }
        host.innerHTML = fields.map(function (f) {
            return '<button class="ppd-field-chip" draggable="true" data-field="' + esc(f.name) + '" data-ftype="' + esc(f.type || 'text') + '">' +
                icon('bi-input-cursor-text') + '<span>' + esc(f.label || f.name) + '</span><small>' + esc(f.type || '') + '</small></button>';
        }).join('');
        qsa('#ppdFieldList .ppd-field-chip').forEach(function (chip) {
            chip.addEventListener('dragstart', function (e) {
                e.dataTransfer.effectAllowed = 'copy';
                e.dataTransfer.setData('text/ppd-type', 'field');
                e.dataTransfer.setData('text/ppd-field', chip.getAttribute('data-field'));
            });
            chip.addEventListener('click', function () {
                var id = model.add('field');
                model.updateProps(id, { field: chip.getAttribute('data-field') });
                model.updateNode(id, { label: title(chip.getAttribute('data-field')) });
                store.setUi({ selectedId: id });
            });
        });
    }

    /* ---------------- context switching ---------------- */
    function selectTable(table) {
        var types = store.pageTypesFor(table);
        var pt = types[0] || 'list';
        store.setUi({ scope: 'table', table: table, pageType: pt, commonPage: '', pageLabel: title(pt), selectedId: '' });
        ensurePage(table, pt, function () { afterContextChange(); });
    }
    // Open a custom page from the "Custom Pages" sidebar group: select its table
    // and switch straight to that custom page-type.
    function selectCustomPage(table, pt) {
        if (!table || !pt) return;
        store.setUi({ scope: 'table', table: table, pageType: pt, commonPage: '', pageLabel: pageTabLabel(pt, table), selectedId: '' });
        ensurePage(table, pt, function () { afterContextChange(); });
    }
    function selectCommon(key) {
        store.setUi({ scope: 'common', commonPage: key, table: '', pageType: key, pageLabel: COMMON[key] || title(key), selectedId: '' });
        ensureCommon(key, function () { afterContextChange(); });
    }
    function selectTemplate(type) {
        store.setUi({ scope: 'template', table: '', commonPage: '', pageType: type, pageLabel: 'Default ' + title(type) + ' Page', selectedId: '' });
        afterContextChange();
    }
    function setPageType(pt) {
        store.setUi({ pageType: pt, pageLabel: title(pt), selectedId: '' });
        ensurePage(store.ui('table'), pt, function () { afterContextChange(); });
    }

    // make sure the active page exists in the model; if not, build it from the
    // client-side master templates (list/forms/cards/import/export), else ask the
    // backend for a template (common/auth/dashboard pages).
    function ensurePage(table, pageType, done) {
        var d = store.designer();
        var exists = d.pages && d.pages[table] && d.pages[table][pageType] && d.pages[table][pageType].pages;
        if (exists) return done();
        var fresh = model.buildDefaultPageFor ? model.buildDefaultPageFor(table, pageType) : null;
        if (fresh) { model.installTemplatedPage(table, pageType, fresh); return done(); }
        PPD.api.template(table, pageType).then(function (res) {
            if (res && res.success && res.page) model.installTemplatedPage(table, pageType, res.page);
            done();
        }).catch(function () { done(); });
    }
    function ensureCommon(key, done) {
        var d = store.designer();
        if (d.common && d.common[key]) return done();
        PPD.api.template('', key).then(function (res) {
            if (res && res.success && res.page) model.installTemplatedPage('', key, res.page);
            done();
        }).catch(function () { done(); });
    }

    function afterContextChange() {
        renderFields(store.ui('table'));
        syncActiveStates();
        buildRibbon();
        PPD.renderer.render();
        PPD.inspector.render();
        syncContext();
        saveLastPage();
        syncFootActive();
        // Keep the Common popover open when the chosen page offers a template
        // gallery (login/register…), so its variants stay reachable; otherwise the
        // user has picked their page — close up and let them design on the canvas.
        if (store.ui('scope') === 'common' && GALLERIES[store.ui('commonPage')]) openFootPop('common');
        else closeFootPops();
    }

    function syncActiveStates() {
        var u = store.ui();
        qsa('[data-table]').forEach(function (b) { b.classList.toggle('active', u.scope === 'table' && b.getAttribute('data-table') === u.table); });
        qsa('[data-common]').forEach(function (b) { b.classList.toggle('active', u.scope === 'common' && b.getAttribute('data-common') === u.commonPage); });
        qsa('[data-template]').forEach(function (b) { b.classList.toggle('active', u.scope === 'template' && b.getAttribute('data-template') === u.pageType); });
        qsa('[data-custom-pt]').forEach(function (b) { b.classList.toggle('active', u.scope === 'table' && b.getAttribute('data-custom-table') === u.table && b.getAttribute('data-custom-pt') === u.pageType); });
        var picker = qs('#ppdPageTemplatePicker');
        var showGallery = u.scope === 'common' && !!GALLERIES[u.commonPage];
        if (picker) {
            picker.hidden = !showGallery;
            if (showGallery) renderGallery(u.commonPage);
        }
        // Drill-in: when a common page has a template gallery, the popover switches
        // to a focused view (page list hidden, templates get the full height) so the
        // templates are never stranded below a scroll. A Back arrow returns to the list.
        setCommonPopView(showGallery, showGallery ? (COMMON[u.commonPage] || title(u.commonPage)) : '');
    }
    // Toggle the Common popover between "list" and "gallery" (drill-in) views.
    function setCommonPopView(gallery, label) {
        var pop = qs('.ppd-foot-pop[data-foot-panel="common"]'); if (!pop) return;
        pop.classList.toggle('is-gallery', !!gallery);
        var back = qs('.ppd-foot-pop-back', pop); if (back) back.hidden = !gallery;
        var t = qs('#ppdCommonPopTitle'); if (t) t.textContent = gallery ? label : 'Common Pages';
        var body = qs('.ppd-foot-pop-body', pop); if (body) body.scrollTop = 0;
    }

    /* ---------------- WORKBAR ---------------- */
    // A custom page tab shows the user's page name (its stored title), not the
    // title-cased `custom_<slug>` key.
    function pageTabLabel(t, table) {
        if (t.indexOf('custom_') === 0) {
            var d = store.designer();
            var slot = d.pages && d.pages[table] && d.pages[table][t];
            var pg = slot && slot.pages && slot.pages[slot.defaultPage || t];
            if (pg && pg.title) return pg.title;
        }
        return title(t);
    }
    function buildRibbon() {
        var ribbon = qs('#ppdRibbon');
        if (!ribbon) return;
        var u = store.ui();
        var isTable = u.scope !== 'common' && u.scope !== 'template';
        var types = isTable ? store.pageTypesFor(u.table) : [u.pageType];
        var html = types.map(function (t) {
            var custom = t.indexOf('custom_') === 0;
            return '<button data-pt="' + esc(t) + '" class="' + (t === u.pageType ? 'active' : '') + (custom ? ' ppd-pt-custom' : '') +
                '"' + (custom ? ' title="Double-click to rename"' : '') + '><span class="ppd-pt-lbl">' + esc(pageTabLabel(t, u.table)) + '</span>' +
                (custom ? '<span class="ppd-pt-del" title="Delete this page" aria-label="Delete page">&times;</span>' : '') + '</button>';
        }).join('');
        // Add a blank custom page (report / chart / mobile list …) — table scope only.
        if (isTable && u.table) {
            html += '<button class="ppd-add-page" title="Add a blank custom page — build a report, chart or mobile-tailored screen from scratch">' + icon('bi-plus-lg') + '<span>Page</span></button>';
        }
        ribbon.innerHTML = html;
        qsa('button[data-pt]', ribbon).forEach(function (b) {
            var pt = b.getAttribute('data-pt');
            b.addEventListener('click', function (e) {
                if (!isTable) return; // common/template are single-page
                if (e.target.closest('.ppd-pt-del')) { e.stopPropagation(); deleteCustomPage(pt); return; }
                setPageType(pt);
            });
            if (pt.indexOf('custom_') === 0) {
                b.addEventListener('dblclick', function (e) { e.preventDefault(); renameCustomPage(pt); });
            }
        });
        var addBtn = qs('.ppd-add-page', ribbon);
        if (addBtn) addBtn.addEventListener('click', addCustomPage);
        bindRibbonNav();
    }
    // Create the page via the API, register it, and switch to it. Shared by the
    // ribbon "+ Page" button and the sidebar "New custom page" action.
    function createCustomPage(table, label, standalone, starterKey) {
        if (!label || (!table && !standalone)) return;
        PPD.api.addPage(standalone ? STANDALONE : table, label, standalone).then(function (res) {
            if (!res || !res.success) { notifyErr((res && res.message) || 'Could not add the page.'); return; }
            store.addPageType(res.table || table || STANDALONE, res.pageType);
            model.installTemplatedPage(res.table || table || STANDALONE, res.pageType, res.page);
            selectCustomPage(res.table || table || STANDALONE, res.pageType);   // switch + open the new page
            store.setUi({ pageLabel: res.label || label });
            // Optional starter layout: replace the fresh page's components with a
            // full landing blueprint (SaaS/Agency/Event) and flip to the landing frame.
            var applied = false;
            if (starterKey && PPD.starters && PPD.starters[starterKey]) {
                applied = model.applyStarter(PPD.starters[starterKey]);
            }
            renderCustomPages(); syncContext();
            if (PPD.toast) PPD.toast(applied ? '“' + (res.label || label) + '” created from the ' + starterKey + ' layout' : '“' + (res.label || label) + '” created');
        }).catch(function () { notifyErr('Could not add the page.'); });
    }
    // Standalone "New page": name + a starter-layout picker (Blank / SaaS / Agency / Event).
    function newStandalonePageWithStarter() {
        if (!window.Swal) { promptText({ title: 'New page', label: 'Page name', value: '', placeholder: 'e.g. Home, About, Pricing', confirm: 'Create page', icon: 'bi-file-earmark-plus' }, function (name) { createCustomPage(STANDALONE, (name || '').trim(), true); }); return; }
        var list = PPD.starterList || [{ key: 'blank', label: 'Blank', icon: 'bi-file-earmark', desc: 'Start from scratch.' }];
        var cards = list.map(function (s, i) {
            var thumb = PPD.starterThumb ? PPD.starterThumb(s.key) : '<i class="bi ' + esc(s.icon) + '"></i>';
            return '<label class="ppd-starter-card' + (i === 0 ? ' is-sel' : '') + '"><input type="radio" name="ppdStarter" value="' + esc(s.key) + '"' + (i === 0 ? ' checked' : '') + '>'
                + '<span class="ppd-starter-thumb-box">' + thumb + '</span><span class="ppd-starter-name">' + esc(s.label) + '</span><small>' + esc(s.desc) + '</small></label>';
        }).join('');
        Swal.fire({
            title: 'New page', iconHtml: '<i class="bi bi-file-earmark-plus"></i>',
            html: '<div class="ppd-swal-form"><label>Page name</label><input id="ppdNewName" class="swal2-input ppd-starter-name-input" placeholder="e.g. Home, Pricing, About">'
                + '<label style="margin-top:10px;display:block">Start from</label><input id="ppdStarterSearch" class="swal2-input ppd-starter-search" placeholder="Search layouts…" autocomplete="off"><div class="ppd-starter-grid">' + cards + '</div></div>',
            width: 620, showCancelButton: true, confirmButtonText: 'Create page', focusConfirm: false,
            didOpen: function () {
                var g = Swal.getPopup();
                g.querySelectorAll('.ppd-starter-card').forEach(function (c) { c.addEventListener('click', function () { g.querySelectorAll('.ppd-starter-card').forEach(function (x) { x.classList.remove('is-sel'); }); c.classList.add('is-sel'); }); });
                wireStarterSearch(g);
                var nm = g.querySelector('#ppdNewName'); if (nm) nm.focus();
            },
            preConfirm: function () {
                var g = Swal.getPopup(); var name = (g.querySelector('#ppdNewName').value || '').trim();
                if (!name) { Swal.showValidationMessage('Please enter a page name'); return false; }
                var sel = g.querySelector('input[name=ppdStarter]:checked');
                return { name: name, starter: sel ? sel.value : 'blank' };
            }
        }).then(function (r) { if (r.isConfirmed && r.value) createCustomPage(STANDALONE, r.value.name, true, r.value.starter === 'blank' ? null : r.value.starter); });
    }
    function afterStarterApplied(key, mode) { renderCustomPages(); syncContext(); if (PPD.toast) PPD.toast((mode === 'append' ? 'Appended the ' : 'Applied the ') + key + ' layout'); }
    // Live filter for the starter-picker cards (matches label + description).
    function wireStarterSearch(g) {
        var search = g.querySelector('#ppdStarterSearch');
        if (!search) return;
        search.addEventListener('input', function () {
            var q = this.value.trim().toLowerCase();
            g.querySelectorAll('.ppd-starter-card').forEach(function (c) {
                var t = (c.textContent || '').toLowerCase();
                c.style.display = (!q || t.indexOf(q) > -1) ? '' : 'none';
            });
        });
    }
    // Apply a starter layout to the CURRENT page. If the page already has sections,
    // ask whether to Replace them or Append the layout below.
    function applyStarterToCurrentPage() {
        if (!window.Swal || !PPD.starterList) { notifyErr('Layouts need the dialog UI.'); return; }
        // In a DB project a starter can only land on a CUSTOM page — a table's
        // list/add/edit/view are field-driven and must not be overwritten with
        // marketing sections. Standalone projects are all custom pages, so skip.
        if (!isStandaloneProject() && String(store.ui('pageType') || '').indexOf('custom_') !== 0) {
            notifyErr('Open (or create) a custom page first — layouts apply to custom pages, not to list/add/edit/view.');
            return;
        }
        var list = PPD.starterList.filter(function (s) { return s.key !== 'blank'; });
        var cards = list.map(function (s, i) {
            var thumb = PPD.starterThumb ? PPD.starterThumb(s.key) : '<i class="bi ' + esc(s.icon) + '"></i>';
            return '<label class="ppd-starter-card' + (i === 0 ? ' is-sel' : '') + '"><input type="radio" name="ppdStarter" value="' + esc(s.key) + '"' + (i === 0 ? ' checked' : '') + '>'
                + '<span class="ppd-starter-thumb-box">' + thumb + '</span><span class="ppd-starter-name">' + esc(s.label) + '</span><small>' + esc(s.desc) + '</small></label>';
        }).join('');
        Swal.fire({
            title: 'Apply a layout', iconHtml: '<i class="bi bi-grid-1x2"></i>', width: 640,
            html: '<div class="ppd-swal-form"><input id="ppdStarterSearch" class="swal2-input ppd-starter-search" placeholder="Search layouts…" autocomplete="off"><div class="ppd-starter-grid">' + cards + '</div></div>',
            showCancelButton: true, confirmButtonText: 'Continue', focusConfirm: false,
            didOpen: function () { var g = Swal.getPopup(); g.querySelectorAll('.ppd-starter-card').forEach(function (c) { c.addEventListener('click', function () { g.querySelectorAll('.ppd-starter-card').forEach(function (x) { x.classList.remove('is-sel'); }); c.classList.add('is-sel'); }); }); wireStarterSearch(g); },
            preConfirm: function () { var sel = Swal.getPopup().querySelector('input[name=ppdStarter]:checked'); return sel ? sel.value : (list[0] && list[0].key); }
        }).then(function (r) {
            if (!r.isConfirmed || !r.value || !PPD.starters[r.value]) return;
            var key = r.value, page = model.activePage();
            if (!page || (page.components || []).length === 0) { model.applyStarter(PPD.starters[key]); afterStarterApplied(key, 'replace'); return; }
            Swal.fire({
                title: 'This page already has content', icon: 'question',
                html: 'Replace it with the <b>' + esc(PPD.starters[key].label || key) + '</b> layout, or append the layout below your existing sections?',
                showCancelButton: true, showDenyButton: true, confirmButtonText: 'Replace', denyButtonText: 'Append', cancelButtonText: 'Cancel'
            }).then(function (rr) {
                if (rr.isConfirmed) { model.applyStarter(PPD.starters[key], 'replace'); afterStarterApplied(key, 'replace'); }
                else if (rr.isDenied) { model.applyStarter(PPD.starters[key], 'append'); afterStarterApplied(key, 'append'); }
            });
        });
    }
    // Ribbon "+ Page": add under the ACTIVE table (name prompt only).
    function addCustomPage() {
        var table = store.ui('table');
        if (store.ui('scope') !== 'table' || !table) { notifyErr('Pick a table first, then add a page under it.'); return; }
        promptText({ title: 'New page', label: 'Page name', value: 'New Page', placeholder: 'e.g. Sales Report, Mobile List, Analytics', confirm: 'Create page', icon: 'bi-file-earmark-plus' },
            function (name) { createCustomPage(table, name); });
    }
    // A standalone PROJECT has no database — every page is a standalone page.
    function isStandaloneProject() { return !!(window.PpDesignerBootstrap || {}).standalone; }
    // Sidebar "New custom page": pick the table + name (works from any context).
    function addCustomPageFromSidebar() {
        var tables = store.tables();
        // Standalone project: no tables, so skip the picker and create a standalone
        // page directly — just ask for a name.
        if (isStandaloneProject()) {
            newStandalonePageWithStarter();
            return;
        }
        if (!tables.length) { notifyErr('Create a table first — custom pages live under a table.'); return; }
        var activeStd = store.ui('table') === STANDALONE;
        var activeT = (store.ui('scope') === 'table' && !activeStd) ? store.ui('table') : (tables[0] && tables[0].name);
        if (window.Swal) {
            // "Standalone" leads the list so a table isn't forced; then every table.
            var opts = '<option value="' + STANDALONE + '"' + (activeStd ? ' selected' : '') + '>◇ Standalone (no table)</option>'
                + tables.map(function (t) { return '<option value="' + esc(t.name) + '"' + (t.name === activeT && !activeStd ? ' selected' : '') + '>' + esc(t.name) + '</option>'; }).join('');
            Swal.fire({
                title: 'New custom page', iconHtml: '<i class="bi bi-file-earmark-plus"></i>',
                html: '<div class="ppd-swal-form">'
                    + '<label>Bind to</label><select id="ppdNewPageTable" class="swal2-select">' + opts + '</select>'
                    + '<small class="ppd-swal-hint">Pick a table to work with its records, or <b>Standalone</b> for a static / multi-table page (each component brings its own data).</small>'
                    + '<label>Page name</label><input id="ppdNewPageName" class="swal2-input" placeholder="e.g. Sales Report" value="">'
                    + '</div>',
                showCancelButton: true, confirmButtonText: 'Create page', cancelButtonText: 'Cancel',
                confirmButtonColor: '#6d5dfc', reverseButtons: true, focusConfirm: false,
                didOpen: function () { var n = document.getElementById('ppdNewPageName'); if (n) n.focus(); },
                preConfirm: function () {
                    var t = (document.getElementById('ppdNewPageTable') || {}).value || '';
                    var n = ((document.getElementById('ppdNewPageName') || {}).value || '').trim();
                    if (!n) { Swal.showValidationMessage('Please enter a page name'); return false; }
                    return { table: t, name: n };
                }
            }).then(function (r) { if (r.isConfirmed && r.value) createCustomPage(r.value.table, r.value.name, r.value.table === STANDALONE); });
        } else {
            var name = (window.prompt('Name this page', 'New Page') || '').trim();
            if (name) createCustomPage(activeT, name);
        }
    }
    function renameCustomPage(pt, table) {
        table = table || store.ui('table');
        if (!table || pt.indexOf('custom_') !== 0) return;
        var current = pageTabLabel(pt, table);
        promptText({ title: 'Rename page', label: 'Page name', value: current, placeholder: 'Page name', confirm: 'Save', icon: 'bi-pencil-square' },
            function (label) {
                label = (label || '').trim();
                if (!label || label === current) return;
                PPD.api.renamePage(table, pt, label).then(function (res) {
                    if (!res || !res.success) { notifyErr((res && res.message) || 'Could not rename the page.'); return; }
                    var d = store.designer(), slot = d.pages && d.pages[table] && d.pages[table][pt];
                    if (slot && slot.pages) { Object.keys(slot.pages).forEach(function (k) { slot.pages[k].title = label; }); }
                    if (store.ui('pageType') === pt && store.ui('table') === table) store.setUi({ pageLabel: label });
                    buildRibbon(); renderCustomPages(); syncContext();
                    if (PPD.toast) PPD.toast('Renamed to “' + label + '”');
                }).catch(function () { notifyErr('Could not rename the page.'); });
            });
    }
    function deleteCustomPage(pt, table) {
        table = table || store.ui('table');
        if (!table || pt.indexOf('custom_') !== 0) return;
        var name = pageTabLabel(pt, table);
        var apply = function () {
            PPD.api.deletePage(table, pt).then(function (res) {
                if (!res || !res.success) { notifyErr((res && res.message) || 'Could not delete the page.'); return; }
                var wasActive = store.ui('pageType') === pt && store.ui('table') === table;
                store.removePageType(table, pt);
                if (wasActive) { setPageType(store.pageTypesFor(table)[0] || 'list'); }
                else { buildRibbon(); }
                renderCustomPages();
                if (PPD.toast) PPD.toast('“' + name + '” deleted');
            }).catch(function () { notifyErr('Could not delete the page.'); });
        };
        if (window.Swal) {
            Swal.fire({
                icon: 'warning', title: 'Delete “' + name + '”?',
                html: 'This custom page will be removed from the app and its menu link on the next build. <b>This can’t be undone.</b>'
                    + '<div class="ppd-swal-form" style="margin-top:14px"><label>Type <b>' + esc(name) + '</b> to confirm</label>'
                    + '<input id="ppdDelConfirm" class="swal2-input" placeholder="' + esc(name) + '" autocomplete="off" spellcheck="false"></div>',
                showCancelButton: true, confirmButtonText: '<i class="bi bi-trash"></i> Delete page', cancelButtonText: 'Keep it',
                confirmButtonColor: '#dc2626', reverseButtons: true, focusCancel: false,
                didOpen: function () {
                    var btn = Swal.getConfirmButton(); var inp = document.getElementById('ppdDelConfirm');
                    if (btn) btn.disabled = true;
                    if (inp) {
                        inp.focus();
                        inp.addEventListener('input', function () { if (btn) btn.disabled = inp.value.trim() !== name; });
                        inp.addEventListener('keydown', function (e) { if (e.key === 'Enter' && btn && !btn.disabled) Swal.clickConfirm(); });
                    }
                },
                preConfirm: function () {
                    var v = ((document.getElementById('ppdDelConfirm') || {}).value || '').trim();
                    if (v !== name) { Swal.showValidationMessage('The name doesn’t match — type it exactly to delete'); return false; }
                    return true;
                }
            }).then(function (r) { if (r.isConfirmed) apply(); });
        } else {
            var typed = window.prompt('Type the page name "' + name + '" to delete it:');
            if (typed !== null && typed.trim() === name) apply();
        }
    }
    // Small helpers: an attractive text prompt (Swal, prompt fallback) + an error toast.
    function promptText(o, cb) {
        if (window.Swal) {
            Swal.fire({
                title: o.title, iconHtml: o.icon ? '<i class="bi ' + o.icon + '"></i>' : undefined,
                input: 'text', inputValue: o.value || '', inputLabel: o.label || '', inputPlaceholder: o.placeholder || '',
                showCancelButton: true, confirmButtonText: o.confirm || 'OK', cancelButtonText: 'Cancel',
                confirmButtonColor: '#6d5dfc', reverseButtons: true,
                inputValidator: function (v) { if (!v || !v.trim()) return 'Please enter a name'; }
            }).then(function (r) { if (r.isConfirmed) cb(r.value); });
        } else { cb(window.prompt(o.title, o.value || '')); }
    }
    function notifyErr(msg) { if (PPD.toast) PPD.toast(msg, 'error'); else window.alert(msg); }
    function syncContext() {
        var u = store.ui();
        var isStd = u.table === STANDALONE;
        var kicker = u.scope === 'common' ? 'Common' : u.scope === 'template' ? 'Template' : isStd ? 'Standalone' : (u.table || 'No table');
        set('#ppdCtxTable', kicker);
        set('#ppdCtxPage', title(u.pageLabel || u.pageType || 'Select a page'));
        var meta = u.scope === 'common' ? 'common page' : u.scope === 'template' ? ('applies to all ' + (u.pageType || 'list') + ' pages') : isStd ? 'standalone page' : ((u.table || 'table') + ' / ' + (u.pageType || 'list'));
        set('#ppdCtxMeta', meta);
    }
    function set(sel, v) { var el = qs(sel); if (el) el.textContent = v; }

    function bindWorkbar() {
        qsa('[data-device]').forEach(function (b) {
            b.addEventListener('click', function () {
                store.setUi({ device: b.getAttribute('data-device') });
                qsa('[data-device]').forEach(function (x) { x.classList.toggle('active', x === b); });
                PPD.renderer.render();
            });
        });
        bindClick('#ppdOpenLeft', function () { setCollapsed('left'); });
        bindClick('#ppdHideLeft', function () { setCollapsed('left', true); });
        bindClick('#ppdToggleRight', function () { setCollapsed('right'); });
        bindClick('#ppdHideRight', function () { setCollapsed('right', true); });
        var prev = qs('#ppdPreview'); if (prev) prev.addEventListener('click', function () { store.emit('request:preview'); });
        syncToggleStates();
    }
    function bindClick(sel, fn) { var el = qs(sel); if (el) el.addEventListener('click', fn); }
    function setCollapsed(side, force) {
        var cls = side === 'left' ? 'ppd-left-collapsed' : 'ppd-right-collapsed';
        if (force === true) document.body.classList.add(cls);
        else document.body.classList.toggle(cls);
        syncToggleStates();
        PPD.renderer.render();
    }
    function syncToggleStates() {
        var ol = qs('#ppdOpenLeft'); if (ol) ol.classList.toggle('is-off', document.body.classList.contains('ppd-left-collapsed'));
        var tr = qs('#ppdToggleRight'); if (tr) tr.classList.toggle('active', !document.body.classList.contains('ppd-right-collapsed'));
    }

    /* ---------------- STATUS BAR ---------------- */
    function bindStatus() {
        var u = qs('#ppdUndo'), r = qs('#ppdRedo'), zi = qs('#ppdZoomIn'), zo = qs('#ppdZoomOut');
        if (u) u.addEventListener('click', function () { store.undo(); });
        if (r) r.addEventListener('click', function () { store.redo(); });
        if (zi) zi.addEventListener('click', function () { zoom(10); });
        if (zo) zo.addEventListener('click', function () { zoom(-10); });
        store.on('history', syncStatus);
        syncStatus();
    }
    function zoom(delta) {
        var z = Math.max(50, Math.min(150, (store.ui('zoom') || 100) + delta));
        store.setUi({ zoom: z });
        set('#ppdZoomLabel', z + '%'); set('#ppdZoomStatus', 'Zoom: ' + z + '%');
        PPD.renderer.render();
    }
    function syncStatus() {
        var u = qs('#ppdUndo'), r = qs('#ppdRedo');
        if (u) u.disabled = !store.canUndo();
        if (r) r.disabled = !store.canRedo();
        var sel = store.ui('selectedId');
        var page = model.activePage();
        var hit = page && sel ? model.find(page, sel) : null;
        set('#ppdSelStatus', 'Selection: ' + (hit && hit.node ? (hit.node.label || hit.node.type) : (page ? title(page.type) + ' page' : 'none')));
    }

    function boot() {
        buildLeft();
        bindWorkbar();
        bindStatus();
        buildRibbon();
        syncContext();
        store.on('ui', function (changed) {
            if (!changed || typeof changed !== 'object') return;   // ignore payload-less emits
            if ('selectedId' in changed || 'selectedPart' in changed || 'selectedCells' in changed) { PPD.renderer.render(); PPD.inspector.render(); syncStatus(); }
            if ('dirty' in changed) { var s = qs('#ppdSave'); if (s) s.classList.toggle('is-dirty', changed.dirty); }
        });
        // model changes re-render the canvas live, but NOT the inspector (so typing keeps focus);
        // undo/redo carry fromHistory and do refresh the inspector to reflect reverted values.
        store.on('model', function (p) { PPD.renderer.render(); if (p && p.fromHistory) PPD.inspector.render(); syncStatus(); });
        // A List setting (freeze / views / group / kanban) changed in the inspector:
        // re-derive the toolbar (Views widget gating) and re-render so the canvas
        // reflects it live — WYSIWYG with what the generated app will build.
        store.on('listSettings', function () { if (PPD.model.reconcileActiveList) PPD.model.reconcileActiveList(); PPD.renderer.render(); });
        // A Table setting (records-per-page, sort, row highlight, loading box…)
        // changed: most are generated-app runtime behaviours, but keep the canvas
        // in sync so any visual-affecting one (row highlight) updates live.
        store.on('tableSettings', function () { if (PPD.model.reconcileActiveList) PPD.model.reconcileActiveList(); PPD.renderer.render(); });
        store.on('rendered', syncStatus);
    }

    PPD.panels = { boot: boot, renderFields: renderFields, selectTable: selectTable, selectCommon: selectCommon, selectTemplate: selectTemplate, afterContextChange: afterContextChange, restoreLastPage: restoreLastPage };
})();
