+ );
+ }
+ return undefined;
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/ts/components/pages/settings/SearchableSetting.tsx b/frontend/src/ts/components/pages/settings/SearchableSetting.tsx
new file mode 100644
index 000000000000..927a1707f9fa
--- /dev/null
+++ b/frontend/src/ts/components/pages/settings/SearchableSetting.tsx
@@ -0,0 +1,47 @@
+import { createMemo, JSXElement } from "solid-js";
+
+import {
+ registerSearchable,
+ settingMatchesSearch,
+} from "../../../states/settings-search";
+import { cn } from "../../../utils/cn";
+import { Setting, SettingProps } from "../../common/Setting";
+
+export type SearchableSettingProps = SettingProps & {
+ // extra text (e.g. option labels) the search filter also matches against
+ extraSearchKeywords?: string;
+};
+
+// pull plain text out of a (possibly JSX) description so search can match it.
+// solid renders JSX to real DOM nodes/arrays, so we can read their textContent.
+function textOf(node: string | JSXElement): string {
+ if (typeof node === "string" || typeof node === "number") return String(node);
+ if (Array.isArray(node)) return node.map(textOf).join(" ");
+ if (node instanceof Node) return node.textContent ?? "";
+ return "";
+}
+
+// a Setting that hides itself when it doesn't match the active settings search.
+// hides (via css) instead of unmounting so typing doesn't remount every setting
+// on each keypress; the hidden class stays on the Setting root so the section
+// auto-collapse selector keeps working.
+export function SearchableSetting(props: SearchableSettingProps): JSXElement {
+ // static per setting — only the query changes as the user types, so build once
+ const haystack = createMemo(() =>
+ [props.title, textOf(props.description), props.extraSearchKeywords ?? ""]
+ .join(" ")
+ .toLowerCase(),
+ );
+
+ // scoring is global (a setting shows only if it ties the best match across all
+ // settings), so register this haystack for the shared best-match computation.
+ // oxlint-disable-next-line solid/reactivity -- getter stored, called in a tracked memo
+ registerSearchable(haystack);
+
+ return (
+
+ );
+}
diff --git a/frontend/src/ts/components/pages/settings/SettingsPage.tsx b/frontend/src/ts/components/pages/settings/SettingsPage.tsx
index df344727ae74..9d098917df63 100644
--- a/frontend/src/ts/components/pages/settings/SettingsPage.tsx
+++ b/frontend/src/ts/components/pages/settings/SettingsPage.tsx
@@ -1,11 +1,7 @@
-import { Config, ConfigSchema } from "@monkeytype/schemas/configs";
-import { createForm } from "@tanstack/solid-form";
-import { createResource, createSignal, For, JSXElement, Show } from "solid-js";
+import { createResource, createSignal, JSXElement, Show } from "solid-js";
import { z } from "zod";
import { resetConfig } from "../../../config/lifecycle";
-import { configMetadata, OptionMetadata } from "../../../config/metadata";
-import { setConfig } from "../../../config/setters";
import { getConfig } from "../../../config/store";
import {
playTimeWarning,
@@ -13,22 +9,18 @@ import {
previewError,
} from "../../../controllers/sound-controller";
import { useLocalStorage } from "../../../hooks/useLocalStorage";
-import { useSavedIndicator } from "../../../hooks/useSavedIndicator";
import { isAuthenticated } from "../../../states/core";
import { showModal } from "../../../states/modals";
+import { isSettingsSearchActive } from "../../../states/settings-search";
import { showSimpleModal } from "../../../states/simple-modal";
import { cn } from "../../../utils/cn";
import fileStorage from "../../../utils/file-storage";
import { wordsToCamelCase } from "../../../utils/strings";
-import { getOptions } from "../../../utils/zod";
import { Anime, AnimeShow } from "../../common/anime";
import { Button } from "../../common/Button";
import { Fa } from "../../common/Fa";
import { Page } from "../../common/Page";
-import { Setting } from "../../common/Setting";
import { CommandlineHotkey } from "../../hotkeys/CommandlineHotkey";
-import { InputField } from "../../ui/form/InputField";
-import { fromSchema } from "../../ui/form/utils";
import { AnimationFpsLimit } from "./custom-setting/AnimationFpsLimit";
import { AutoSwitchTheme } from "./custom-setting/AutoSwitchTheme";
import { CustomBackground } from "./custom-setting/CustomBackground";
@@ -52,6 +44,9 @@ import { SoundVolume } from "./custom-setting/SoundVolume";
import { Tags } from "./custom-setting/Tags";
import { Theme } from "./custom-setting/Theme";
import { QuickNav } from "./QuickNav";
+import { SearchableAutoSetting } from "./SearchableAutoSetting";
+import { SearchableSetting } from "./SearchableSetting";
+import { SettingsSearch } from "./SettingsSearch";
export function SettingsPage(): JSXElement {
const [hasLocalBg] = createResource(
@@ -62,54 +57,63 @@ export function SettingsPage(): JSXElement {
return (
-
+ {/* while filtering, only the matching settings stay visible; everything
+ else is hidden with css so nothing unmounts while typing */}
+
-
+
tip: You can also change all these settings quickly using the
command line
( )
-
+
+ {/* while filtering, lay the matching sections out with a uniform gap */}
+
Account settings have moved. You can now access them by hovering over
@@ -287,10 +296,20 @@ function Section(props: { title: string; children: JSXElement }): JSXElement {
const [isOpen, setIsOpen] = createSignal(true);
return (
-