diff --git a/PR_DESCRIPTION_SETTINGS_SEARCH.md b/PR_DESCRIPTION_SETTINGS_SEARCH.md
new file mode 100644
index 0000000..f6c946c
--- /dev/null
+++ b/PR_DESCRIPTION_SETTINGS_SEARCH.md
@@ -0,0 +1,17 @@
+## What and why
+
+Adds a global Settings search field. It searches setting titles and descriptions across every Settings category, shows matching results with their category, and opens the selected category. The Settings layout now responds to the Settings pane width with container queries: narrow panes stack the header, make the search field full width, and turn the vertical category sidebar into a horizontal scrollable rail so settings cards retain usable content width.
+
+## How it was checked
+
+- [x] `npm run verify` passes — 33 checks passed
+- [x] `cargo test` not required — no `src-tauri/` files changed
+- [x] `npm run build` passes
+- [x] Search check covers empty, multi-term, category, and no-match queries
+- [ ] Screenshot or clip below (UI changes)
+
+## Notes for the reviewer
+
+- Search metadata is isolated in `src/ui/components/settingsSearchIndex.ts`; the page component only owns query state and category navigation.
+- The compact layout is a named container query, not a viewport breakpoint. It responds correctly when Settings is constrained by application chrome, window scaling, or an embedded pane.
+- Selecting a search result clears the query and opens its category.
diff --git a/src/ui/components/SettingsSearch.tsx b/src/ui/components/SettingsSearch.tsx
new file mode 100644
index 0000000..ebfec2a
--- /dev/null
+++ b/src/ui/components/SettingsSearch.tsx
@@ -0,0 +1,65 @@
+import { useMemo } from "react";
+import { SearchIcon } from "../icons";
+import { searchSettings, type SettingsTab } from "./settingsSearchIndex";
+
+export type { SettingsTab } from "./settingsSearchIndex";
+
+export function SettingsSearch({
+ query,
+ onQueryChange,
+}: {
+ query: string;
+ onQueryChange: (query: string) => void;
+}) {
+ const resultCount = useMemo(() => searchSettings(query).length, [query]);
+ const active = query.trim().length > 0;
+
+ return (
+
+ );
+}
+
+export function SettingsSearchResults({
+ query,
+ onSelect,
+}: {
+ query: string;
+ onSelect: (tab: SettingsTab) => void;
+}) {
+ const results = useMemo(() => searchSettings(query), [query]);
+
+ return (
+
+ {results.length > 0 ? results.map((result) => (
+
+ )) : (
+
No settings match “{query.trim()}”.
+ )}
+
+ );
+}
diff --git a/src/ui/components/settingsSearch.check.ts b/src/ui/components/settingsSearch.check.ts
new file mode 100644
index 0000000..061d493
--- /dev/null
+++ b/src/ui/components/settingsSearch.check.ts
@@ -0,0 +1,20 @@
+export {};
+
+import { searchSettings } from "./settingsSearchIndex";
+
+function check(condition: boolean, message: string): void {
+ if (!condition) throw new Error(`FAILED: ${message}`);
+}
+
+check(searchSettings("").length === 0, "empty search has no results");
+check(
+ searchSettings("discord pause").some((entry) => entry.title === "Discord presence" && entry.tab === "about"),
+ "search matches terms across a setting description",
+);
+check(
+ searchSettings("quality").some((entry) => entry.title === "Streaming quality"),
+ "search finds a library setting",
+);
+check(searchSettings("this cannot match").length === 0, "unknown query has no results");
+
+console.log("settingsSearch.check.ts passed");
diff --git a/src/ui/components/settingsSearchIndex.ts b/src/ui/components/settingsSearchIndex.ts
new file mode 100644
index 0000000..3c4b984
--- /dev/null
+++ b/src/ui/components/settingsSearchIndex.ts
@@ -0,0 +1,51 @@
+export type SettingsTab = "about" | "appearance" | "playback" | "system" | "shortcuts" | "window";
+
+export type SettingsSearchEntry = {
+ tab: SettingsTab;
+ category: string;
+ title: string;
+ description: string;
+};
+
+const ENTRIES: SettingsSearchEntry[] = [
+ { tab: "about", category: "Account", title: "Account", description: "Sign in and manage your YouTube Music account." },
+ { tab: "about", category: "Account", title: "Last.fm", description: "Connect Last.fm and scrobble plays." },
+ { tab: "about", category: "Account", title: "Discord presence", description: "Show what you are playing and hide status when paused." },
+ { tab: "about", category: "Account", title: "Updates", description: "Check for and install Zuno updates." },
+ { tab: "appearance", category: "Appearance", title: "Theme", description: "Choose light, dark, or system theme." },
+ { tab: "appearance", category: "Appearance", title: "Motion", description: "Control animations and visual effects." },
+ { tab: "appearance", category: "Appearance", title: "Toolbar", description: "Choose which controls appear in the title bar." },
+ { tab: "appearance", category: "Appearance", title: "Onboarding", description: "Restart the first-run introduction." },
+ { tab: "appearance", category: "Appearance", title: "Made for you", description: "Show personalized recommendations on Home." },
+ { tab: "playback", category: "Playback", title: "Output device", description: "Choose where Zuno sends audio." },
+ { tab: "playback", category: "Playback", title: "Equalizer", description: "Adjust audio frequencies and presets." },
+ { tab: "playback", category: "Playback", title: "Gapless playback", description: "Remove silence between tracks." },
+ { tab: "playback", category: "Playback", title: "Crossfade", description: "Blend the end of one track into the next." },
+ { tab: "playback", category: "Playback", title: "Restore tabs and queues", description: "Restore your playback session after restarting." },
+ { tab: "playback", category: "Playback", title: "Audio engine", description: "Choose the playback method and authenticated streaming." },
+ { tab: "system", category: "Library", title: "Local music", description: "Create playlists from folders on this computer." },
+ { tab: "system", category: "Library", title: "Cache", description: "Set cache size and clear cached content." },
+ { tab: "system", category: "Library", title: "Downloads", description: "Set offline storage limits and remove downloads." },
+ { tab: "system", category: "Library", title: "Streaming quality", description: "Choose quality for music played over the network." },
+ { tab: "system", category: "Library", title: "Download quality", description: "Choose quality for offline music." },
+ { tab: "system", category: "Library", title: "Lyrics", description: "Translate lyrics, set text size, and choose a source." },
+ { tab: "system", category: "Library", title: "Launch at startup", description: "Start Zuno when your computer starts." },
+ { tab: "system", category: "Library", title: "Minimize to tray", description: "Keep Zuno available from the system tray." },
+ { tab: "system", category: "Library", title: "Remember window size and location", description: "Restore the main window geometry." },
+ { tab: "system", category: "Library", title: "Troubleshooting", description: "Open logs or reset Zuno data." },
+ { tab: "window", category: "Window", title: "Mini player", description: "Show a compact player when the main window loses focus." },
+ { tab: "window", category: "Window", title: "Library sidebar", description: "Choose the playlist rail size and hover behavior." },
+ { tab: "window", category: "Window", title: "Window controls", description: "Choose macOS, Windows, or native title-bar buttons." },
+ { tab: "window", category: "Window", title: "Compact player bar", description: "Use a smaller playback bar." },
+ { tab: "window", category: "Window", title: "System media controls", description: "Show playback in system media controls." },
+ { tab: "shortcuts", category: "Shortcuts", title: "Keyboard shortcuts", description: "View, record, or clear keyboard bindings." },
+];
+
+export function searchSettings(query: string): SettingsSearchEntry[] {
+ const terms = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
+ if (terms.length === 0) return [];
+ return ENTRIES.filter((entry) => {
+ const haystack = `${entry.category} ${entry.title} ${entry.description}`.toLocaleLowerCase();
+ return terms.every((term) => haystack.includes(term));
+ });
+}
diff --git a/src/ui/pages/SettingsPage.tsx b/src/ui/pages/SettingsPage.tsx
index 3826074..c5db5b7 100644
--- a/src/ui/pages/SettingsPage.tsx
+++ b/src/ui/pages/SettingsPage.tsx
@@ -102,6 +102,11 @@ import {
usePotatoPcMode,
} from "../settings/renderEffects";
import { setMadeForYouVisible, useMadeForYouVisible } from "../settings/homeSections";
+import {
+ SettingsSearch,
+ SettingsSearchResults,
+ type SettingsTab,
+} from "../components/SettingsSearch";
import { GoogleSignInButton } from "../components/GoogleSignInButton";
import { ExternalLinkButton } from "../components/ExternalLinkButton";
import {
@@ -655,8 +660,6 @@ function SettingsCardHeader({
}
/** Quiet outbound links in the page header. */
-type SettingsTab = "about" | "appearance" | "playback" | "system" | "shortcuts" | "window";
-
type WindowControlStyle = "macos" | "windows" | "native";
const SETTINGS_TABS: Array<{
@@ -738,6 +741,7 @@ export function SettingsPage({
const [lastFmBusy, setLastFmBusy] = useState(false);
const [lastFmError, setLastFmError] = useState(null);
const [activeTab, setActiveTab] = useState("about");
+ const [settingsQuery, setSettingsQuery] = useState("");
const themePreference = useThemePreference();
const [listeningShortcut, setListeningShortcut] = useState(null);
const keyboardShortcuts = useKeyboardShortcuts();
@@ -1122,8 +1126,8 @@ export function SettingsPage({
};
return (
-
-
+
+
Settings
@@ -1136,7 +1140,10 @@ export function SettingsPage({
part of the description above and were routinely missed. They stay unfilled so they
still sit below the category nav in the hierarchy.
*/}
-
+
+
+
+
}
label="Star on GitHub"
@@ -1150,12 +1157,11 @@ export function SettingsPage({
- {/* Vertical nav rather than a pill row: it has room for a description per
- category and scales as sections are added, the way desktop settings do.
- The nav sticks so the categories stay reachable while a long panel scrolls. */}
-
+ {/* The category rail follows this pane's width, not the application viewport: an embedded
+ or scaled Settings pane can be narrow even on a wide desktop window. */}
+