From 923064be21e5df5c471c52d512b749e436ade5b7 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 1 Jun 2026 10:02:58 +0200 Subject: [PATCH 1/3] fix(settings): allow input of decimal values (@fehmer) --- .../src/ts/components/modals/SimpleModal.tsx | 42 +------------ .../pages/settings/SettingsPage.tsx | 5 +- .../src/ts/components/ui/form/InputField.tsx | 59 ++++++++++++++++++- 3 files changed, 63 insertions(+), 43 deletions(-) diff --git a/frontend/src/ts/components/modals/SimpleModal.tsx b/frontend/src/ts/components/modals/SimpleModal.tsx index 42303e5f0f53..eb8470f19d23 100644 --- a/frontend/src/ts/components/modals/SimpleModal.tsx +++ b/frontend/src/ts/components/modals/SimpleModal.tsx @@ -10,14 +10,7 @@ import { Switch, untrack, } from "solid-js"; -import { - z, - ZodDate, - ZodDefault, - ZodFirstPartyTypeKind, - ZodNumber, - ZodTypeAny, -} from "zod"; +import { z, ZodDefault, ZodFirstPartyTypeKind, ZodTypeAny } from "zod"; import { hideLoaderBar, showLoaderBar } from "../../states/loader-bar"; import { @@ -129,6 +122,7 @@ function FieldInput(props: { } > @@ -184,7 +177,6 @@ function FieldInput(props: { "w-full", props.input.class, )} - {...getMinAndMax(props.schema)} step={(props.input as { step?: number }).step} value={props.field().state.value as string} disabled={props.input.disabled} @@ -208,7 +200,6 @@ function FieldInput(props: { class={cn("w-full", props.input.class)} value={formatDate(props.field().state.value as Date)} disabled={props.input.disabled} - {...getDateMinAndMax(props.schema, formatDate)} onInput={(e) => { props.field().handleChange(e.currentTarget.value); props.input.oninput?.(e); @@ -443,35 +434,6 @@ export function convertFn( } } -function getMinAndMax(schema: ZodTypeAny): { - min?: number; - max?: number; -} { - if (getZodType(schema) !== ZodFirstPartyTypeKind.ZodNumber) return {}; - - return { - min: (schema as ZodNumber).minValue ?? undefined, - max: (schema as ZodNumber).maxValue ?? undefined, - }; -} -function getDateMinAndMax( - schema: ZodTypeAny, - format: (val: Date | undefined) => string | undefined, -): { - min?: string; - max?: string; -} { - if (getZodType(schema) !== ZodFirstPartyTypeKind.ZodDate) return {}; - - const applyFormat = (it: Date | null) => - it === null ? undefined : format(it); - - return { - min: applyFormat((schema as ZodDate).minDate), - max: applyFormat((schema as ZodDate).maxDate), - }; -} - function getZodType(schema: ZodTypeAny): ZodFirstPartyTypeKind { // oxlint-disable-next-line typescript/no-unsafe-assignment typescript/no-unsafe-member-access return schema._def["typeName"] as ZodFirstPartyTypeKind; diff --git a/frontend/src/ts/components/pages/settings/SettingsPage.tsx b/frontend/src/ts/components/pages/settings/SettingsPage.tsx index fe7b77e8c3f2..16da1cc15277 100644 --- a/frontend/src/ts/components/pages/settings/SettingsPage.tsx +++ b/frontend/src/ts/components/pages/settings/SettingsPage.tsx @@ -324,7 +324,7 @@ function AutoSetting(props: { [props.key]: getConfig[props.key], }, onSubmit: ({ value }) => { - const val = parseInt(String(value[props.key])); + const val = parseFloat(String(value[props.key])); if (val === getConfig[props.key]) return; savedIndicator.flash(); setConfig(props.key, val as Config[T]); @@ -350,7 +350,7 @@ function AutoSetting(props: { name={props.key} validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -368,6 +368,7 @@ function AutoSetting(props: {
+ date === undefined + ? undefined + : dateFormat( + date, + props.type === "date" ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm:ss", + ); + return (
props.field().handleChange(e.target.value)} + onInput={(e) => { + props.field().handleChange(e.target.value); + }} onKeyDown={(e) => { if (e.key === "Enter") { shakeItIfYouWantIt(); @@ -84,8 +98,11 @@ export function InputField(props: { onFocus={() => props.onFocus?.()} dir={props.dir} maxLength={props.maxLength} + {...getNumberOptions(props.schema)} + {...getDateOptions(props.schema, formatDate)} min={props.min} max={props.max} + step={props.step} /> @@ -93,3 +110,43 @@ export function InputField(props: {
); } + +function getNumberOptions(schema: ZodTypeAny | undefined): { + min?: number; + max?: number; + step?: string; +} { + if (schema === undefined) return {}; + if (getZodType(schema) !== ZodFirstPartyTypeKind.ZodNumber) return {}; + const numberSchema = schema as ZodNumber; + + return { + min: numberSchema.minValue ?? undefined, + max: numberSchema.maxValue ?? undefined, + step: numberSchema.isInt ? "1" : "any", + }; +} + +function getDateOptions( + schema: ZodTypeAny | undefined, + format: (val: Date | undefined) => string | undefined, +): { + min?: string; + max?: string; +} { + if (schema === undefined) return {}; + if (getZodType(schema) !== ZodFirstPartyTypeKind.ZodDate) return {}; + + const applyFormat = (it: Date | null) => + it === null ? undefined : format(it); + + return { + min: applyFormat((schema as ZodDate).minDate), + max: applyFormat((schema as ZodDate).maxDate), + }; +} + +function getZodType(schema: ZodTypeAny): ZodFirstPartyTypeKind { + // oxlint-disable-next-line typescript/no-unsafe-assignment typescript/no-unsafe-member-access + return schema._def["typeName"] as ZodFirstPartyTypeKind; +} From 1de38e89f2c9c3fefe0e41ec550f833812d38cb5 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 1 Jun 2026 10:22:26 +0200 Subject: [PATCH 2/3] custom settings --- .../pages/settings/custom-setting/AnimationFpsLimit.tsx | 5 +++-- .../pages/settings/custom-setting/MaxLineWidth.tsx | 5 +++-- .../ts/components/pages/settings/custom-setting/MinAcc.tsx | 5 +++-- .../ts/components/pages/settings/custom-setting/MinBurst.tsx | 5 +++-- .../ts/components/pages/settings/custom-setting/MinSpeed.tsx | 5 +++-- .../components/pages/settings/custom-setting/PaceCaret.tsx | 5 +++-- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx b/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx index b2425367441e..c7ad5b19eba3 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx @@ -16,7 +16,7 @@ export function AnimationFpsLimit(): JSXElement { fpsLimit: "", }, onSubmit: ({ value }) => { - const val = parseInt(String(value.fpsLimit)); + const val = parseFloat(String(value.fpsLimit)); if (val === getfpsLimit()) return; setfpsLimit(val); savedIndicator.flash(); @@ -54,7 +54,7 @@ export function AnimationFpsLimit(): JSXElement { name="fpsLimit" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -72,6 +72,7 @@ export function AnimationFpsLimit(): JSXElement { field={field} placeholder={"custom limit"} type="number" + schema={fpsLimitSchema} resetToDefaultIfEmptyOnBlur /> diff --git a/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx b/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx index f69258ea38cc..822523b1cb95 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/MaxLineWidth.tsx @@ -19,7 +19,7 @@ export function MaxLineWidth(): JSXElement { maxLineWidth: getConfig.maxLineWidth, }, onSubmit: ({ value }) => { - const val = parseInt(String(value.maxLineWidth)); + const val = parseFloat(String(value.maxLineWidth)); if (val === getConfig.maxLineWidth) return; flash(); setConfig("maxLineWidth", val); @@ -45,7 +45,7 @@ export function MaxLineWidth(): JSXElement { name="maxLineWidth" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -61,6 +61,7 @@ export function MaxLineWidth(): JSXElement {
{ - const val = parseInt(String(value.minAccCustom)); + const val = parseFloat(String(value.minAccCustom)); if (val === getConfig.minAccCustom) return; if (getConfig.minAcc === "custom") { // @@ -51,7 +51,7 @@ export function MinAcc(): JSXElement { name="minAccCustom" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -67,6 +67,7 @@ export function MinAcc(): JSXElement {
{ - const val = parseInt(String(value.minBurstCustomSpeed)); + const val = parseFloat(String(value.minBurstCustomSpeed)); if (val === getConfig.minBurstCustomSpeed) return; if (getConfig.minBurst !== "off") { // @@ -51,7 +51,7 @@ export function MinBurst(): JSXElement { name="minBurstCustomSpeed" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -67,6 +67,7 @@ export function MinBurst(): JSXElement {
{ - const val = parseInt(String(value.minWpmCustomSpeed)); + const val = parseFloat(String(value.minWpmCustomSpeed)); if (val === getConfig.minWpmCustomSpeed) return; if (getConfig.minWpm === "custom") { // @@ -51,7 +51,7 @@ export function MinSpeed(): JSXElement { name="minWpmCustomSpeed" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -65,6 +65,7 @@ export function MinSpeed(): JSXElement {
{ - const val = parseInt(String(value.paceCaretCustomSpeed)); + const val = parseFloat(String(value.paceCaretCustomSpeed)); if (val === getConfig.paceCaretCustomSpeed) return; if (getConfig.paceCaret !== "off") { // @@ -54,7 +54,7 @@ export function PaceCaret(): JSXElement { name="paceCaretCustomSpeed" validators={{ onChange: ({ value }) => { - const val = parseInt(String(value)); + const val = parseFloat(String(value)); if (isNaN(val)) { return "Must be a number"; } @@ -70,6 +70,7 @@ export function PaceCaret(): JSXElement {
Date: Mon, 1 Jun 2026 11:06:59 +0200 Subject: [PATCH 3/3] fix special inputs on SimpleModal --- .../src/ts/components/modals/SimpleModal.tsx | 66 +++++-------------- .../src/ts/components/ui/form/InputField.tsx | 5 +- .../ts/components/ui/form/TextareaField.tsx | 2 + 3 files changed, 20 insertions(+), 53 deletions(-) diff --git a/frontend/src/ts/components/modals/SimpleModal.tsx b/frontend/src/ts/components/modals/SimpleModal.tsx index eb8470f19d23..6aac8bea6270 100644 --- a/frontend/src/ts/components/modals/SimpleModal.tsx +++ b/frontend/src/ts/components/modals/SimpleModal.tsx @@ -1,5 +1,4 @@ import { AnyFieldApi, createForm } from "@tanstack/solid-form"; -import { format as dateFormat } from "date-fns/format"; import { Accessor, For, @@ -31,6 +30,7 @@ import { AnimatedModal } from "../common/AnimatedModal"; import { Checkbox } from "../ui/form/Checkbox"; import { InputField } from "../ui/form/InputField"; import { SubmitButton } from "../ui/form/SubmitButton"; +import { TextareaField } from "../ui/form/TextareaField"; import { fieldMandatory, fromSchema, handleResult } from "../ui/form/utils"; type SyncValidator = (opts: { @@ -109,13 +109,6 @@ function FieldInput(props: { input: GenericSimpleModalInput; schema: z.ZodTypeAny; }): JSXElement { - const formatDate = (date: Date | undefined) => - date === undefined - ? undefined - : dateFormat( - date, - props.input.type === "date" ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm:ss", - ); return ( } @@ -149,64 +149,30 @@ function FieldInput(props: { /> - + />
- { - props.field().handleChange(e.currentTarget.value); - props.input.oninput?.(e); - }} - onBlur={() => props.field().handleBlur()} /> {props.field().state.value as string}
- - - { - props.field().handleChange(e.currentTarget.value); - props.input.oninput?.(e); - }} - onBlur={() => props.field().handleBlur()} - /> -
); } diff --git a/frontend/src/ts/components/ui/form/InputField.tsx b/frontend/src/ts/components/ui/form/InputField.tsx index d6eb4e0a7f58..cd4a9c7f7e4d 100644 --- a/frontend/src/ts/components/ui/form/InputField.tsx +++ b/frontend/src/ts/components/ui/form/InputField.tsx @@ -25,7 +25,7 @@ export function InputField(props: { schema?: ZodTypeAny; min?: number; max?: number; - step?: string; + step?: string | number; }): JSXElement { const [shake, setShake] = createSignal(false); @@ -65,7 +65,6 @@ export function InputField(props: { )} type={props.type ?? "text"} placeholder={props.placeholder ?? ""} - // oxlint-disable-next-line react/no-unknown-property autocomplete={props.autocomplete} name={props.field().name as string} value={props.field().state.value as string} @@ -102,7 +101,7 @@ export function InputField(props: { {...getDateOptions(props.schema, formatDate)} min={props.min} max={props.max} - step={props.step} + step={props.step?.toString()} /> diff --git a/frontend/src/ts/components/ui/form/TextareaField.tsx b/frontend/src/ts/components/ui/form/TextareaField.tsx index 394069d40645..555027c597ce 100644 --- a/frontend/src/ts/components/ui/form/TextareaField.tsx +++ b/frontend/src/ts/components/ui/form/TextareaField.tsx @@ -8,6 +8,7 @@ export function TextareaField(props: { field: Accessor; ref?: HTMLTextAreaElement | ((el: HTMLTextAreaElement) => void); placeholder?: string; + autocomplete?: string; disabled?: boolean; class?: string; maxLength?: number; @@ -28,6 +29,7 @@ export function TextareaField(props: { id={props.field().name as string} name={props.field().name as string} placeholder={props.placeholder ?? ""} + autocomplete={props.autocomplete} value={props.field().state.value as string} onBlur={() => props.field().handleBlur()} onInput={(e) => props.field().handleChange(e.currentTarget.value)}