From de3e120ce94f9d40797ee951629343859a7dd7e1 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 13:07:21 +0200 Subject: [PATCH 01/56] brrap --- frontend/src/html/pages/settings.html | 2 + frontend/src/ts/commandline/util.ts | 18 +-- frontend/src/ts/components/mount.tsx | 2 + .../ts/components/pages/settings/Setting.tsx | 43 +++++ .../ts/components/pages/settings/Settings.tsx | 82 ++++++++++ .../ts/config/{metadata.ts => metadata.tsx} | 153 +++++++++++++++++- frontend/src/ts/utils/zod.ts | 18 +++ 7 files changed, 294 insertions(+), 24 deletions(-) create mode 100644 frontend/src/ts/components/pages/settings/Setting.tsx create mode 100644 frontend/src/ts/components/pages/settings/Settings.tsx rename frontend/src/ts/config/{metadata.ts => metadata.tsx} (72%) create mode 100644 frontend/src/ts/utils/zod.ts diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index 7049cbd4ee1f..053c82ccaab5 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -42,6 +42,8 @@ ) + + + } + /> + ); +} From 03d09475db0de46f376be5da20c8363def6aa5bd Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 19:02:32 +0200 Subject: [PATCH 07/56] min speed --- .../settings/custom-setting/MinSpeed.tsx | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx index e1e37b0a6825..d9096e669dd6 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx @@ -1,17 +1,21 @@ import { MinWpmCustomSpeedSchema } from "@monkeytype/schemas/configs"; import { createForm } from "@tanstack/solid-form"; -import { JSXElement } from "solid-js"; +import { createSignal, JSXElement } from "solid-js"; import { configMetadata } from "../../../../config/metadata"; import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; -import { showSuccessNotification } from "../../../../states/notifications"; +import { AnimeShow } from "../../../common/anime"; +// import { showSuccessNotification } from "../../../../states/notifications"; import { Button } from "../../../common/Button"; +import { Fa } from "../../../common/Fa"; import { InputField } from "../../../ui/form/InputField"; import { fromSchema } from "../../../ui/form/utils"; import { Setting } from "../Setting"; export function MinSpeed(): JSXElement { + const [showSavedIndicator, setShowSavedIndicator] = createSignal(false); + const form = createForm(() => ({ defaultValues: { minWpmCustomSpeed: getConfig.minWpmCustomSpeed, @@ -24,7 +28,13 @@ export function MinSpeed(): JSXElement { } else { setConfig("minWpm", "custom"); } - showSuccessNotification("Min speed saved"); + // showSuccessNotification("Min speed saved", { + // durationMs: 1000, + // }); + setShowSavedIndicator(true); + setTimeout(() => { + setShowSavedIndicator(false); + }, 2000); setConfig("minWpmCustomSpeed", val); }, })); @@ -58,12 +68,19 @@ export function MinSpeed(): JSXElement { }, }} children={(field) => ( - +
+ + +
+ +
+
+
)} /> From 136233035ef4b501ee3f424c76a2c4cd07d637d5 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 19:08:49 +0200 Subject: [PATCH 08/56] customs --- .../ts/components/pages/settings/Settings.tsx | 9 +- .../pages/settings/custom-setting/MinAcc.tsx | 116 ++++++++++++++++ .../settings/custom-setting/MinBurst.tsx | 125 ++++++++++++++++++ .../settings/custom-setting/MinSpeed.tsx | 6 +- 4 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 frontend/src/ts/components/pages/settings/custom-setting/MinAcc.tsx create mode 100644 frontend/src/ts/components/pages/settings/custom-setting/MinBurst.tsx diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index bfdedf9210f4..93eda00e7835 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -9,14 +9,14 @@ import { getOptions } from "../../../utils/zod"; import { Anime, AnimeShow } from "../../common/anime"; import { Button } from "../../common/Button"; import { Fa } from "../../common/Fa"; +import { MinAcc } from "./custom-setting/MinAcc"; +import { MinBurst } from "./custom-setting/MinBurst"; import { MinSpeed } from "./custom-setting/MinSpeed"; import { Setting } from "./Setting"; export function Settings(): JSXElement { return (
- -
@@ -25,8 +25,9 @@ export function Settings(): JSXElement { - {/* todo: min accuracy */} - {/* todo: min burst */} + + + {/* todo: language */} {/* todo: funbox */} diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MinAcc.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MinAcc.tsx new file mode 100644 index 000000000000..420ebd5a12e7 --- /dev/null +++ b/frontend/src/ts/components/pages/settings/custom-setting/MinAcc.tsx @@ -0,0 +1,116 @@ +import { MinimumAccuracyCustomSchema } from "@monkeytype/schemas/configs"; +import { createForm } from "@tanstack/solid-form"; +import { createSignal, JSXElement } from "solid-js"; + +import { configMetadata } from "../../../../config/metadata"; +import { setConfig } from "../../../../config/setters"; +import { getConfig } from "../../../../config/store"; +import { AnimeShow } from "../../../common/anime"; +// import { showSuccessNotification } from "../../../../states/notifications"; +import { Button } from "../../../common/Button"; +import { Fa } from "../../../common/Fa"; +import { InputField } from "../../../ui/form/InputField"; +import { fromSchema } from "../../../ui/form/utils"; +import { Setting } from "../Setting"; + +export function MinAcc(): JSXElement { + const [showSavedIndicator, setShowSavedIndicator] = createSignal(false); + + const form = createForm(() => ({ + defaultValues: { + minAccCustom: getConfig.minAccCustom, + }, + onSubmit: ({ value }) => { + const val = parseInt(String(value.minAccCustom)); + if (val === getConfig.minAccCustom) return; + if (getConfig.minAcc === "custom") { + // + } else { + setConfig("minAcc", "custom"); + } + // showSuccessNotification("Min accuracy saved", { + // durationMs: 1000, + // }); + setShowSavedIndicator(true); + setTimeout(() => { + setShowSavedIndicator(false); + }, 2000); + setConfig("minAccCustom", val); + }, + })); + + return ( + +
{ + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + > + { + const val = parseInt(String(value)); + if (isNaN(val)) { + return "must be a number"; + } + return fromSchema(MinimumAccuracyCustomSchema)({ + value: val, + }); + }, + onBlur: () => { + void form.handleSubmit(); + }, + }} + children={(field) => ( +
+ + +
+ +
+
+
+ )} + /> + + {/* */} +
+ + +
+
+ } + /> + ); +} diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MinBurst.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MinBurst.tsx new file mode 100644 index 000000000000..e817b236f632 --- /dev/null +++ b/frontend/src/ts/components/pages/settings/custom-setting/MinBurst.tsx @@ -0,0 +1,125 @@ +import { MinimumBurstCustomSpeedSchema } from "@monkeytype/schemas/configs"; +import { createForm } from "@tanstack/solid-form"; +import { createSignal, JSXElement } from "solid-js"; + +import { configMetadata } from "../../../../config/metadata"; +import { setConfig } from "../../../../config/setters"; +import { getConfig } from "../../../../config/store"; +import { AnimeShow } from "../../../common/anime"; +// import { showSuccessNotification } from "../../../../states/notifications"; +import { Button } from "../../../common/Button"; +import { Fa } from "../../../common/Fa"; +import { InputField } from "../../../ui/form/InputField"; +import { fromSchema } from "../../../ui/form/utils"; +import { Setting } from "../Setting"; + +export function MinBurst(): JSXElement { + const [showSavedIndicator, setShowSavedIndicator] = createSignal(false); + + const form = createForm(() => ({ + defaultValues: { + minBurstCustomSpeed: getConfig.minBurstCustomSpeed, + }, + onSubmit: ({ value }) => { + const val = parseInt(String(value.minBurstCustomSpeed)); + if (val === getConfig.minBurstCustomSpeed) return; + if (getConfig.minBurst !== "off") { + // + } else { + setConfig("minBurst", "fixed"); + } + // showSuccessNotification("Min burst saved", { + // durationMs: 1000, + // }); + setShowSavedIndicator(true); + setTimeout(() => { + setShowSavedIndicator(false); + }, 2000); + setConfig("minBurstCustomSpeed", val); + }, + })); + + return ( + +
{ + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + > + { + const val = parseInt(String(value)); + if (isNaN(val)) { + return "must be a number"; + } + return fromSchema(MinimumBurstCustomSpeedSchema)({ + value: val, + }); + }, + onBlur: () => { + void form.handleSubmit(); + }, + }} + children={(field) => ( +
+ + +
+ +
+
+
+ )} + /> + + {/* */} +
+ + + +
+ + } + /> + ); +} diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx index d9096e669dd6..aef0f733edbf 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/MinSpeed.tsx @@ -41,7 +41,7 @@ export function MinSpeed(): JSXElement { return ( From 976d31c9e484c66f1f15a68e74659ee5f5bb3c7d Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 20:10:53 +0200 Subject: [PATCH 09/56] language --- .../ts/components/pages/settings/Settings.tsx | 3 +- .../settings/custom-setting/Language.tsx | 44 +++++++++++++++++++ frontend/src/ts/components/ui/SlimSelect.tsx | 28 +++++++----- 3 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 frontend/src/ts/components/pages/settings/custom-setting/Language.tsx diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index 93eda00e7835..8f10eb249122 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -9,6 +9,7 @@ import { getOptions } from "../../../utils/zod"; import { Anime, AnimeShow } from "../../common/anime"; import { Button } from "../../common/Button"; import { Fa } from "../../common/Fa"; +import { Language } from "./custom-setting/Language"; import { MinAcc } from "./custom-setting/MinAcc"; import { MinBurst } from "./custom-setting/MinBurst"; import { MinSpeed } from "./custom-setting/MinSpeed"; @@ -29,7 +30,7 @@ export function Settings(): JSXElement { - {/* todo: language */} + {/* todo: funbox */} {/* todo: custom layoutfluid */} {/* todo: polyglot languages */} diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Language.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Language.tsx new file mode 100644 index 000000000000..2a6c107f9e33 --- /dev/null +++ b/frontend/src/ts/components/pages/settings/custom-setting/Language.tsx @@ -0,0 +1,44 @@ +import { Language as LanguageSchema } from "@monkeytype/schemas/languages"; +import { Optgroup } from "slim-select/store"; +import { JSXElement } from "solid-js"; + +import { configMetadata } from "../../../../config/metadata"; +import { setConfig } from "../../../../config/setters"; +import { getConfig } from "../../../../config/store"; +import { + LanguageGroupNames, + LanguageGroups, +} from "../../../../constants/languages"; +import { getLanguageDisplayString } from "../../../../utils/strings"; +import SlimSelect from "../../../ui/SlimSelect"; +import { Setting } from "../Setting"; + +export function Language(): JSXElement { + return ( + + ({ + label: group, + options: LanguageGroups[group]?.map((language) => ({ + text: getLanguageDisplayString(language), + value: language, + })), + }) as Optgroup, + )} + selected={getConfig.language} + onChange={(val) => { + if (getConfig.language === (val as LanguageSchema)) return; + setConfig("language", val as LanguageSchema); + }} + /> + } + /> + ); +} diff --git a/frontend/src/ts/components/ui/SlimSelect.tsx b/frontend/src/ts/components/ui/SlimSelect.tsx index e6cd154192ba..69bce2655df0 100644 --- a/frontend/src/ts/components/ui/SlimSelect.tsx +++ b/frontend/src/ts/components/ui/SlimSelect.tsx @@ -23,6 +23,7 @@ function updateSlimSelectData( export type SlimSelectProps = { options?: Pick[]; + optionGroups?: Optgroup[]; values?: string[]; // Simple string array where value === text settings?: Config["settings"] & { scrollToTop?: boolean; @@ -33,6 +34,7 @@ export type SlimSelectProps = { children?: JSX.Element; ref?: (instance: SlimSelectCore | null) => void; disabled?: boolean; + appendToBody?: boolean; } & ( | { multiple?: never; @@ -82,6 +84,11 @@ export default function SlimSelect(props: SlimSelectProps): JSXElement { return []; }; + const getInitialData = (): (Partial)[] => { + if (props.optionGroups) return props.optionGroups; + return getDataWithAll(buildData(getOptions(), getSelected())); + }; + // Build option data with selection state const buildData = ( options: Pick[] = [], @@ -244,10 +251,10 @@ export default function SlimSelect(props: SlimSelectProps): JSXElement { const config: Config = { select: selectRef, - data: getDataWithAll(buildData(getOptions(), getSelected())) as Option[], + data: getInitialData() as Option[], settings: { ...props.settings, - contentLocation: containerRef, + ...(props.appendToBody ? {} : { contentLocation: containerRef }), }, ...(props.cssClasses && { cssClasses: props.cssClasses }), events: { @@ -354,9 +361,12 @@ export default function SlimSelect(props: SlimSelectProps): JSXElement { setIsInitialMount(false); - // Initialize with selected values + // Initialize currentSelected from data without firing onChange requestAnimationFrame(() => { - if (!props.onChange || (!props.options && !props.values) || !slimSelect) { + if ( + (!props.options && !props.values && !props.optionGroups) || + !slimSelect + ) { setIsInitializing(false); return; } @@ -377,13 +387,6 @@ export default function SlimSelect(props: SlimSelectProps): JSXElement { } } - if (initialValue.length > 0 && props.onChange !== undefined) { - if (props.multiple) { - props.onChange(initialValue); - } else { - props.onChange(initialValue[0] ?? ""); - } - } currentSelected = initialValue; } @@ -406,6 +409,9 @@ export default function SlimSelect(props: SlimSelectProps): JSXElement { return; } + // When using optionGroups without selected prop, selection is embedded in the data + if (props.selected === undefined) return; + if (slimSelect && selected !== undefined) { currentSelected = selected; From 3eb51e76da5f3556cfcfcda5ba6b4670181e8ebe Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 20:52:32 +0200 Subject: [PATCH 10/56] funbox --- .../ts/components/pages/settings/Settings.tsx | 2 + .../pages/settings/custom-setting/Funbox.tsx | 82 +++++++++++++++++++ frontend/src/ts/config/lifecycle.ts | 5 +- frontend/src/ts/config/setters.ts | 2 + frontend/src/ts/config/store.ts | 23 +++++- 5 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index 8f10eb249122..0147f5a4b974 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -9,6 +9,7 @@ import { getOptions } from "../../../utils/zod"; import { Anime, AnimeShow } from "../../common/anime"; import { Button } from "../../common/Button"; import { Fa } from "../../common/Fa"; +import { Funbox } from "./custom-setting/Funbox"; import { Language } from "./custom-setting/Language"; import { MinAcc } from "./custom-setting/MinAcc"; import { MinBurst } from "./custom-setting/MinBurst"; @@ -19,6 +20,7 @@ export function Settings(): JSXElement { return (
+ diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx new file mode 100644 index 000000000000..e867d44ea346 --- /dev/null +++ b/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx @@ -0,0 +1,82 @@ +import { checkCompatibility, getAllFunboxes } from "@monkeytype/funbox"; +import { For, JSXElement, type JSX } from "solid-js"; + +// import { canSetFunboxWithConfig } from "../../../../config/funbox-validation"; +import { configMetadata } from "../../../../config/metadata"; +import { toggleFunbox } from "../../../../config/setters"; +import { getConfig } from "../../../../config/store"; +import { getActiveFunboxNames } from "../../../../test/funbox/list"; +import { Button } from "../../../common/Button"; +import { Setting } from "../Setting"; + +export function Funbox(): JSXElement { + return ( + + + {(funbox) => { + const active = () => getConfig.funbox.includes(funbox.name); + + const disabled = () => { + if (active()) return false; + const incompatible = !checkCompatibility( + getActiveFunboxNames(), + funbox.name, + ); + return incompatible; + // const configIncompatible = !canSetFunboxWithConfig( + // funbox.name, + // getConfig, + // ).ok; + // return incompatible || configIncompatible; + }; + + const style = (): JSX.CSSProperties | undefined => { + if (funbox.name === "mirror") { + return { + transform: "scaleX(-1)", + }; + } + if (funbox.name === "upside_down") { + return { + transform: "scaleY(-1)", + }; + } + return undefined; + }; + + const text = () => { + if (funbox.name === "underscore_spaces") { + return "underscore_spaces"; + } + return funbox.name.replace(/_/g, " "); + }; + + return ( +
+ +
+ ); + }} +
+
+ } + /> + ); +} diff --git a/frontend/src/ts/config/lifecycle.ts b/frontend/src/ts/config/lifecycle.ts index 9402afa95698..4f88247173e4 100644 --- a/frontend/src/ts/config/lifecycle.ts +++ b/frontend/src/ts/config/lifecycle.ts @@ -9,14 +9,13 @@ import { saveToLocalStorage, saveFullConfigToLocalStorage, } from "./persistence"; -import { Config, setConfigStore } from "./store"; +import { Config, setFullConfigStore } from "./store"; import { getDefaultConfig } from "../constants/default-config"; import { configEvent } from "../events/config"; import { migrateConfig } from "./utils"; import { promiseWithResolvers, typedKeys } from "../utils/misc"; import { setConfig } from "./setters"; import { deleteConfig } from "../ape/config"; -import { reconcile } from "solid-js/store"; export async function applyConfigFromJson(json: string): Promise { try { @@ -108,7 +107,7 @@ export async function applyConfig( } configEvent.dispatch({ key: "fullConfigChangeFinished" }); - setConfigStore(reconcile(Config)); + setFullConfigStore(fullConfig); } export async function resetConfig(): Promise { diff --git a/frontend/src/ts/config/setters.ts b/frontend/src/ts/config/setters.ts index 385b948fc428..382012a3f322 100644 --- a/frontend/src/ts/config/setters.ts +++ b/frontend/src/ts/config/setters.ts @@ -202,5 +202,7 @@ export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean { previousValue, }); + setConfigStore("funbox", newConfig); + return true; } diff --git a/frontend/src/ts/config/store.ts b/frontend/src/ts/config/store.ts index 1e73844649c3..5d35f3bd94b1 100644 --- a/frontend/src/ts/config/store.ts +++ b/frontend/src/ts/config/store.ts @@ -1,10 +1,29 @@ import type { Config as ConfigSchema } from "@monkeytype/schemas/configs"; import { getDefaultConfig } from "../constants/default-config"; -import { createStore } from "solid-js/store"; +import { createStore, reconcile } from "solid-js/store"; export const Config: ConfigSchema = { ...getDefaultConfig(), }; -export const [getConfig, setConfigStore] = +const [getConfigStore, setConfigStoreRaw] = createStore(getDefaultConfig()); + +export const getConfig = getConfigStore; + +export function setFullConfigStore(newConfig: ConfigSchema): void { + setConfigStoreRaw(reconcile(newConfig)); +} + +export function setConfigStore( + key: K, + value: ConfigSchema[K], +): void { + if (Array.isArray(value)) { + setConfigStoreRaw(key, reconcile(value)); + } else { + setConfigStoreRaw(key, value); + } +} + +// window.getConfigStore = getConfigStore; From 4bc52cd614f519b461e0cbdbc3290d76491d71b5 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 20:52:44 +0200 Subject: [PATCH 11/56] order --- frontend/src/ts/components/pages/settings/Settings.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index 0147f5a4b974..1f7ebee801fb 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -20,7 +20,6 @@ export function Settings(): JSXElement { return (
- @@ -33,7 +32,7 @@ export function Settings(): JSXElement { - {/* todo: funbox */} + {/* todo: custom layoutfluid */} {/* todo: polyglot languages */}
From 0981406ce4e7a1a983178cc3de9ff16db5178347 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 20:55:31 +0200 Subject: [PATCH 12/56] always auto --- .../ts/components/pages/settings/Settings.tsx | 202 +++++++++--------- 1 file changed, 97 insertions(+), 105 deletions(-) diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index 1f7ebee801fb..08536601df41 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -20,90 +20,90 @@ export function Settings(): JSXElement { return (
- - - - - - - + + + + + + + - + {/* todo: custom layoutfluid */} {/* todo: polyglot languages */}
- - - - - - - - - - + + + + + + + + + + {/* todo: layout emulator */} - +
{/* todo: sound volume */} - - - + + +
- - + + {/* pace caret */} - - + +
- - - - - - - - - + + + + + + + + + {/* todo: tape margin */} - - - - - + + + + + {/* todo: max line width */} {/* todo: font size */} {/* todo: font family */} - + {/* todo: keymap layout */} - - - + + + {/* todo: keymap size */}
- - + + {/* todo: custom background (url + local image + size) */} {/* todo: custom background filter */} - + {/* todo: auto switch theme inputs (light/dark selects) */} - + {/* todo: theme picker (preset + custom tabs) */}
- - - - + + + +
{/* todo: danger zone */}
@@ -141,59 +141,53 @@ function Section(props: { title: string; children: JSXElement }): JSXElement { function KeyedSetting(props: { key: keyof Config; inputs?: JSXElement; - fullWidthInputs?: JSXElement; - autoInputs?: boolean; - autoWide?: boolean; + wide?: boolean; }): JSXElement { const autoInputs = () => { - if (props.autoInputs === true) { - const options = getOptions(ConfigSchema.shape[props.key]); - if (options !== undefined) { - return ( -
- - {(option) => { - const text = () => { - const optionsMeta = configMetadata[props.key] - .optionsMetadata as - | Record - | undefined; - const match = optionsMeta?.[String(option)]; - if (match?.displayString !== undefined) { - return match.displayString; - } + const options = getOptions(ConfigSchema.shape[props.key]); + if (options !== undefined) { + return ( +
+ + {(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"; - } + if (option === true) { + return "on"; + } + if (option === false) { + return "off"; + } - return option.toString().replace(/_/g, " "); - }; - return ( - - ); - }} - -
- ); - } + return option.toString().replace(/_/g, " "); + }; + return ( + + ); + }} +
+
+ ); } return undefined; }; @@ -203,11 +197,9 @@ function KeyedSetting(props: { title={configMetadata[props.key].displayString ?? props.key} fa={configMetadata[props.key].fa} description={configMetadata[props.key].description} - inputs={!props.autoWide ? autoInputs() : props.inputs} + inputs={!props.wide ? autoInputs() : props.inputs} fullWidthInputs={ - props.autoWide - ? (autoInputs() ?? props.fullWidthInputs) - : props.fullWidthInputs + props.wide ? (autoInputs() ?? props.inputs) : props.inputs } /> ); From 0aed268fb59c510cacf014e1a4157fae2b6b511c Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 20:55:47 +0200 Subject: [PATCH 13/56] rename --- .../ts/components/pages/settings/Settings.tsx | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index 08536601df41..0401d5bdbf91 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -20,90 +20,90 @@ export function Settings(): JSXElement { return (
- - - - - - - + + + + + + + - + {/* todo: custom layoutfluid */} {/* todo: polyglot languages */}
- - - - - - - - - - + + + + + + + + + + {/* todo: layout emulator */} - +
{/* todo: sound volume */} - - - + + +
- - + + {/* pace caret */} - - + +
- - - - - - - - - + + + + + + + + + {/* todo: tape margin */} - - - - - + + + + + {/* todo: max line width */} {/* todo: font size */} {/* todo: font family */} - + {/* todo: keymap layout */} - - - + + + {/* todo: keymap size */}
- - + + {/* todo: custom background (url + local image + size) */} {/* todo: custom background filter */} - + {/* todo: auto switch theme inputs (light/dark selects) */} - + {/* todo: theme picker (preset + custom tabs) */}
- - - - + + + +
{/* todo: danger zone */}
@@ -138,7 +138,7 @@ function Section(props: { title: string; children: JSXElement }): JSXElement { ); } -function KeyedSetting(props: { +function KeyedAutoSetting(props: { key: keyof Config; inputs?: JSXElement; wide?: boolean; From ec6fac7ba83c2bd25f9399931cdc212cb06ba821 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 20:58:34 +0200 Subject: [PATCH 14/56] rename --- .../ts/components/pages/settings/Settings.tsx | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index 0401d5bdbf91..60a67d245923 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -20,90 +20,90 @@ export function Settings(): JSXElement { return (
- - - - - - - + + + + + + + - + {/* todo: custom layoutfluid */} {/* todo: polyglot languages */}
- - - - - - - - - - + + + + + + + + + + {/* todo: layout emulator */} - +
{/* todo: sound volume */} - - - + + +
- - + + {/* pace caret */} - - + +
- - - - - - - - - + + + + + + + + + {/* todo: tape margin */} - - - - - + + + + + {/* todo: max line width */} {/* todo: font size */} {/* todo: font family */} - + {/* todo: keymap layout */} - - - + + + {/* todo: keymap size */}
- - + + {/* todo: custom background (url + local image + size) */} {/* todo: custom background filter */} - + {/* todo: auto switch theme inputs (light/dark selects) */} - + {/* todo: theme picker (preset + custom tabs) */}
- - - - + + + +
{/* todo: danger zone */}
@@ -138,7 +138,7 @@ function Section(props: { title: string; children: JSXElement }): JSXElement { ); } -function KeyedAutoSetting(props: { +function AutoSetting(props: { key: keyof Config; inputs?: JSXElement; wide?: boolean; From 822e158e8f98783f281c9ba05aa4f40e57bd601d Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 21:04:09 +0200 Subject: [PATCH 15/56] only set overflow when necessary --- .../ts/components/common/anime/AnimeShow.tsx | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/frontend/src/ts/components/common/anime/AnimeShow.tsx b/frontend/src/ts/components/common/anime/AnimeShow.tsx index 00899655f4e7..8c6b6ab3953d 100644 --- a/frontend/src/ts/components/common/anime/AnimeShow.tsx +++ b/frontend/src/ts/components/common/anime/AnimeShow.tsx @@ -59,18 +59,40 @@ export function AnimeShow( > - } - animate={ - { height: "auto", duration: duration() } as AnimationParams - } - exit={{ height: 0, duration: duration() } as AnimationParams} - style={{ overflow: "hidden" }} - {...props.animeProps} - class={props.class} - > - {props.children} - + {(() => { + let ref: HTMLElement | undefined; + return ( + (ref = el)} + initial={{ height: 0 } as Partial} + animate={ + { + height: "auto", + duration: duration(), + onBegin: () => { + if (ref) ref.style.overflow = "hidden"; + }, + onComplete: () => { + if (ref) ref.style.overflow = ""; + }, + } as AnimationParams + } + exit={ + { + height: 0, + duration: duration(), + onBegin: () => { + if (ref) ref.style.overflow = "hidden"; + }, + } as AnimationParams + } + {...props.animeProps} + class={props.class} + > + {props.children} + + ); + })()} From a48b61a14ded70c23ee3a6e71c2e3d07ba61484a Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 21:05:32 +0200 Subject: [PATCH 16/56] ballon size --- .../src/ts/components/pages/settings/custom-setting/Funbox.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx index e867d44ea346..7ee9da993924 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Funbox.tsx @@ -66,6 +66,7 @@ export function Funbox(): JSXElement { disabled={disabled()} balloon={{ text: funbox.description, + length: "xlarge", }} class="w-full" > From 15439ba05431ad37b5a5cc6a87fc4ee1bee7773c Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 21:28:36 +0200 Subject: [PATCH 17/56] custom poly, custom layoutfluid --- .../ts/components/pages/settings/Settings.tsx | 6 +- .../custom-setting/CustomLayoutfluid.tsx | 46 +++++++++++++++ .../custom-setting/CustomPolyglot.tsx | 57 +++++++++++++++++++ frontend/src/ts/config/metadata.tsx | 2 +- 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 frontend/src/ts/components/pages/settings/custom-setting/CustomLayoutfluid.tsx create mode 100644 frontend/src/ts/components/pages/settings/custom-setting/CustomPolyglot.tsx diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index 60a67d245923..fe2e38b9666f 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -9,6 +9,8 @@ import { getOptions } from "../../../utils/zod"; import { Anime, AnimeShow } from "../../common/anime"; import { Button } from "../../common/Button"; import { Fa } from "../../common/Fa"; +import { CustomLayoutfluid } from "./custom-setting/CustomLayoutfluid"; +import { CustomPolyglot } from "./custom-setting/CustomPolyglot"; import { Funbox } from "./custom-setting/Funbox"; import { Language } from "./custom-setting/Language"; import { MinAcc } from "./custom-setting/MinAcc"; @@ -33,8 +35,8 @@ export function Settings(): JSXElement { - {/* todo: custom layoutfluid */} - {/* todo: polyglot languages */} + +
diff --git a/frontend/src/ts/components/pages/settings/custom-setting/CustomLayoutfluid.tsx b/frontend/src/ts/components/pages/settings/custom-setting/CustomLayoutfluid.tsx new file mode 100644 index 000000000000..9bf531f821c9 --- /dev/null +++ b/frontend/src/ts/components/pages/settings/custom-setting/CustomLayoutfluid.tsx @@ -0,0 +1,46 @@ +import { CustomLayoutFluid } from "@monkeytype/schemas/configs"; +import { JSXElement } from "solid-js"; + +import { configMetadata } from "../../../../config/metadata"; +import { setConfig } from "../../../../config/setters"; +import { getConfig } from "../../../../config/store"; +import { LayoutsList } from "../../../../constants/layouts"; +import { areUnsortedArraysEqual } from "../../../../utils/arrays"; +import SlimSelect from "../../../ui/SlimSelect"; +import { Setting } from "../Setting"; + +export function CustomLayoutfluid(): JSXElement { + return ( + ({ + text: layout.replace(/_/g, " "), + value: layout, + }))} + selected={getConfig.customLayoutfluid} + onChange={(val) => { + if ( + areUnsortedArraysEqual( + getConfig.customLayoutfluid, + val as CustomLayoutFluid, + ) + ) { + return; + } + setConfig("customLayoutfluid", val as CustomLayoutFluid); + }} + /> + } + /> + ); +} diff --git a/frontend/src/ts/components/pages/settings/custom-setting/CustomPolyglot.tsx b/frontend/src/ts/components/pages/settings/custom-setting/CustomPolyglot.tsx new file mode 100644 index 000000000000..8185fd0eba41 --- /dev/null +++ b/frontend/src/ts/components/pages/settings/custom-setting/CustomPolyglot.tsx @@ -0,0 +1,57 @@ +import { CustomPolyglot as CustomPolyglotType } from "@monkeytype/schemas/configs"; +import { Optgroup } from "slim-select/store"; +import { JSXElement } from "solid-js"; + +import { configMetadata } from "../../../../config/metadata"; +import { setConfig } from "../../../../config/setters"; +import { getConfig } from "../../../../config/store"; +import { + LanguageGroupNames, + LanguageGroups, +} from "../../../../constants/languages"; +import { areUnsortedArraysEqual } from "../../../../utils/arrays"; +import { getLanguageDisplayString } from "../../../../utils/strings"; +import SlimSelect from "../../../ui/SlimSelect"; +import { Setting } from "../Setting"; + +export function CustomPolyglot(): JSXElement { + return ( + + ({ + label: group, + options: LanguageGroups[group]?.map((language) => ({ + text: getLanguageDisplayString(language), + value: language, + })), + }) as Optgroup, + )} + selected={getConfig.customPolyglot} + onChange={(val) => { + if ( + areUnsortedArraysEqual( + getConfig.customPolyglot, + val as CustomPolyglotType, + ) + ) { + return; + } + setConfig("customPolyglot", val as CustomPolyglotType); + }} + /> + } + /> + ); +} diff --git a/frontend/src/ts/config/metadata.tsx b/frontend/src/ts/config/metadata.tsx index 1060162b0b91..50b205b39a1d 100644 --- a/frontend/src/ts/config/metadata.tsx +++ b/frontend/src/ts/config/metadata.tsx @@ -441,7 +441,7 @@ export const configMetadata: ConfigMetadataObject = { customPolyglot: { key: "customPolyglot", fa: { icon: "fa-language" }, - displayString: "custom polyglot", + displayString: "polyglot languages", changeRequiresRestart: false, group: "behavior", description: "Select which languages you want the polyglot funbox to use.", From b373dc303392e5a73fbc87a08221657eb28b8830 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 21:35:33 +0200 Subject: [PATCH 18/56] compare --- frontend/src/html/pages/settings.html | 3815 +++++++++++++------------ 1 file changed, 1928 insertions(+), 1887 deletions(-) diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index 053c82ccaab5..5818e02a9296 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -1,1954 +1,1995 @@
diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Layout.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Layout.tsx new file mode 100644 index 000000000000..f4bcf5ea5ea6 --- /dev/null +++ b/frontend/src/ts/components/pages/settings/custom-setting/Layout.tsx @@ -0,0 +1,33 @@ +import { Layout as LayoutSchema } from "@monkeytype/schemas/configs"; +import { JSXElement } from "solid-js"; + +import { configMetadata } from "../../../../config/metadata"; +import { setConfig } from "../../../../config/setters"; +import { getConfig } from "../../../../config/store"; +import { LayoutsList } from "../../../../constants/layouts"; +import SlimSelect from "../../../ui/SlimSelect"; +import { Setting } from "../Setting"; + +export function Layout(): JSXElement { + return ( + ({ + text: layout.replace(/_/g, " "), + value: layout, + }))} + selected={getConfig.layout} + onChange={(val) => { + if (getConfig.layout === (val as LayoutSchema)) return; + setConfig("layout", val as LayoutSchema); + }} + /> + } + /> + ); +} From b87048cbd0bd0efc4a503c4525828f40d1fc7333 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 22:05:52 +0200 Subject: [PATCH 22/56] options metadata --- frontend/src/ts/config/metadata.tsx | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/frontend/src/ts/config/metadata.tsx b/frontend/src/ts/config/metadata.tsx index 50b205b39a1d..bd991e055654 100644 --- a/frontend/src/ts/config/metadata.tsx +++ b/frontend/src/ts/config/metadata.tsx @@ -596,6 +596,24 @@ export const configMetadata: ConfigMetadataObject = { }, playSoundOnClick: { key: "playSoundOnClick", + optionsMetadata: { + "1": { displayString: "click" }, + "2": { displayString: "beep" }, + "3": { displayString: "pop" }, + "4": { displayString: "nk creams" }, + "5": { displayString: "typewriter" }, + "6": { displayString: "osu" }, + "7": { displayString: "hitmarker" }, + "8": { displayString: "sine" }, + "9": { displayString: "sawtooth" }, + "10": { displayString: "square" }, + "11": { displayString: "triangle" }, + "12": { displayString: "pentatonic" }, + "13": { displayString: "wholetone" }, + "14": { displayString: "fist fight" }, + "15": { displayString: "rubber keys" }, + "16": { displayString: "fart" }, + }, fa: { icon: "fa-volume-up" }, displayString: "play sound on click", changeRequiresRestart: false, @@ -604,6 +622,12 @@ export const configMetadata: ConfigMetadataObject = { }, playSoundOnError: { key: "playSoundOnError", + optionsMetadata: { + "1": { displayString: "damage" }, + "2": { displayString: "triangle" }, + "3": { displayString: "square" }, + "4": { displayString: "missed punch" }, + }, fa: { icon: "fa-volume-mute" }, displayString: "play sound on error", changeRequiresRestart: false, @@ -613,6 +637,12 @@ export const configMetadata: ConfigMetadataObject = { }, playTimeWarning: { key: "playTimeWarning", + optionsMetadata: { + "1": { displayString: "1 second" }, + "3": { displayString: "3 seconds" }, + "5": { displayString: "5 seconds" }, + "10": { displayString: "10 seconds" }, + }, fa: { icon: "fa-exclamation-triangle" }, displayString: "play time warning", changeRequiresRestart: false, From 471ed0f80c3ca716785fcfcb226999b0b59dee90 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 22:21:25 +0200 Subject: [PATCH 23/56] last one for today --- frontend/src/ts/components/common/Slider.tsx | 39 +++++++++++++++++++ .../ts/components/pages/settings/Settings.tsx | 3 +- .../settings/custom-setting/SoundVolume.tsx | 32 +++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 frontend/src/ts/components/common/Slider.tsx create mode 100644 frontend/src/ts/components/pages/settings/custom-setting/SoundVolume.tsx diff --git a/frontend/src/ts/components/common/Slider.tsx b/frontend/src/ts/components/common/Slider.tsx new file mode 100644 index 000000000000..4da7a338c6d1 --- /dev/null +++ b/frontend/src/ts/components/common/Slider.tsx @@ -0,0 +1,39 @@ +import { createEffect, createSignal, JSXElement } from "solid-js"; + +type Props = { + value: number; + min: number; + max: number; + step?: number; + onChange?: (value: number) => void; + text?: (value: number) => string | JSXElement; +}; + +export function Slider(props: Props): JSXElement { + // oxlint-disable-next-line solid/reactivity + const [value, setValue] = createSignal(props.value); + + createEffect(() => setValue(props.value)); + + const textToDisplay = () => { + if (props.text) { + return props.text(value()); + } + return value(); + }; + + return ( +
+
{textToDisplay()}
+ setValue(Number(e.target.value))} + onChange={(e) => props.onChange?.(Number(e.target.value))} + /> +
+ ); +} diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index e3d36c54ef2a..f8d16bbb12c5 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -22,6 +22,7 @@ import { Layout } from "./custom-setting/Layout"; import { MinAcc } from "./custom-setting/MinAcc"; import { MinBurst } from "./custom-setting/MinBurst"; import { MinSpeed } from "./custom-setting/MinSpeed"; +import { SoundVolume } from "./custom-setting/SoundVolume"; import { QuickNav } from "./QuickNav"; import { Setting } from "./Setting"; @@ -80,7 +81,7 @@ export function Settings(): JSXElement {
- {/* todo: sound volume */} + diff --git a/frontend/src/ts/components/pages/settings/custom-setting/SoundVolume.tsx b/frontend/src/ts/components/pages/settings/custom-setting/SoundVolume.tsx new file mode 100644 index 000000000000..0d2ebd13f244 --- /dev/null +++ b/frontend/src/ts/components/pages/settings/custom-setting/SoundVolume.tsx @@ -0,0 +1,32 @@ +import { JSXElement } from "solid-js"; + +import { configMetadata } from "../../../../config/metadata"; +import { setConfig } from "../../../../config/setters"; +import { getConfig } from "../../../../config/store"; +import { Slider } from "../../../common/Slider"; +import { Setting } from "../Setting"; + +export function SoundVolume(): JSXElement { + return ( + { + return value.toFixed(1); + }} + value={getConfig.soundVolume} + onChange={(value) => { + if (value === getConfig.soundVolume) return; + setConfig("soundVolume", value); + }} + /> + } + /> + ); +} From b34a52fc18864ee81d267993fde535a0102c1ca6 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 22:29:26 +0200 Subject: [PATCH 24/56] aiaiai --- .../src/ts/components/pages/settings/Settings.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index f8d16bbb12c5..7007cf3e66d5 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -6,14 +6,14 @@ import { configMetadata } from "../../../config/metadata"; import { setConfig } from "../../../config/setters"; import { getConfig } from "../../../config/store"; import { useLocalStorage } from "../../../hooks/useLocalStorage"; -import { hotkeys } from "../../../states/hotkeys"; +// import { hotkeys } from "../../../states/hotkeys"; import { cn } from "../../../utils/cn"; -import { isFirefox } from "../../../utils/misc"; +// import { isFirefox } from "../../../utils/misc"; import { getOptions } from "../../../utils/zod"; import { Anime, AnimeShow } from "../../common/anime"; import { Button } from "../../common/Button"; import { Fa } from "../../common/Fa"; -import { Kbd } from "../../common/Kbd"; +// import { Kbd } from "../../common/Kbd"; import { CustomLayoutfluid } from "./custom-setting/CustomLayoutfluid"; import { CustomPolyglot } from "./custom-setting/CustomPolyglot"; import { Funbox } from "./custom-setting/Funbox"; @@ -30,7 +30,8 @@ export function Settings(): JSXElement { return (
- + {/* todo: bring back */} + {/*
tip: You can also change all these settings quickly using the command line @@ -41,7 +42,7 @@ export function Settings(): JSXElement { )
-
+
*/} @@ -145,7 +163,11 @@ function FieldInput(props: {
diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index c7ef2d10a928..a8cd917473e4 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -62,17 +62,17 @@ export function Settings(): JSXElement {
*/} -
{/* todo: tags */} {/* todo: presets */} - - - - + {/* */} + {/* */} + {/* */} + {/* */} diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx index acb986c18b63..749009492a14 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx @@ -1,13 +1,31 @@ -import { createSignal, For, JSXElement } from "solid-js"; +import { For, JSXElement, Show } from "solid-js"; +import { debounce } from "throttle-debounce"; import { configMetadata } from "../../../../config/metadata"; +import { setConfig } from "../../../../config/setters"; import { getConfig } from "../../../../config/store"; -import { ThemesList, ThemeWithName } from "../../../../constants/themes"; +import { + ColorName, + ThemesList, + ThemeWithName, +} from "../../../../constants/themes"; +import { + convertCustomColorsToTheme, + convertThemeToCustomColors, +} from "../../../../controllers/theme-controller"; +import { createEffectOn } from "../../../../hooks/effects"; +import { + showNoticeNotification, + showSuccessNotification, +} from "../../../../states/notifications"; +import { showSimpleModal } from "../../../../states/simple-modal"; +import { getTheme, setTheme, updateThemeColor } from "../../../../states/theme"; import { cn } from "../../../../utils/cn"; import { hexToHSL } from "../../../../utils/colors"; import { AnimeConditional } from "../../../common/anime"; import { Button } from "../../../common/Button"; import { Fa } from "../../../common/Fa"; +import { Separator } from "../../../common/Separator"; import { Setting } from "../Setting"; export const sortedThemes: ThemeWithName[] = [...ThemesList].sort((a, b) => { @@ -17,8 +35,169 @@ export const sortedThemes: ThemeWithName[] = [...ThemesList].sort((a, b) => { }); export function Theme(): JSXElement { - const [currentTab, setCurrentTab] = createSignal<"preset" | "custom">( - "preset", + const Presets = () => ( +
+ 0}> +
+ + getConfig.favThemes.includes(t.name), + )} + > + {(theme) => } + +
+
+ 0}> + + +
+ !getConfig.favThemes.includes(t.name), + )} + > + {(theme) => } + +
+
+ ); + + const Customs = () => ( +
+
+ + + + + + + + +
+ when colorful mode is enabled: +
+ + +
+
+
+
+ ); + + createEffectOn( + () => getConfig.customTheme, + (custom) => { + if (custom) { + const colorsObj = convertCustomColorsToTheme( + getConfig.customThemeColors, + ); + setTheme({ ...colorsObj, name: "custom" }); + } + }, ); return ( @@ -29,13 +208,13 @@ export function Theme(): JSXElement { inputs={
@@ -43,33 +222,19 @@ export function Theme(): JSXElement { fullWidthInputs={ - - {(theme) => ( - - )} - -
- } - else={<>custom} + if={!getConfig.customTheme} + then={} + else={} /> } /> ); } -function ThemeButton(props: { - name: string; - theme: ThemeWithName; - active?: boolean; - favorite?: boolean; -}): JSXElement { +function ThemeButton(props: { theme: ThemeWithName }): JSXElement { + const isActive = () => getConfig.theme === props.theme.name; + const isFav = () => getConfig.favThemes.includes(props.theme.name); + return ( ); } + +function Picker(props: { color: ColorName }): JSXElement { + let colorInputRef: HTMLInputElement | undefined = undefined; + + const text = () => { + if (props.color === "bg") return "background"; + if (props.color === "main") return "main"; + if (props.color === "sub") return "sub"; + if (props.color === "subAlt") return "sub alt"; + if (props.color === "caret") return "caret"; + if (props.color === "text") return "text"; + if (props.color === "error") return "error"; + if (props.color === "errorExtra") return "extra error"; + if (props.color === "colorfulError") return "error"; + if (props.color === "colorfulErrorExtra") return "extra error"; + return "unknown"; + }; + + const _classes = [ + "bg-(--picker-bg)", + "bg-(--picker-main)", + "bg-(--picker-caret)", + "bg-(--picker-sub)", + "bg-(--picker-subAlt)", + "bg-(--picker-text)", + "bg-(--picker-error)", + "bg-(--picker-errorExtra)", + "bg-(--picker-colorfulError)", + "bg-(--picker-colorfulErrorExtra)", + ]; + + // oxlint-disable-next-line solid/reactivity + const debouncedInput = debounce(125, (e: InputEvent) => { + const target = e.target as HTMLInputElement; + const color = target.value; + const key = props.color; + + updateThemeColor(key, color); + }); + + return ( +
+
{text()}
+ (colorInputRef = el)} + type="color" + value={getTheme()[props.color]} + onInput={debouncedInput} + // onChange={(e) => { + // const current = [...getConfig.customThemeColors]; + // current[colorIndex()] = e.currentTarget.value; + // setConfig( + // "customThemeColors", + // current as typeof getConfig.customThemeColors, + // ); + // }} + /> + { + const value = e.currentTarget.value; + if (!/^#([0-9A-Fa-f]{3}){1,2}$/.test(value)) { + // invalid hex color + e.currentTarget.value = getTheme()[props.color]; + return; + } + updateThemeColor(props.color, value); + }} + /> +
+ ); +} diff --git a/frontend/src/ts/components/ui/form/InputField.tsx b/frontend/src/ts/components/ui/form/InputField.tsx index dc423c9c1927..a0b1841c9f4f 100644 --- a/frontend/src/ts/components/ui/form/InputField.tsx +++ b/frontend/src/ts/components/ui/form/InputField.tsx @@ -11,6 +11,8 @@ export function InputField(props: { autocomplete?: string; type?: string; disabled?: boolean; + readOnly?: boolean; + clickToSelect?: boolean; class?: string; dir?: "ltr" | "rtl" | "auto"; maxLength?: number; @@ -36,6 +38,10 @@ export function InputField(props: { onBlur={() => props.field().handleBlur()} onInput={(e) => props.field().handleChange(e.target.value)} disabled={props.disabled} + readOnly={props.readOnly} + onClick={(e) => { + if (props.clickToSelect) e.currentTarget.select(); + }} onFocus={() => props.onFocus?.()} dir={props.dir} maxLength={props.maxLength} diff --git a/frontend/src/ts/config/metadata.tsx b/frontend/src/ts/config/metadata.tsx index 519f7b00d444..a94464276c38 100644 --- a/frontend/src/ts/config/metadata.tsx +++ b/frontend/src/ts/config/metadata.tsx @@ -1124,7 +1124,8 @@ export const configMetadata: ConfigMetadataObject = { fa: { icon: "fa-palette" }, changeRequiresRestart: false, group: "theme", - description: "Change the theme of the website.", + description: + "Completely change the look and feel of the website by picking one of the presets, or by creating your own completely custom theme.", overrideConfig: () => { return { customTheme: false, diff --git a/frontend/src/ts/states/simple-modal.ts b/frontend/src/ts/states/simple-modal.ts index 911bffbafa38..5b56b86c8a8d 100644 --- a/frontend/src/ts/states/simple-modal.ts +++ b/frontend/src/ts/states/simple-modal.ts @@ -19,6 +19,7 @@ type CommonInput = { disabled?: boolean; optional?: boolean; label?: string; + class?: string; oninput?: (event: Event) => void; validation?: { schema?: z.Schema; @@ -27,8 +28,14 @@ type CommonInput = { }; }; -export type TextInput = CommonInput<"text", string>; -export type TextArea = CommonInput<"textarea", string>; +export type TextInput = { + readOnly?: boolean; + clickToSelect?: boolean; +} & CommonInput<"text", string>; +export type TextArea = { + readOnly?: boolean; + clickToSelect?: boolean; +} & CommonInput<"textarea", string>; export type PasswordInput = CommonInput<"password", string>; type EmailInput = CommonInput<"email", string>; @@ -80,11 +87,13 @@ export type ExecReturn = { }; export type SimpleModalConfig = { + class?: string; title: string; inputs?: SimpleModalInput[]; text?: string; textAllowHtml?: boolean; - buttonText: string; + buttonText?: string; + buttonAlwaysEnabled?: boolean; execFn: (...inputValues: string[]) => Promise; }; From bd9d55d3a45ff7c20480a3b79996b1077e8e27b0 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 11 Apr 2026 21:35:35 +0200 Subject: [PATCH 45/56] limit cookie import export reset --- frontend/src/ts/anim.ts | 8 +- frontend/src/ts/components/common/Button.tsx | 4 + .../ts/components/pages/settings/Settings.tsx | 61 ++++++++++++ .../custom-setting/AnimationFpsLimit.tsx | 93 ++++++++++++++++++ .../settings/custom-setting/ImportExport.tsx | 97 +++++++++++++++++++ 5 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx create mode 100644 frontend/src/ts/components/pages/settings/custom-setting/ImportExport.tsx diff --git a/frontend/src/ts/anim.ts b/frontend/src/ts/anim.ts index 48b81f4dd095..0446820a07e1 100644 --- a/frontend/src/ts/anim.ts +++ b/frontend/src/ts/anim.ts @@ -1,6 +1,7 @@ import { engine } from "animejs"; import { LocalStorageWithSchema } from "./utils/local-storage-with-schema"; import { z } from "zod"; +import { createSignal } from "solid-js"; export const fpsLimitSchema = z.number().int().min(15).max(1000); @@ -10,14 +11,19 @@ const fpsLimit = new LocalStorageWithSchema({ fallback: 1000, }); +const [fpsLimitSignal, setFpsLimitSignal] = createSignal(fpsLimit.get()); + export function setfpsLimit(fps: number): boolean { const result = fpsLimit.set(fps); + if (result) { + setFpsLimitSignal(fps); + } applyEngineSettings(); return result; } export function getfpsLimit(): number { - return fpsLimit.get(); + return fpsLimitSignal(); } export function applyEngineSettings(): void { diff --git a/frontend/src/ts/components/common/Button.tsx b/frontend/src/ts/components/common/Button.tsx index 6527f74c9643..a692bec65b6d 100644 --- a/frontend/src/ts/components/common/Button.tsx +++ b/frontend/src/ts/components/common/Button.tsx @@ -26,6 +26,7 @@ export type ButtonProps = BaseProps & { href?: never; sameTarget?: true; disabled?: boolean; + danger?: boolean; }; type AnchorProps = BaseProps & { @@ -70,6 +71,9 @@ export function Button(props: ButtonProps | AnchorProps): JSXElement { variant() === "text" && isActive() && "[--themable-button-hover-text:var(--themable-button-hover-text)] [--themable-button-text:var(--themable-button-active)]", + !isAnchor() && + (props as ButtonProps).danger && + "[--themable-button-bg:var(--error-color)] [--themable-button-hover-bg:var(--text-color)] [--themable-button-hover-text:var(--bg-color)] [--themable-button-text:var(--bg-color)]", { "pointer-events-none opacity-[0.33]": props.disabled, }, diff --git a/frontend/src/ts/components/pages/settings/Settings.tsx b/frontend/src/ts/components/pages/settings/Settings.tsx index a8cd917473e4..b41505f79855 100644 --- a/frontend/src/ts/components/pages/settings/Settings.tsx +++ b/frontend/src/ts/components/pages/settings/Settings.tsx @@ -3,10 +3,13 @@ import { createForm } from "@tanstack/solid-form"; import { createResource, createSignal, For, 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 { useLocalStorage } from "../../../hooks/useLocalStorage"; +import { showErrorNotification } from "../../../states/notifications"; +import { showSimpleModal } from "../../../states/simple-modal"; // import { hotkeys } from "../../../states/hotkeys"; import { cn } from "../../../utils/cn"; import fileStorage from "../../../utils/file-storage"; @@ -17,6 +20,7 @@ import { Button } from "../../common/Button"; import { Fa } from "../../common/Fa"; 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"; import { CustomBackgroundFilters } from "./custom-setting/CustomBackgroundFilters"; @@ -25,6 +29,7 @@ import { CustomLayoutfluid } from "./custom-setting/CustomLayoutfluid"; import { CustomPolyglot } from "./custom-setting/CustomPolyglot"; import { FontFamily } from "./custom-setting/FontFamily"; import { Funbox } from "./custom-setting/Funbox"; +import { ImportExport } from "./custom-setting/ImportExport"; import { KeymapLayout } from "./custom-setting/KeymapLayout"; import { KeymapSize } from "./custom-setting/KeymapSize"; import { Language } from "./custom-setting/Language"; @@ -156,7 +161,63 @@ export function Settings(): JSXElement {
+ + { + showErrorNotification("//todo"); + }} + > + open + + } + /> + + + Resets settings to the default (but doesn't touch your tags + and presets). +
+
You can't undo this!
+
+ } + fa={{ + icon: "fa-cookie-bite", + }} + inputs={ + + } + /> diff --git a/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx b/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx new file mode 100644 index 000000000000..49188ba4ffcd --- /dev/null +++ b/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx @@ -0,0 +1,93 @@ +import { createForm } from "@tanstack/solid-form"; +import { createSignal, JSXElement } from "solid-js"; + +import { fpsLimitSchema, getfpsLimit, setfpsLimit } from "../../../../anim"; +import { AnimeShow } from "../../../common/anime"; +import { Button } from "../../../common/Button"; +import { Fa } from "../../../common/Fa"; +import { Separator } from "../../../common/Separator"; +import { InputField } from "../../../ui/form/InputField"; +import { fromSchema } from "../../../ui/form/utils"; +import { Setting } from "../Setting"; + +export function AnimationFpsLimit(): JSXElement { + const [showSavedIndicator, setShowSavedIndicator] = createSignal(false); + const form = createForm(() => ({ + defaultValues: { + fpsLimit: "", + }, + onSubmit: ({ value }) => { + const val = parseInt(String(value.fpsLimit)); + if (val === getfpsLimit()) return; + setfpsLimit(val); + setShowSavedIndicator(true); + setTimeout(() => { + setShowSavedIndicator(false); + }, 2000); + }, + })); + + return ( + + + + + } + /> + ); +} From 9a983c3ec5701db38da0e0266a9a356ac17b7118 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 11 Apr 2026 21:37:07 +0200 Subject: [PATCH 46/56] meta --- frontend/src/ts/config/metadata.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/ts/config/metadata.tsx b/frontend/src/ts/config/metadata.tsx index a94464276c38..130679529d1f 100644 --- a/frontend/src/ts/config/metadata.tsx +++ b/frontend/src/ts/config/metadata.tsx @@ -1163,6 +1163,10 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "hideElements", description: "Shows the keybind tips at the bottom of the page.", + optionsMetadata: { + true: { displayString: "show" }, + false: { displayString: "hide" }, + }, }, showOutOfFocusWarning: { key: "showOutOfFocusWarning", @@ -1172,6 +1176,10 @@ export const configMetadata: ConfigMetadataObject = { group: "hideElements", description: "Shows an out of focus reminder after 1 second of being 'out of focus' (not being able to type).", + optionsMetadata: { + true: { displayString: "show" }, + false: { displayString: "hide" }, + }, }, capsLockWarning: { key: "capsLockWarning", @@ -1180,6 +1188,10 @@ export const configMetadata: ConfigMetadataObject = { changeRequiresRestart: false, group: "hideElements", description: "Displays a warning when caps lock is on.", + optionsMetadata: { + true: { displayString: "show" }, + false: { displayString: "hide" }, + }, }, showAverage: { key: "showAverage", From c110c0128d3b2de7b0dcbb0c072db2801284ed3f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sat, 11 Apr 2026 21:50:04 +0200 Subject: [PATCH 47/56] fiiix --- frontend/src/ts/components/pages/settings/QuickNav.tsx | 9 +++++++-- .../components/pages/settings/custom-setting/Theme.tsx | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/components/pages/settings/QuickNav.tsx b/frontend/src/ts/components/pages/settings/QuickNav.tsx index cb21699d1726..761994908252 100644 --- a/frontend/src/ts/components/pages/settings/QuickNav.tsx +++ b/frontend/src/ts/components/pages/settings/QuickNav.tsx @@ -7,8 +7,13 @@ export function QuickNav(): JSXElement { const buttonClass = "px-3 py-3"; return (
- {/* todo: responsiveness */} -
+
+
+ } + else={ +
+ + We use Cloudflare cookies to improve security and performance + of our site. They do not store any personal information and + are required. +
+ } + checked={true} + disabled={true} + /> + + setAccepted({ ...accepted(), analytics: checked }) + } + /> + + setAccepted({ ...accepted(), sentry: checked }) + } + /> + +
+ Our advertising partner may use cookies to deliver ads that + are more relevant to you. +
+
+ } + checked={false} + hideCheckbox={true} + /> +