diff --git a/frontend/src/ts/components/pages/settings/Setting.tsx b/frontend/src/ts/components/pages/settings/Setting.tsx
index 4de64430c3d5..7a3d1d97cf1f 100644
--- a/frontend/src/ts/components/pages/settings/Setting.tsx
+++ b/frontend/src/ts/components/pages/settings/Setting.tsx
@@ -6,6 +6,7 @@ import {
showErrorNotification,
showSuccessNotification,
} from "../../../states/notifications";
+import { getSettingsSearch } from "../../../states/settings-search";
import { cn } from "../../../utils/cn";
import { Button } from "../../common/Button";
import { FaProps } from "../../common/Fa";
@@ -18,67 +19,84 @@ type Props = {
description: string | JSXElement;
inputs?: JSXElement;
fullWidthInputs?: JSXElement;
+ // extra text (e.g. option labels) the search filter also matches against
+ searchTerms?: string;
};
export function Setting(props: Props): JSXElement {
+ const matchesSearch = (): boolean => {
+ const query = getSettingsSearch().trim().toLowerCase();
+ if (query === "") return true;
+ const haystack = [
+ props.title,
+ typeof props.description === "string" ? props.description : "",
+ props.searchTerms ?? "",
+ ]
+ .join(" ")
+ .toLowerCase();
+ return haystack.includes(query);
+ };
+
return (
-
-
-
-
+
);
}
diff --git a/frontend/src/ts/components/pages/settings/SettingsPage.tsx b/frontend/src/ts/components/pages/settings/SettingsPage.tsx
index 16da1cc15277..dfdf8f179300 100644
--- a/frontend/src/ts/components/pages/settings/SettingsPage.tsx
+++ b/frontend/src/ts/components/pages/settings/SettingsPage.tsx
@@ -5,6 +5,10 @@ import { z } from "zod";
import { resetConfig } from "../../../config/lifecycle";
import { configMetadata, OptionMetadata } from "../../../config/metadata";
+import {
+ getOptionLabel,
+ getOptionSearchTerms,
+} from "../../../config/option-strings";
import { setConfig } from "../../../config/setters";
import { getConfig } from "../../../config/store";
import {
@@ -16,6 +20,7 @@ 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";
@@ -52,6 +57,7 @@ import { Tags } from "./custom-setting/Tags";
import { Theme } from "./custom-setting/Theme";
import { QuickNav } from "./QuickNav";
import { Setting } from "./Setting";
+import { SettingsSearch } from "./SettingsSearch";
export function SettingsPage(): JSXElement {
const [hasLocalBg] = createResource(
@@ -62,16 +68,23 @@ export function SettingsPage(): JSXElement {
return (
-
-
+
+ {/* while filtering, show only the matching settings (hide everything else) */}
+
+
+
+
tip: You can also change all these settings quickly using the
command line
( )
-
-
+
+
+
+ {/* while filtering, lay the matching sections out with a uniform gap */}
+
@@ -248,7 +261,9 @@ export function SettingsPage(): JSXElement {
-
+
+
+
);
@@ -287,25 +302,39 @@ function Section(props: { title: string; children: JSXElement }): JSXElement {
const [isOpen, setIsOpen] = createSignal(true);
return (
-
-
setIsOpen((prev) => !prev)}
- >
-
+
+ setIsOpen((prev) => !prev)}
>
-
-
- {props.title}
-
-
+
+
+
+ {props.title}
+
+
+
{props.children}
-
+
+
+
);
@@ -405,39 +434,18 @@ function AutoSetting(props: {
)}
>
- {(option) => {
- const text = () => {
- const optionsMeta = configMetadata[props.key]
- .optionsMetadata as
- | Record
- | undefined;
- const match = optionsMeta?.[String(option)];
- if (match?.displayString !== undefined) {
- return match.displayString;
- }
-
- if (option === true) {
- return "on";
- }
- if (option === false) {
- return "off";
- }
-
- return (option as string).toString().replace(/_/g, " ");
- };
- return (
- {
- if (getConfig[props.key] === option) return;
- props.onOptionClick?.(option as Config[T]);
- setConfig(props.key, option as Config[T]);
- }}
- >
- {text()}
-
- );
- }}
+ {(option) => (
+ {
+ if (getConfig[props.key] === option) return;
+ props.onOptionClick?.(option as Config[T]);
+ setConfig(props.key, option as Config[T]);
+ }}
+ >
+ {getOptionLabel(props.key, option as Config[T])}
+
+ )}
);
@@ -451,6 +459,7 @@ function AutoSetting(props: {
title={configMetadata[props.key].displayString ?? props.key}
fa={configMetadata[props.key].fa}
description={configMetadata[props.key].description}
+ searchTerms={getOptionSearchTerms(props.key)}
inputs={!props.wide ? autoInputs() : props.inputs}
fullWidthInputs={
props.wide ? (autoInputs() ?? props.inputs) : props.inputs
diff --git a/frontend/src/ts/components/pages/settings/SettingsSearch.tsx b/frontend/src/ts/components/pages/settings/SettingsSearch.tsx
new file mode 100644
index 000000000000..40c74540da13
--- /dev/null
+++ b/frontend/src/ts/components/pages/settings/SettingsSearch.tsx
@@ -0,0 +1,44 @@
+import { JSXElement, onCleanup, Show } from "solid-js";
+
+import {
+ getSettingsSearch,
+ setSettingsSearch,
+} from "../../../states/settings-search";
+import { cn } from "../../../utils/cn";
+import { Button } from "../../common/Button";
+import { Fa } from "../../common/Fa";
+
+export function SettingsSearch(): JSXElement {
+ // reset the filter when leaving the settings page
+ onCleanup(() => setSettingsSearch(""));
+
+ return (
+
+
+ setSettingsSearch(e.currentTarget.value)}
+ />
+
+ setSettingsSearch("")}
+ />
+
+
+ );
+}
diff --git a/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx b/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx
index 9f2b526696ea..b7551fbf461f 100644
--- a/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx
+++ b/frontend/src/ts/components/pages/settings/custom-setting/CustomBackground.tsx
@@ -6,6 +6,7 @@ import { createForm } from "@tanstack/solid-form";
import { createResource, JSXElement, For, Show } from "solid-js";
import { configMetadata } from "../../../../config/metadata";
+import { getOptionSearchTerms } from "../../../../config/option-strings";
import { setConfig } from "../../../../config/setters";
import { getConfig } from "../../../../config/store";
import { applyCustomBackground } from "../../../../controllers/theme-controller";
@@ -56,6 +57,7 @@ export function CustomBackground(): JSXElement {
configMetadata.customBackground.displayString ?? "custom background"
}
fa={configMetadata.customBackground.fa}
+ searchTerms={getOptionSearchTerms("customBackgroundSize")}
description={
<>
{configMetadata.customBackground.description}
diff --git a/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx b/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx
index 19ace4b8a7c4..a0f4d82d2c1b 100644
--- a/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx
+++ b/frontend/src/ts/components/pages/settings/custom-setting/FontFamily.tsx
@@ -3,6 +3,7 @@ import { createResource, For, JSXElement, Show } from "solid-js";
import { z } from "zod";
import { configMetadata } from "../../../../config/metadata";
+import { getOptionSearchTerms } from "../../../../config/option-strings";
import { setConfig } from "../../../../config/setters";
import { getConfig } from "../../../../config/store";
import { showNoticeNotification } from "../../../../states/notifications";
@@ -38,6 +39,7 @@ export function FontFamily(): JSXElement {
key="fontFamily"
title={configMetadata.fontFamily.displayString ?? "font family"}
fa={configMetadata.fontFamily.fa}
+ searchTerms={getOptionSearchTerms("fontFamily")}
description={
<>
{configMetadata.fontFamily.description}
diff --git a/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx b/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx
index 89226f20319e..166a92eae826 100644
--- a/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx
+++ b/frontend/src/ts/components/pages/settings/custom-setting/PaceCaret.tsx
@@ -6,6 +6,7 @@ import { createForm } from "@tanstack/solid-form";
import { For, JSXElement } from "solid-js";
import { configMetadata } from "../../../../config/metadata";
+import { getOptionSearchTerms } from "../../../../config/option-strings";
import { setConfig } from "../../../../config/setters";
import { getConfig } from "../../../../config/store";
import { useSavedIndicator } from "../../../../hooks/useSavedIndicator";
@@ -40,6 +41,7 @@ export function PaceCaret(): JSXElement {
key="paceCaret"
title={configMetadata.paceCaret.displayString ?? "pace caret"}
fa={configMetadata.paceCaret.fa}
+ searchTerms={getOptionSearchTerms("paceCaret")}
description={configMetadata.paceCaret.description}
inputs={
diff --git a/frontend/src/ts/config/option-strings.ts b/frontend/src/ts/config/option-strings.ts
new file mode 100644
index 000000000000..fd5a4280723d
--- /dev/null
+++ b/frontend/src/ts/config/option-strings.ts
@@ -0,0 +1,39 @@
+import { Config, ConfigSchema } from "@monkeytype/schemas/configs";
+
+import { getOptions } from "../utils/zod";
+import { configMetadata, OptionMetadata } from "./metadata";
+
+// the label shown for a single option (and used to match it while searching)
+export function getOptionLabel(
+ key: T,
+ option: Config[T],
+): string {
+ const optionMeta = (
+ configMetadata[key] as {
+ optionsMetadata?: Record | undefined;
+ }
+ ).optionsMetadata?.[String(option)];
+
+ if (optionMeta?.displayString !== undefined) return optionMeta.displayString;
+ if (option === true) return "on";
+ if (option === false) return "off";
+ return String(option).replace(/_/g, " ");
+}
+
+// all of a setting's option labels joined, so the settings search can match on them
+export function getOptionSearchTerms(key: T): string {
+ const optionsMeta = (
+ configMetadata[key] as {
+ optionsMetadata?: Record | undefined;
+ }
+ ).optionsMetadata;
+
+ const options = getOptions(ConfigSchema.shape[key])?.filter(
+ (option) => optionsMeta?.[String(option)]?.visible !== false,
+ );
+ if (options === undefined) return "";
+
+ return options
+ .map((option) => getOptionLabel(key, option as Config[T]))
+ .join(" ");
+}
diff --git a/frontend/src/ts/states/settings-search.ts b/frontend/src/ts/states/settings-search.ts
new file mode 100644
index 000000000000..9e7f21e2a315
--- /dev/null
+++ b/frontend/src/ts/states/settings-search.ts
@@ -0,0 +1,8 @@
+import { createSignal } from "solid-js";
+
+// the current settings filter query, shared between the search input and the
+// settings/sections that hide themselves when they don't match
+export const [getSettingsSearch, setSettingsSearch] = createSignal("");
+
+export const isSettingsSearchActive = (): boolean =>
+ getSettingsSearch().trim() !== "";