From 73dae688abb9b257981c0de40998661b07f4c6b3 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Mon, 27 Apr 2026 02:07:34 +0200 Subject: [PATCH] popup --- modules/pi_control/assets/pi_control.css | 24 -------- modules/pihole/assets/pihole.css | 28 ---------- modules/pihole/assets/pihole_instances.js | 34 ++++++++---- public/assets/css/app.css | 51 +++++++++++++++++ public/assets/js/app.js | 68 +++++++++++++++++++++++ 5 files changed, 143 insertions(+), 62 deletions(-) diff --git a/modules/pi_control/assets/pi_control.css b/modules/pi_control/assets/pi_control.css index 32b9b61..b54a4f9 100644 --- a/modules/pi_control/assets/pi_control.css +++ b/modules/pi_control/assets/pi_control.css @@ -269,32 +269,8 @@ padding: 0 6px; } -.modal { - position: fixed; - inset: 0; - background: rgba(10, 14, 24, 0.55); - display: none; - align-items: center; - justify-content: center; - padding: 24px; - z-index: 40; -} -.modal.is-open { display: flex; } .modal-card { width: min(1100px, 96vw); - max-height: 90vh; - overflow: auto; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 16px; - box-shadow: var(--shadow); - padding: 16px; -} -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; } .modal-actions { display: inline-flex; diff --git a/modules/pihole/assets/pihole.css b/modules/pihole/assets/pihole.css index 9a3c512..3775e1e 100644 --- a/modules/pihole/assets/pihole.css +++ b/modules/pihole/assets/pihole.css @@ -290,34 +290,6 @@ font-size: 1.1rem; } -.modal { - position: fixed; - inset: 0; - background: rgba(10, 14, 24, 0.55); - display: none; - align-items: center; - justify-content: center; - padding: 24px; - z-index: 40; -} -.modal.is-open { display: flex; } -.modal-card { - width: min(880px, 96vw); - max-height: 90vh; - overflow: auto; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 16px; - box-shadow: var(--shadow); - padding: 16px; -} -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - @media (max-width: 680px) { .pihole-actions { flex-direction: column; diff --git a/modules/pihole/assets/pihole_instances.js b/modules/pihole/assets/pihole_instances.js index 34beada..4206687 100644 --- a/modules/pihole/assets/pihole_instances.js +++ b/modules/pihole/assets/pihole_instances.js @@ -66,7 +66,15 @@ if (primaryInput) primaryInput.checked = false; }; + const modalApi = window.NexusModal && typeof window.NexusModal.create === 'function' + ? window.NexusModal.create(modal, { initialFocus: 'input[name="instance_id"]' }) + : null; + const openModal = () => { + if (modalApi) { + modalApi.open(); + return; + } modal.classList.add('is-open'); modal.setAttribute('aria-hidden', 'false'); if (idInput) { @@ -75,6 +83,10 @@ }; const closeModal = () => { + if (modalApi) { + modalApi.close(); + return; + } modal.classList.remove('is-open'); modal.setAttribute('aria-hidden', 'true'); }; @@ -113,15 +125,17 @@ }); } - modal.addEventListener('click', (event) => { - if (event.target === modal) { - closeModal(); - } - }); + if (!modalApi) { + modal.addEventListener('click', (event) => { + if (event.target === modal) { + closeModal(); + } + }); - document.addEventListener('keydown', (event) => { - if (event.key === 'Escape' && modal.classList.contains('is-open')) { - closeModal(); - } - }); + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && modal.classList.contains('is-open')) { + closeModal(); + } + }); + } })(); diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 7490a65..80d97a9 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -198,6 +198,57 @@ a { margin-bottom: 8px; } +body.has-modal-open { + overflow: hidden; +} + +.modal { + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + padding: 24px; + background: + linear-gradient(180deg, rgba(8, 14, 24, 0.52), rgba(8, 14, 24, 0.64)); + backdrop-filter: blur(6px); + z-index: 70; +} + +.modal.is-open { + display: flex; +} + +.modal-card { + width: min(820px, 96vw); + max-height: 90vh; + overflow: auto; + padding: 20px; + border: 1px solid var(--line); + border-radius: 20px; + background: + linear-gradient(180deg, rgba(255,255,255,0.96), rgba(247,250,252,0.94)); + box-shadow: 0 22px 60px rgba(1, 22, 32, 0.22); +} + +:root[data-theme="night"] .modal-card { + background: + linear-gradient(180deg, rgba(17,22,32,0.98), rgba(10,14,24,0.96)); +} + +.modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.modal-actions { + display: inline-flex; + align-items: center; + gap: 8px; +} + .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); diff --git a/public/assets/js/app.js b/public/assets/js/app.js index d5b0848..adb21c5 100755 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -120,3 +120,71 @@ if (setupTabs.length > 0) { const activeSetupTab = document.querySelector('[data-setup-tab-target].is-active') || setupTabs[0]; activateSetupTab(activeSetupTab.dataset.setupTabTarget); } + +window.NexusModal = (() => { + const openModals = new Set(); + + const syncBodyState = () => { + document.body.classList.toggle('has-modal-open', openModals.size > 0); + }; + + const create = (root, options = {}) => { + if (!root) { + return null; + } + + const modal = { + root, + isOpen: () => root.classList.contains('is-open'), + open() { + root.classList.add('is-open'); + root.setAttribute('aria-hidden', 'false'); + openModals.add(root); + syncBodyState(); + if (typeof options.onOpen === 'function') { + options.onOpen(); + } + const focusTarget = options.initialFocus ? root.querySelector(options.initialFocus) : null; + if (focusTarget instanceof HTMLElement) { + window.setTimeout(() => focusTarget.focus(), 20); + } + }, + close() { + root.classList.remove('is-open'); + root.setAttribute('aria-hidden', 'true'); + openModals.delete(root); + syncBodyState(); + if (typeof options.onClose === 'function') { + options.onClose(); + } + }, + }; + + if (!root.dataset.modalBound) { + root.addEventListener('click', (event) => { + if (event.target === root) { + modal.close(); + } + }); + root.dataset.modalBound = '1'; + } + + return modal; + }; + + document.addEventListener('keydown', (event) => { + if (event.key !== 'Escape') { + return; + } + const active = Array.from(openModals).pop(); + if (!active) { + return; + } + active.classList.remove('is-open'); + active.setAttribute('aria-hidden', 'true'); + openModals.delete(active); + syncBodyState(); + }); + + return { create }; +})();