diff --git a/css/style.css b/css/style.css
index 3c290e4..b690a6f 100644
--- a/css/style.css
+++ b/css/style.css
@@ -2183,6 +2183,16 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
.settings-search-clear svg { width: 12px; height: 12px; }
.settings-search-clear:hover { color: var(--text); background: var(--bg-elev-2); }
.settings-search-wrap.has-query .settings-search-clear { display: inline-flex; }
+/* Settings tabs — group sections; JS toggles .is-tab-hidden. */
+.settings-tabs { display: flex; gap: 4px; padding: 0 16px 8px; flex-wrap: wrap; }
+.settings-tab {
+ flex: 1; min-width: 70px; padding: 6px 10px; font-size: 12px; cursor: pointer;
+ color: var(--text-dim); background: var(--bg-elev-2); border: 1px solid var(--border);
+ border-radius: 6px;
+}
+.settings-tab:hover { background: var(--bg-elev-3); }
+.settings-tab.is-active { color: var(--accent-fg); background: var(--accent); border-color: var(--accent); }
+.slideover-field.is-tab-hidden { display: none; }
/* Hidden section + per-row + no-results banner driven by JS. */
.slideover-field.is-search-hidden,
.setting-row.is-search-hidden { display: none; }
diff --git a/index.html b/index.html
index 3b54da4..014011f 100644
--- a/index.html
+++ b/index.html
@@ -413,6 +413,12 @@
+
Appearance
diff --git a/js/app.js b/js/app.js
index 5c3447e..8710b65 100644
--- a/js/app.js
+++ b/js/app.js
@@ -7808,6 +7808,31 @@ function closeSettings() {
$('#settingsPanel').classList.remove('is-open');
}
+// Settings tabs — each section (.slideover-field, keyed by its label text)
+// belongs to one tab; the active tab shows its sections, hides the rest.
+const SETTINGS_TAB_OF = {
+ 'Appearance': 'general',
+ 'Security': 'security', 'Clipboard privacy': 'security',
+ 'PIN unlock': 'security', 'Quick unlock': 'security', 'Recovery key': 'security',
+ 'Account': 'account',
+ 'Export': 'sync', 'Sync (WebDAV)': 'sync', 'Auto-backup': 'sync', 'Import': 'sync',
+};
+let settingsActiveTab = 'general';
+
+function applySettingsTab(tab) {
+ if (tab) settingsActiveTab = tab;
+ $$('#settingsTabs .settings-tab').forEach(b =>
+ b.classList.toggle('is-active', b.dataset.tab === settingsActiveTab));
+ // A live search filters rows across ALL sections — suspend the tab filter
+ // so a match in another tab isn't hidden. Empty search → tab filter on.
+ const searching = !!($('#settingsSearch') && $('#settingsSearch').value.trim());
+ $$('#settingsBody > .slideover-field').forEach(sec => {
+ const lbl = sec.querySelector('.slideover-field-label');
+ const t = lbl ? (SETTINGS_TAB_OF[lbl.textContent.trim()] || 'general') : 'general';
+ sec.classList.toggle('is-tab-hidden', !searching && t !== settingsActiveTab);
+ });
+}
+
// Filter the settings panel by text. Matches against the label + the
// section body so e.g. "Ctrl" finds the hotkeys section via its button
// labels. Empty query = show everything. Adds a "No matches" hint when
@@ -7874,6 +7899,7 @@ function applySettingsSearch(rawQuery) {
if (body) body.appendChild(banner);
}
banner.classList.toggle('is-visible', q.length > 0 && totalShownRows === 0);
+ applySettingsTab(); // re-apply tab filter (off while searching, on when cleared)
}
// ---- Auto-lock with 30s warning countdown -------------------
@@ -8759,6 +8785,8 @@ async function init() {
// Settings search box: live filter on every input. Escape clears the
// query (without closing the panel — the existing Esc handler also
// closes settings, but only when focus isn't inside an input).
+ $$('#settingsTabs .settings-tab').forEach(b =>
+ b.addEventListener('click', () => applySettingsTab(b.dataset.tab)));
const sInput = $('#settingsSearch');
const sClear = $('#settingsSearchClear');
if (sInput) {