diff --git a/containers/libreportal/frontend/css/backup.css b/containers/libreportal/frontend/css/backup.css index 94be310..06f0452 100755 --- a/containers/libreportal/frontend/css/backup.css +++ b/containers/libreportal/frontend/css/backup.css @@ -865,6 +865,50 @@ color: var(--text-primary); } +/* Export dropdown in the backup page header (Configuration tab). */ +#backup-page-header .page-header-actions { + position: relative; +} + +.backup-export-menu { + position: absolute; + top: calc(100% + 6px); + right: 0; + z-index: 50; + min-width: 210px; + padding: 6px; + background: var(--card-bg); + border: 1px solid var(--card-border, var(--border-color)); + border-radius: 10px; + box-shadow: var(--card-shadow, 0 8px 24px rgba(0, 0, 0, 0.25)); + display: flex; + flex-direction: column; + gap: 2px; +} + +.backup-export-menu[hidden] { + display: none; +} + +.backup-export-menu-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + text-align: left; + padding: 8px 10px; + background: transparent; + border: none; + border-radius: 6px; + color: var(--text-primary); + font-size: 0.85rem; + cursor: pointer; +} + +.backup-export-menu-item:hover { + background: rgba(var(--text-rgb), 0.08); +} + .backup-warning-banner [data-action="export-passwords"] { flex-shrink: 0; white-space: nowrap; diff --git a/containers/libreportal/frontend/html/backup-content.html b/containers/libreportal/frontend/html/backup-content.html index 5967183..a940f78 100644 --- a/containers/libreportal/frontend/html/backup-content.html +++ b/containers/libreportal/frontend/html/backup-content.html @@ -55,6 +55,16 @@ Refresh +
diff --git a/containers/libreportal/frontend/js/components/backup/backup-page.js b/containers/libreportal/frontend/js/components/backup/backup-page.js index cb528da..7eff02b 100644 --- a/containers/libreportal/frontend/js/components/backup/backup-page.js +++ b/containers/libreportal/frontend/js/components/backup/backup-page.js @@ -136,6 +136,14 @@ class BackupPage { // the URL via parseTabFromUrl() at init time. document.addEventListener('click', (e) => { + // Clicking outside the export dropdown (and not on its trigger) closes it. + const exportMenu = document.getElementById('backup-export-menu'); + if (exportMenu && !exportMenu.hidden + && !e.target.closest('#backup-export-menu') + && !e.target.closest('#backup-primary-action')) { + this.toggleExportMenu(false); + } + const tabBtn = e.target.closest('.backup-layout .sidebar .category[data-backup-tab]'); if (tabBtn) { this.switchTab(tabBtn.dataset.backupTab); @@ -201,7 +209,7 @@ class BackupPage { if (engineBtn) { this.openEngineDetailsModal(engineBtn); return; } const exportBtn = e.target.closest('[data-action="export-passwords"]'); - if (exportBtn) { this.exportRepositoryPasswords(exportBtn); return; } + if (exportBtn) { this.toggleExportMenu(false); this.exportRepositoryPasswords(exportBtn); return; } const dismissWarn = e.target.closest('[data-action="dismiss-config-warning"]'); if (dismissWarn) { @@ -381,6 +389,8 @@ class BackupPage { updatePrimaryAction() { const btn = document.getElementById('backup-primary-action'); if (!btn) return; + // Switching tabs always closes the export dropdown. + this.toggleExportMenu(false); if (this.currentTab === 'locations') { btn.innerHTML = ` @@ -390,6 +400,21 @@ class BackupPage { Add location `; btn.dataset.intent = 'add-location'; + btn.removeAttribute('aria-haspopup'); + } else if (this.currentTab === 'configuration') { + btn.innerHTML = ` + + + + + + Export + + + + `; + btn.dataset.intent = 'export-menu'; + btn.setAttribute('aria-haspopup', 'menu'); } else { btn.innerHTML = ` @@ -400,6 +425,7 @@ class BackupPage { Backup all apps `; btn.dataset.intent = 'backup-all'; + btn.removeAttribute('aria-haspopup'); } } @@ -407,11 +433,22 @@ class BackupPage { const intent = document.getElementById('backup-primary-action')?.dataset.intent; if (intent === 'add-location') { this.openAddLocationModal(); + } else if (intent === 'export-menu') { + this.toggleExportMenu(); } else { this.runBackupAllApps(); } } + toggleExportMenu(force) { + const menu = document.getElementById('backup-export-menu'); + const btn = document.getElementById('backup-primary-action'); + if (!menu) return; + const show = typeof force === 'boolean' ? force : menu.hidden; + menu.hidden = !show; + if (btn) btn.setAttribute('aria-expanded', show ? 'true' : 'false'); + } + render() { this.renderDashboard(); this.renderLocations(); @@ -808,14 +845,6 @@ class BackupPage { Keep your LibrePortal config backed up offline. Repository passwords live inside the config directory. Without that backup, snapshots cannot be decrypted by anyone — including you.
-