From de3e120ce94f9d40797ee951629343859a7dd7e1 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 6 Apr 2026 13:07:21 +0200 Subject: [PATCH 001/126] 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 007/126] 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 008/126] 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 009/126] 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 010/126] 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 011/126] 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 012/126] 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 013/126] 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 014/126] 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 015/126] 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 016/126] 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 017/126] 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 018/126] 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 022/126] 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 023/126] 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 024/126] 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 045/126] 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 046/126] 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 047/126] 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} + /> + + ); +} + function ThemeButton(props: { theme: ThemeWithName }): JSXElement { const isActive = () => getConfig.theme === props.theme.name; const isFav = () => getConfig.favThemes.includes(props.theme.name); diff --git a/frontend/src/ts/config/metadata.tsx b/frontend/src/ts/config/metadata.tsx index 0f2009cc462c..c7b65dffd24d 100644 --- a/frontend/src/ts/config/metadata.tsx +++ b/frontend/src/ts/config/metadata.tsx @@ -3,8 +3,8 @@ import * as ConfigSchemas from "@monkeytype/schemas/configs"; import { roundTo1 } from "@monkeytype/util/numbers"; import { JSXElement } from "solid-js"; +import * as CustomThemes from "../collections/custom-themes"; import { getDefaultConfig } from "../constants/default-config"; -import * as DB from "../db"; import { isAuthenticated } from "../states/core"; import { showNoticeNotification } from "../states/notifications"; import { FaObject } from "../types/font-awesome"; @@ -1110,20 +1110,13 @@ export const configMetadata: ConfigMetadataObject = { }, isBlocked: ({ value }) => { if (value === "custom") { - const snapshot = DB.getSnapshot(); if (!isAuthenticated()) { showNoticeNotification( "Random theme 'custom' is unavailable without an account", ); return true; } - if (!snapshot) { - showNoticeNotification( - "Random theme 'custom' requires a snapshot to be set", - ); - return true; - } - if (snapshot?.customThemes?.length === 0) { + if (CustomThemes.__nonReactive.getCustomThemes().length === 0) { showNoticeNotification( "Random theme 'custom' requires at least one custom theme to be saved", ); diff --git a/frontend/src/ts/constants/default-snapshot.ts b/frontend/src/ts/constants/default-snapshot.ts index 17ca8074d841..74f192019a34 100644 --- a/frontend/src/ts/constants/default-snapshot.ts +++ b/frontend/src/ts/constants/default-snapshot.ts @@ -54,6 +54,7 @@ export type Snapshot = Omit< | "streak" | "resultFilterPresets" | "tags" + | "customThemes" | "xp" | "testActivity" > & { @@ -87,7 +88,6 @@ const defaultSnap = { uid: "", isPremium: false, config: getDefaultConfig(), - customThemes: [], banned: undefined, verified: undefined, lbMemory: { time: { 15: { english: 0 }, 60: { english: 0 } } }, diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index 7fca36a702dd..64a4008ff182 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -5,7 +5,7 @@ import { Config } from "../config/store"; import { setConfig } from "../config/setters"; import * as BackgroundFilter from "../elements/custom-background-filter"; import { configEvent } from "../events/config"; -import * as DB from "../db"; +import * as CustomThemes from "../collections/custom-themes"; import { showNoticeNotification } from "../states/notifications"; import { debounce } from "throttle-debounce"; import { CustomThemeColors, ThemeName } from "@monkeytype/schemas/configs"; @@ -88,10 +88,11 @@ function updateThemeIndicator(nameOverride?: string): void { if (Config.customTheme && nameOverride === undefined) { // Match current custom theme by colors since Config does not store custom theme IDs - const snapshot = DB.getSnapshot(); - const matchedTheme = snapshot?.customThemes?.find((ct) => - Arrays.areSortedArraysEqual(ct.colors, Config.customThemeColors), - ); + const matchedTheme = CustomThemes.__nonReactive + .getCustomThemes() + .find((ct) => + Arrays.areSortedArraysEqual(ct.colors, Config.customThemeColors), + ); if (matchedTheme) { str = `${matchedTheme.name} (custom)`; @@ -181,8 +182,10 @@ async function changeThemeList(): Promise { themesList = themes.map((t) => { return t.name; }); - } else if (Config.randomTheme === "custom" && DB.getSnapshot()) { - themesList = DB.getSnapshot()?.customThemes?.map((ct) => ct._id) ?? []; + } else if (Config.randomTheme === "custom") { + themesList = CustomThemes.__nonReactive + .getCustomThemes() + .map((ct) => ct._id); } Arrays.shuffle(themesList); randomThemeIndex = 0; @@ -213,8 +216,8 @@ export async function randomizeTheme(): Promise { let colorsOverride: CustomThemeColors | undefined; if (Config.randomTheme === "custom") { - const theme = DB.getSnapshot()?.customThemes?.find( - (ct) => ct._id === randomTheme, + const theme = CustomThemes.__nonReactive.getCustomTheme( + randomTheme as string, ); colorsOverride = theme?.colors; randomTheme = "custom"; @@ -229,7 +232,7 @@ export async function randomizeTheme(): Promise { let name = randomTheme.replace(/_/g, " "); if (Config.randomTheme === "custom") { name = ( - DB.getSnapshot()?.customThemes?.find((ct) => ct._id === randomTheme) + CustomThemes.__nonReactive.getCustomTheme(randomTheme as string) ?.name ?? "custom" ).replace(/_/g, " "); } diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 8538ccd13799..dfb1fe549365 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -1,8 +1,5 @@ import Ape from "./ape"; -import { - showNoticeNotification, - showErrorNotification, -} from "./states/notifications"; +import { showErrorNotification } from "./states/notifications"; import { getAuthenticatedUser } from "./firebase"; import { isAuthenticated } from "./states/core"; import * as Dates from "date-fns"; @@ -11,7 +8,7 @@ import { ModifiableTestActivityCalendar, } from "./elements/test-activity-calendar"; import { showLoaderBar, hideLoaderBar } from "./states/loader-bar"; -import { Badge, CustomTheme } from "@monkeytype/schemas/users"; +import { Badge } from "@monkeytype/schemas/users"; import { Difficulty } from "@monkeytype/schemas/configs"; import { Mode, @@ -44,6 +41,7 @@ import { FunboxMetadata } from "@monkeytype/funbox"; import { fillTagsCollection, __nonReactive } from "./collections/tags"; import { updateTagsInFilterStorage } from "./states/result-filters"; import { fillPresetsCollection } from "./collections/presets"; +import { fillCustomThemesCollection } from "./collections/custom-themes"; let dbSnapshot: Snapshot | undefined; const firstDayOfTheWeek = getFirstDayOfTheWeek(); @@ -195,8 +193,7 @@ export async function initSnapshot(): Promise { snap.lbMemory = userData.lbMemory; } - snap.customThemes = userData.customThemes ?? []; - + fillCustomThemesCollection(userData.customThemes ?? []); fillTagsCollection(userData.tags ?? []); fillPresetsCollection(presetsData ?? []); @@ -216,94 +213,6 @@ export async function initSnapshot(): Promise { } } -export async function addCustomTheme( - theme: Omit, -): Promise { - if (!dbSnapshot) return false; - - dbSnapshot.customThemes ??= []; - - if (dbSnapshot.customThemes.length >= 20) { - showNoticeNotification("Too many custom themes!"); - return false; - } - - const response = await Ape.users.addCustomTheme({ body: { ...theme } }); - if (response.status !== 200) { - showErrorNotification("Error adding custom theme", { response }); - return false; - } - - if (response.body.data === null) { - showErrorNotification("Error adding custom theme: No data returned"); - return false; - } - - const newCustomTheme: CustomTheme = { - ...theme, - _id: response.body.data._id, - }; - - dbSnapshot.customThemes.push(newCustomTheme); - return true; -} - -export async function editCustomTheme( - themeId: string, - newTheme: Omit, -): Promise { - if (!isAuthenticated()) return false; - if (!dbSnapshot) return false; - - dbSnapshot.customThemes ??= []; - - const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId); - if (!customTheme) { - showErrorNotification( - "Editing failed: Custom theme with id: " + themeId + " does not exist", - ); - return false; - } - - const response = await Ape.users.editCustomTheme({ - body: { themeId, theme: newTheme }, - }); - if (response.status !== 200) { - showErrorNotification("Error editing custom theme", { response }); - return false; - } - - const newCustomTheme: CustomTheme = { - ...newTheme, - _id: themeId, - }; - - dbSnapshot.customThemes[dbSnapshot.customThemes.indexOf(customTheme)] = - newCustomTheme; - - return true; -} - -export async function deleteCustomTheme(themeId: string): Promise { - if (!isAuthenticated()) return false; - if (!dbSnapshot) return false; - - const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId); - if (!customTheme) return false; - - const response = await Ape.users.deleteCustomTheme({ body: { themeId } }); - if (response.status !== 200) { - showErrorNotification("Error deleting custom theme", { response }); - return false; - } - - dbSnapshot.customThemes = dbSnapshot.customThemes?.filter( - (t) => t._id !== themeId, - ); - - return true; -} - export async function getLocalPB( mode: M, mode2: Mode2, diff --git a/frontend/src/ts/elements/settings/theme-picker.ts b/frontend/src/ts/elements/settings/theme-picker.ts index b66f07d97494..ec96a4606a79 100644 --- a/frontend/src/ts/elements/settings/theme-picker.ts +++ b/frontend/src/ts/elements/settings/theme-picker.ts @@ -9,7 +9,7 @@ import { showSuccessNotification, } from "../../states/notifications"; import { showLoaderBar, hideLoaderBar } from "../../states/loader-bar"; -import * as DB from "../../db"; +import * as CustomThemes from "../../collections/custom-themes"; import { configEvent } from "../../events/config"; import { getActivePage, isAuthenticated } from "../../states/core"; import { ThemeName } from "@monkeytype/schemas/configs"; @@ -169,7 +169,7 @@ export async function fillCustomButtons(): Promise { saveButton?.setText("save as new"); addButton?.show(); - const customThemes = DB.getSnapshot()?.customThemes ?? []; + const customThemes = CustomThemes.__nonReactive.getCustomThemes(); if (customThemes.length === 0) { customThemesEl?.setStyle({ marginBottom: "0" }); @@ -284,9 +284,7 @@ qs(".pageSettings")?.onChild( if ((e.target as HTMLElement).classList.contains("delButton")) return; if ((e.target as HTMLElement).classList.contains("editButton")) return; const customThemeId = target.getAttribute("customThemeId") ?? ""; - const theme = DB.getSnapshot()?.customThemes?.find( - (e) => e._id === customThemeId, - ); + const theme = CustomThemes.__nonReactive.getCustomTheme(customThemeId); if (theme === undefined) { //this shouldnt happen but typescript needs this check @@ -408,7 +406,7 @@ qs(".pageSettings #saveCustomThemeButton")?.on("click", async () => { }; showLoaderBar(); - await DB.addCustomTheme(newCustomTheme); + await CustomThemes.addCustomTheme(newCustomTheme); hideLoaderBar(); } void fillCustomButtons(); diff --git a/frontend/src/ts/modals/simple-modals.ts b/frontend/src/ts/modals/simple-modals.ts index 2917a9fd3b8b..8b41e3e53a54 100644 --- a/frontend/src/ts/modals/simple-modals.ts +++ b/frontend/src/ts/modals/simple-modals.ts @@ -21,6 +21,7 @@ import { reloadAfter } from "../utils/misc"; import { isDevEnvironment } from "../utils/env"; import { createErrorMessage } from "../utils/error"; import * as ThemeController from "../controllers/theme-controller"; +import * as CustomThemes from "../collections/custom-themes"; import * as AccountSettings from "../pages/account-settings"; import { ExecReturn, @@ -1002,17 +1003,8 @@ list.updateCustomTheme = new SimpleModal({ ], buttonText: "update", execFn: async (_thisPopup, name, updateColors): Promise => { - const snapshot = DB.getSnapshot(); - if (!snapshot) { - return { - status: "error", - message: "Failed to update custom theme: no snapshot", - }; - } - - const customTheme = snapshot.customThemes?.find( - (t) => t._id === _thisPopup.parameters[0], - ); + const themeId = _thisPopup.parameters[0] as string; + const customTheme = CustomThemes.__nonReactive.getCustomTheme(themeId); if (customTheme === undefined) { return { status: "error", @@ -1025,17 +1017,11 @@ list.updateCustomTheme = new SimpleModal({ ? ThemeController.convertThemeToCustomColors(getTheme()) : customTheme.colors; - const newTheme = { + await CustomThemes.editCustomTheme({ + themeId: customTheme._id, name: normalizeName(name), colors: newColors, - }; - const validation = await DB.editCustomTheme(customTheme._id, newTheme); - if (!validation) { - return { - status: "error", - message: "Failed to update custom theme", - }; - } + }); setConfig("customThemeColors", newColors); void ThemePicker.fillCustomButtons(); @@ -1045,12 +1031,8 @@ list.updateCustomTheme = new SimpleModal({ }; }, beforeInitFn: (_thisPopup): void => { - const snapshot = DB.getSnapshot(); - if (!snapshot) return; - - const customTheme = snapshot.customThemes?.find( - (t) => t._id === _thisPopup.parameters[0], - ); + const themeId = _thisPopup.parameters[0] as string; + const customTheme = CustomThemes.__nonReactive.getCustomTheme(themeId); if (!customTheme) return; (_thisPopup.inputs[0] as TextInput).initVal = customTheme.name.replace( /_/g, @@ -1065,7 +1047,9 @@ list.deleteCustomTheme = new SimpleModal({ text: "Are you sure?", buttonText: "delete", execFn: async (_thisPopup): Promise => { - await DB.deleteCustomTheme(_thisPopup.parameters[0] as string); + await CustomThemes.deleteCustomTheme({ + themeId: _thisPopup.parameters[0] as string, + }); void ThemePicker.fillCustomButtons(); return { From cadc24e392eae6555e4e086837dfca11298251bf Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 12 May 2026 19:17:53 +0200 Subject: [PATCH 074/126] fixes --- frontend/src/ts/collections/custom-themes.ts | 20 ++++++++++---------- frontend/src/ts/db.ts | 3 --- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/frontend/src/ts/collections/custom-themes.ts b/frontend/src/ts/collections/custom-themes.ts index e05702f736f3..ebe2df607fd2 100644 --- a/frontend/src/ts/collections/custom-themes.ts +++ b/frontend/src/ts/collections/custom-themes.ts @@ -8,7 +8,8 @@ import { import Ape from "../ape"; import { queryClient } from "../queries"; import { baseKey } from "../queries/utils/keys"; -import { tempId } from "./utils/misc"; +import { applyIdWorkaround, tempId } from "./utils/misc"; +import { isAuthenticated } from "../states/core"; export type CustomThemeItem = CustomTheme; @@ -34,7 +35,14 @@ const customThemesCollection = createCollection( queryClient, getKey: (it) => it._id, queryFn: async () => { - return [] as CustomThemeItem[]; + if (!isAuthenticated()) return [] as CustomThemeItem[]; + const response = await Ape.users.getCustomThemes(); + + if (response.status !== 200) { + throw new Error("Error fetching presets:" + response.body.message); + } + + return response.body.data.map(applyIdWorkaround); }, }), ); @@ -136,14 +144,6 @@ function getCustomTheme(id: string): CustomThemeItem | undefined { return customThemesCollection.get(id); } -export function fillCustomThemesCollection(themes: CustomTheme[]): void { - customThemesCollection.utils.writeBatch(() => { - themes.forEach((item) => { - customThemesCollection.utils.writeInsert(item); - }); - }); -} - export async function addCustomTheme( params: ActionType["addCustomTheme"], ): Promise { diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 5cfe38a46520..c5b76bce0c46 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -41,7 +41,6 @@ import { __nonReactive } from "./collections/tags"; import { updateTagsInFilterStorage } from "./states/result-filters"; import { fetchUserFromApi } from "./ape/user"; import { SnapshotInitError } from "./utils/snapshot-init-error"; -import { fillCustomThemesCollection } from "./collections/custom-themes"; let dbSnapshot: Snapshot | undefined; const firstDayOfTheWeek = getFirstDayOfTheWeek(); @@ -169,8 +168,6 @@ export async function initSnapshot(): Promise { snap.lbMemory = userData.lbMemory; } - fillCustomThemesCollection(userData.customThemes ?? []); - updateTagsInFilterStorage(userData.tags?.map((it) => it._id) ?? []); snap.connections = convertConnections(connectionsData); From 247bcb9608a945b78a6dfaa85b647d6cdee35497 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 12 May 2026 19:22:57 +0200 Subject: [PATCH 075/126] implement save as new --- .../pages/settings/custom-setting/Theme.tsx | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) 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 1fda991b92eb..ebc7421ede2e 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx @@ -3,6 +3,7 @@ import { For, JSXElement, Show, untrack } from "solid-js"; import { debounce } from "throttle-debounce"; import { + addCustomTheme, editCustomTheme, useCustomThemesLiveQuery, } from "../../../../collections/custom-themes"; @@ -190,17 +191,30 @@ export function Theme(): JSXElement { }); }} /> - ); From a98b957ba7fbab8e315af1fcf499d35a426ebffd Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 12 May 2026 21:23:32 +0200 Subject: [PATCH 077/126] yeet --- frontend/src/ts/components/common/AnimatedModal.tsx | 3 --- frontend/src/ts/components/popups/alerts/AlertsPopup.tsx | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/ts/components/common/AnimatedModal.tsx b/frontend/src/ts/components/common/AnimatedModal.tsx index 61b4880f46f8..f52aeef725b4 100644 --- a/frontend/src/ts/components/common/AnimatedModal.tsx +++ b/frontend/src/ts/components/common/AnimatedModal.tsx @@ -50,9 +50,7 @@ type AnimatedModalProps = ParentProps<{ title?: string; modalClass?: string; - dialogClass?: string; wrapperClass?: string; - //todo check if wrapper and dialog can be merged into one }>; const DEFAULT_ANIMATION_DURATION = 125; @@ -321,7 +319,6 @@ export function AnimatedModal(props: AnimatedModalProps): JSXElement { ref={dialogRef} class={cn( "fixed top-0 left-0 z-1000 m-0 hidden h-screen max-h-screen w-screen max-w-screen border-none bg-[rgba(0,0,0,0.5)] p-8 backdrop:bg-transparent", - props.dialogClass, )} onKeyDown={handleKeyDown} onMouseDown={handleBackdropClick} diff --git a/frontend/src/ts/components/popups/alerts/AlertsPopup.tsx b/frontend/src/ts/components/popups/alerts/AlertsPopup.tsx index 1d405eb3bbf6..cac8561a3a44 100644 --- a/frontend/src/ts/components/popups/alerts/AlertsPopup.tsx +++ b/frontend/src/ts/components/popups/alerts/AlertsPopup.tsx @@ -12,7 +12,7 @@ export function AlertsPopup(): JSXElement { return ( Date: Tue, 12 May 2026 21:27:40 +0200 Subject: [PATCH 078/126] remove debugger --- .../src/ts/components/pages/settings/custom-setting/Theme.tsx | 1 - 1 file changed, 1 deletion(-) 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 e42378e6b1cf..3b407eac5f45 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx @@ -354,7 +354,6 @@ function CustomThemeButton(props: { theme: CustomTheme }): JSXElement { ], buttonText: "update", execFn: async (name, updateColors) => { - debugger; if (name === undefined) { return { status: "error", From f79a8445a10620b028960bc147449b10187c7793 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 12 May 2026 21:53:57 +0200 Subject: [PATCH 079/126] f --- .../ts/components/common/AnimatedModal.tsx | 65 ++++++++++--------- .../src/ts/components/modals/CookiesModal.tsx | 1 + .../components/popups/alerts/AlertsPopup.tsx | 1 - 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/frontend/src/ts/components/common/AnimatedModal.tsx b/frontend/src/ts/components/common/AnimatedModal.tsx index f52aeef725b4..3080ca59794e 100644 --- a/frontend/src/ts/components/common/AnimatedModal.tsx +++ b/frontend/src/ts/components/common/AnimatedModal.tsx @@ -1,11 +1,6 @@ -import { - JSXElement, - ParentProps, - Show, - createEffect, - onCleanup, -} from "solid-js"; +import { JSXElement, ParentProps, Show, onCleanup } from "solid-js"; +import { createEffectOn } from "../../hooks/effects"; import { useRefWithUtils } from "../../hooks/useRefWithUtils"; import { hideModal as storeHideModal, @@ -51,6 +46,7 @@ type AnimatedModalProps = ParentProps<{ title?: string; modalClass?: string; wrapperClass?: string; + deferShow?: boolean; }>; const DEFAULT_ANIMATION_DURATION = 125; @@ -64,15 +60,23 @@ export function AnimatedModal(props: AnimatedModalProps): JSXElement { const visibility = (): boolean => isModalOpen(props.id); // Handle open/close with animations - createEffect(() => { - const isChained = isModalChained(props.id); - - if (visibility()) { - void showModal(isChained); - } else { - void hideModal(isChained); - } - }); + createEffectOn( + visibility, + (visible) => { + const isChained = isModalChained(props.id); + console.log("aaa"); + + if (visible) { + void showModal(isChained); + } else { + void hideModal(isChained); + } + }, + { + // oxlint-disable-next-line solid/reactivity no need for reactivity + defer: props.deferShow === false ? false : true, + }, + ); const showModal = async (isChained: boolean): Promise => { if (dialogEl() === undefined || modalEl() === undefined) return; @@ -85,6 +89,7 @@ export function AnimatedModal(props: AnimatedModalProps): JSXElement { // Open the dialog dialogEl()?.show(); + dialogEl()?.setStyle({}); if (props.mode === "dialog") { dialogEl()?.native.show(); } else { @@ -319,29 +324,27 @@ export function AnimatedModal(props: AnimatedModalProps): JSXElement { ref={dialogRef} class={cn( "fixed top-0 left-0 z-1000 m-0 hidden h-screen max-h-screen w-screen max-w-screen border-none bg-[rgba(0,0,0,0.5)] p-8 backdrop:bg-transparent", + "flex h-full w-full items-center justify-center", + props.wrapperClass, )} + style={{ + display: "none", + }} onKeyDown={handleKeyDown} onMouseDown={handleBackdropClick} >
props.onScroll?.(e)} > -
props.onScroll?.(e)} - > - -
{props.title}
-
- {props.children} -
+ +
{props.title}
+
+ {props.children}
); diff --git a/frontend/src/ts/components/modals/CookiesModal.tsx b/frontend/src/ts/components/modals/CookiesModal.tsx index bc110c2f96e1..4d3ed2b42f8a 100644 --- a/frontend/src/ts/components/modals/CookiesModal.tsx +++ b/frontend/src/ts/components/modals/CookiesModal.tsx @@ -39,6 +39,7 @@ export function CookiesModal(): JSXElement { wrapperClass="justify-end items-end" closeOnEscape={false} closeOnWrapperClick={false} + deferShow={false} >

Date: Tue, 12 May 2026 21:55:14 +0200 Subject: [PATCH 080/126] something something mutating in place --- frontend/src/ts/config/setters.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/ts/config/setters.ts b/frontend/src/ts/config/setters.ts index 382012a3f322..de181296161f 100644 --- a/frontend/src/ts/config/setters.ts +++ b/frontend/src/ts/config/setters.ts @@ -185,8 +185,7 @@ export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean { if (newConfig.includes(funbox)) { newConfig = newConfig.filter((it) => it !== funbox); } else { - newConfig.push(funbox); - newConfig.sort(); + newConfig = [...newConfig, funbox].sort(); } if (!isConfigValueValid("funbox", newConfig, ConfigSchemas.FunboxSchema)) { From 259e71700afec0169529d1f5fdb753654a476bd0 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 12 May 2026 21:57:25 +0200 Subject: [PATCH 081/126] comments --- frontend/src/ts/collections/custom-themes.ts | 4 +++- .../ts/components/pages/settings/custom-setting/Theme.tsx | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/collections/custom-themes.ts b/frontend/src/ts/collections/custom-themes.ts index ebe2df607fd2..27117d003214 100644 --- a/frontend/src/ts/collections/custom-themes.ts +++ b/frontend/src/ts/collections/custom-themes.ts @@ -39,7 +39,9 @@ const customThemesCollection = createCollection( const response = await Ape.users.getCustomThemes(); if (response.status !== 200) { - throw new Error("Error fetching presets:" + response.body.message); + throw new Error( + "Error fetching custom themes:" + response.body.message, + ); } return response.body.data.map(applyIdWorkaround); 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 3b407eac5f45..a3d861fc5539 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Theme.tsx @@ -23,6 +23,7 @@ import { import { createEffectOn } from "../../../../hooks/effects"; import { isAuthenticated } from "../../../../states/core"; import { + showErrorNotification, showNoticeNotification, showSuccessNotification, } from "../../../../states/notifications"; @@ -108,7 +109,9 @@ export function Theme(): JSXElement { if (presetTheme) { setTheme({ ...presetTheme, name: "custom" }); } else { - showSuccessNotification("Current preset theme not found"); + showErrorNotification( + "Current preset theme not found. How is this possible?", + ); } }} /> From 87af58235ddd70552a4a0e046778fc28e1682ced Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 12 May 2026 22:00:27 +0200 Subject: [PATCH 082/126] remove --- frontend/src/html/popups.html | 76 -------------- frontend/src/styles/popups.scss | 76 -------------- frontend/src/ts/event-handlers/settings.ts | 7 -- frontend/src/ts/index.ts | 2 - frontend/src/ts/modals/cookies.ts | 112 --------------------- frontend/src/ts/test/test-screenshot.ts | 10 -- 6 files changed, 283 deletions(-) delete mode 100644 frontend/src/ts/modals/cookies.ts diff --git a/frontend/src/html/popups.html b/frontend/src/html/popups.html index 0461edfa0343..3e972ae15f28 100644 --- a/frontend/src/html/popups.html +++ b/frontend/src/html/popups.html @@ -49,82 +49,6 @@ - - - - diff --git a/frontend/src/index.html b/frontend/src/index.html index 016aa7b2e605..da677825157e 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -36,7 +36,11 @@ - + + + diff --git a/frontend/src/ts/commandline/lists/presets.ts b/frontend/src/ts/commandline/lists/presets.ts index 5a6f5c4b870b..a9bd320118c4 100644 --- a/frontend/src/ts/commandline/lists/presets.ts +++ b/frontend/src/ts/commandline/lists/presets.ts @@ -1,7 +1,5 @@ import * as ModesNotice from "../../elements/modes-notice"; -import * as Settings from "../../pages/settings"; import * as PresetController from "../../controllers/preset-controller"; -import * as EditPresetPopup from "../../modals/edit-preset"; import { isAuthenticated } from "../../states/core"; import { Command, CommandsSubgroup } from "../types"; import { __nonReactive } from "../../collections/presets"; @@ -36,22 +34,19 @@ function update(): void { id: "applyPreset" + preset._id, display: preset.name, exec: async (): Promise => { - Settings.setEventDisabled(true); await PresetController.apply(preset._id); - Settings.setEventDisabled(false); - void Settings.update(); void ModesNotice.update(); }, }); }); - subgroup.list.push({ - id: "createPreset", - display: "Create preset", - icon: "fa-plus", - exec: (): void => { - EditPresetPopup.show("add"); - }, - }); + // subgroup.list.push({ + // id: "createPreset", + // display: "Create preset", + // icon: "fa-plus", + // exec: (): void => { + // EditPresetPopup.show("add"); + // }, + // }); } export default commands; diff --git a/frontend/src/ts/commandline/lists/tags.ts b/frontend/src/ts/commandline/lists/tags.ts index 677e993fc10f..3f7351ba85c9 100644 --- a/frontend/src/ts/commandline/lists/tags.ts +++ b/frontend/src/ts/commandline/lists/tags.ts @@ -1,4 +1,3 @@ -import * as EditTagsPopup from "../../modals/edit-tag"; import * as ModesNotice from "../../elements/modes-notice"; import { clearActiveTags, @@ -76,16 +75,16 @@ function update(): void { }); } } - subgroup.list.push({ - id: "createTag", - display: "Create tag", - icon: "fa-plus", - shouldFocusTestUI: false, - opensModal: true, - exec: ({ commandlineModal }): void => { - EditTagsPopup.show("add", undefined, commandlineModal); - }, - }); + // subgroup.list.push({ + // id: "createTag", + // display: "Create tag", + // icon: "fa-plus", + // shouldFocusTestUI: false, + // opensModal: true, + // exec: ({ commandlineModal }): void => { + // EditTagsPopup.show("add", undefined, commandlineModal); + // }, + // }); } export default commands; diff --git a/frontend/src/ts/components/mount.tsx b/frontend/src/ts/components/mount.tsx index bbb2a5c11812..b0ce72d8e261 100644 --- a/frontend/src/ts/components/mount.tsx +++ b/frontend/src/ts/components/mount.tsx @@ -25,6 +25,7 @@ import { Popups } from "./popups/Popups"; const components: Record JSXElement> = { footer: () =>