From 3462738b897c999efabeec03cfc23be269ad488e Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 1 Jun 2026 23:01:28 +0200 Subject: [PATCH 1/4] refactor: test events phase 1.5 (@miodec) (#8042) Rebase all the small changes i made in phase 2 to master so that they can be compared to live --- frontend/__tests__/test/events/data.spec.ts | 32 --- .../__tests__/test/events/helpers.spec.ts | 269 ++++++++++++++++-- .../src/ts/input/handlers/before-delete.ts | 6 + frontend/src/ts/input/handlers/delete.ts | 57 +++- frontend/src/ts/input/handlers/insert-text.ts | 29 +- .../src/ts/input/helpers/word-navigation.ts | 4 +- frontend/src/ts/test/events/data.ts | 41 ++- frontend/src/ts/test/events/helpers.ts | 97 ++++++- frontend/src/ts/test/events/stats.ts | 12 +- frontend/src/ts/test/events/types.ts | 16 +- .../src/ts/test/funbox/funbox-functions.ts | 2 +- frontend/src/ts/test/test-input.ts | 10 - frontend/src/ts/test/test-logic.ts | 10 +- frontend/src/ts/test/test-state.ts | 5 + frontend/src/ts/test/test-stats.ts | 6 +- frontend/src/ts/test/test-timer.ts | 8 +- frontend/src/ts/test/test-ui.ts | 6 +- frontend/src/ts/test/timer-progress.ts | 5 +- 18 files changed, 470 insertions(+), 145 deletions(-) diff --git a/frontend/__tests__/test/events/data.spec.ts b/frontend/__tests__/test/events/data.spec.ts index 14cbaaaccd79..e9b9e075103f 100644 --- a/frontend/__tests__/test/events/data.spec.ts +++ b/frontend/__tests__/test/events/data.spec.ts @@ -100,12 +100,6 @@ describe("data.ts", () => { expect(inputs).toHaveLength(1); }); - it("computes testMs relative to start", () => { - logTestEvent("timer", 1500, timerData("start", 0)); - const events = getAllTestEvents(); - expect(events[0]!.testMs).toBe(500); // 1500 - 1000 - }); - it("caches getAllTestEvents and invalidates on new event", () => { logTestEvent("timer", 1100, timerData("start", 0)); const first = getAllTestEvents(); @@ -275,32 +269,6 @@ describe("data.ts", () => { expect(perWord.get(1)).toHaveLength(1); }); - it("attributes deleteContentBackward at charIndex 0 to previous word", () => { - logTestEvent("input", 1010, inputData({ wordIndex: 0, charIndex: 0 })); - logTestEvent("input", 1020, { - charIndex: 0, - wordIndex: 1, - inputType: "deleteContentBackward", - } as InputEventData); - - const perWord = getInputEventsPerWord(); - expect(perWord.get(0)).toHaveLength(2); - expect(perWord.has(1)).toBe(false); - }); - - it("attributes deleteWordBackward at charIndex 0 to previous word", () => { - logTestEvent("input", 1010, inputData({ wordIndex: 0, charIndex: 0 })); - logTestEvent("input", 1020, { - charIndex: 0, - wordIndex: 1, - inputType: "deleteWordBackward", - } as InputEventData); - - const perWord = getInputEventsPerWord(); - expect(perWord.get(0)).toHaveLength(2); - expect(perWord.has(1)).toBe(false); - }); - it("does not shift delete at charIndex 0 if wordIndex is 0", () => { logTestEvent("input", 1010, { charIndex: 0, diff --git a/frontend/__tests__/test/events/helpers.spec.ts b/frontend/__tests__/test/events/helpers.spec.ts index c79ec075c45d..6cd541f717b9 100644 --- a/frontend/__tests__/test/events/helpers.spec.ts +++ b/frontend/__tests__/test/events/helpers.spec.ts @@ -6,7 +6,9 @@ vi.mock("../../../src/ts/config/store", () => ({ })); import { - getSimulatedInput, + findInputValueMismatches, + getInputFromDom, + getInputFromEvents, getTestEventCode, } from "../../../src/ts/test/events/helpers"; import type { InputEvent } from "../../../src/ts/test/events/types"; @@ -91,65 +93,67 @@ function reset(): void { wordIndex = 0; } -describe("getSimulatedInput", () => { +describe("getInputFromEvents", () => { beforeEach(() => { reset(); }); it("builds string from insertText events", () => { - expect(getSimulatedInput([...insert("hello")])).toBe("hello"); + expect(getInputFromEvents([...insert("hello")])).toBe("hello"); }); it("builds string from insertText events with trailing space", () => { - expect(getSimulatedInput([...insert("hello ")])).toBe("hello "); + expect(getInputFromEvents([...insert("hello ")])).toBe("hello "); }); it("handles deleteContentBackward", () => { - expect(getSimulatedInput([...insert("abc"), ...deleteBackward()])).toBe( + expect(getInputFromEvents([...insert("abc"), ...deleteBackward()])).toBe( "ab", ); }); it("handles deleteContentBackward after space", () => { - expect(getSimulatedInput([...insert("abc "), ...deleteBackward()])).toBe( + expect(getInputFromEvents([...insert("abc "), ...deleteBackward()])).toBe( "abc", ); }); it("handles multiple deletes", () => { - expect(getSimulatedInput([...insert("ab"), ...deleteBackward(2)])).toBe(""); + expect(getInputFromEvents([...insert("ab"), ...deleteBackward(2)])).toBe( + "", + ); }); it("handles multiple deletes after space", () => { - expect(getSimulatedInput([...insert("ab "), ...deleteBackward(2)])).toBe( + expect(getInputFromEvents([...insert("ab "), ...deleteBackward(2)])).toBe( "a", ); }); it("handles deleteWordBackward", () => { - expect(getSimulatedInput([...insert("hello"), deleteWordBackward()])).toBe( + expect(getInputFromEvents([...insert("hello"), deleteWordBackward()])).toBe( "", ); }); it("handles deleteWordBackward after space", () => { - expect(getSimulatedInput([...insert("hello "), deleteWordBackward()])).toBe( - "", - ); + expect( + getInputFromEvents([...insert("hello "), deleteWordBackward()]), + ).toBe(""); }); it("returns empty string for no events", () => { - expect(getSimulatedInput([])).toBe(""); + expect(getInputFromEvents([])).toBe(""); }); it("handles deleteContentBackward on empty string", () => { const events = [...deleteBackward()]; - expect(getSimulatedInput(events)).toBe(""); + expect(getInputFromEvents(events)).toBe(""); }); it("skips inputStopped events", () => { expect( - getSimulatedInput([ + getInputFromEvents([ ...insert("he"), ...insert("x", "insertText", { inputStopped: true }), ...insert("llo"), @@ -157,21 +161,248 @@ describe("getSimulatedInput", () => { ).toBe("hello"); }); + it("handles deleteContentBackward within the same word correctly", () => { + expect(getInputFromEvents([...insert("a a"), deleteWordBackward()])).toBe( + "a ", + ); + }); + + it("handles deleteWordBackward with multiple internal spaces", () => { + expect( + getInputFromEvents([...insert("foo bar baz"), deleteWordBackward()]), + ).toBe("foo bar "); + }); + + it("handles deleteWordBackward with trailing space after multiple words", () => { + expect( + getInputFromEvents([...insert("foo bar "), deleteWordBackward()]), + ).toBe("foo "); + }); + + it("handles consecutive deleteWordBackward events", () => { + expect( + getInputFromEvents([ + ...insert("foo bar baz"), + deleteWordBackward(), + deleteWordBackward(), + ]), + ).toBe("foo "); + }); + + it("handles deleteWordBackward on empty string", () => { + expect(getInputFromEvents([deleteWordBackward()])).toBe(""); + }); + + it("handles deleteWordBackward on only whitespace", () => { + expect(getInputFromEvents([...insert(" "), deleteWordBackward()])).toBe( + "", + ); + }); + + it("ignores recorded inputValue (pure op-based simulation)", () => { + const events: InputEvent[] = [ + ...insert("hello"), + { + type: "input", + ms: 100, + testMs: 100, + data: { + inputType: "deleteWordBackward", + charIndex: 5, + wordIndex: 0, + inputValue: "RECORDED_BUT_IGNORED", + }, + }, + ]; + // pure simulation: deleteWordBackward on "hello" → "" + expect(getInputFromEvents(events)).toBe(""); + }); +}); + +describe("getInputFromDom", () => { + beforeEach(() => { + reset(); + }); + + it("falls through to op-based logic when inputValue is absent", () => { + expect(getInputFromDom([...insert("hello")])).toBe("hello"); + }); + + it("uses recorded inputValue when present, overriding op-based logic", () => { + const events: InputEvent[] = [ + ...insert("hello"), + { + type: "input", + ms: 100, + testMs: 100, + data: { + inputType: "deleteWordBackward", + charIndex: 5, + wordIndex: 0, + inputValue: "he", + }, + }, + ]; + // op-based would yield "", but inputValue is truth + expect(getInputFromDom(events)).toBe("he"); + }); + + it("uses latest event's inputValue across multiple recorded events", () => { + const events: InputEvent[] = [ + ...insert("hello"), + { + type: "input", + ms: 100, + testMs: 100, + data: { + inputType: "deleteContentBackward", + charIndex: 5, + wordIndex: 0, + inputValue: "hi", + }, + }, + ]; + expect(getInputFromDom(events)).toBe("hi"); + }); + + it("mixes captured and op-based across events", () => { + const events: InputEvent[] = [ + ...insert("ab"), // no inputValue, op = "ab" + { + type: "input", + ms: 100, + testMs: 100, + data: { + inputType: "insertText", + data: "c", + charIndex: 2, + wordIndex: 0, + correct: true, + isCompositionEnding: false, + inputStopped: false, + inputValue: "abc", + }, + }, + // next event has no inputValue, falls through to op (append "d") + { + type: "input", + ms: 110, + testMs: 110, + data: { + inputType: "insertText", + data: "d", + charIndex: 3, + wordIndex: 0, + correct: true, + isCompositionEnding: false, + inputStopped: false, + }, + }, + ]; + expect(getInputFromDom(events)).toBe("abcd"); + }); +}); + +describe("findInputValueMismatches", () => { + beforeEach(() => { + reset(); + }); + + it("returns empty when no events have recorded inputValue", () => { + expect(findInputValueMismatches([...insert("hello")])).toEqual([]); + }); + + it("returns empty when recorded values match derivation", () => { + const events: InputEvent[] = [ + { + type: "input", + ms: 10, + testMs: 10, + data: { + inputType: "insertText", + data: "a", + charIndex: 0, + wordIndex: 0, + correct: true, + isCompositionEnding: false, + inputStopped: false, + inputValue: "a", + }, + }, + { + type: "input", + ms: 20, + testMs: 20, + data: { + inputType: "insertText", + data: "b", + charIndex: 1, + wordIndex: 0, + correct: true, + isCompositionEnding: false, + inputStopped: false, + inputValue: "ab", + }, + }, + ]; + expect(findInputValueMismatches(events)).toEqual([]); + }); + + it("returns mismatches when recorded value differs from derivation", () => { + const events: InputEvent[] = [ + { + type: "input", + ms: 10, + testMs: 10, + data: { + inputType: "insertText", + data: "a", + charIndex: 0, + wordIndex: 0, + correct: true, + isCompositionEnding: false, + inputStopped: false, + inputValue: "DIFFERENT", + }, + }, + ]; + expect(findInputValueMismatches(events)).toEqual([ + { index: 0, derived: "a", recorded: "DIFFERENT" }, + ]); + }); + + it("skips events without inputValue, still tracks ones with it", () => { + const events: InputEvent[] = [ + ...insert("hello"), // no inputValue on these + { + type: "input", + ms: 100, + testMs: 100, + data: { + inputType: "deleteContentBackward", + charIndex: 5, + wordIndex: 0, + inputValue: "hell", + }, + }, + ]; + // derivation: "hello" then slice = "hell". Recorded = "hell". Match. + expect(findInputValueMismatches(events)).toEqual([]); + }); + // it("handles insertCompositionText events", () => { // const events = [ // ...insert("k", "insertCompositionText"), // ...insert("ka", "insertCompositionText"), // ]; - // expect(getSimulatedInput(events)).toBe("ka"); + // expect(getInputFromEvents(events)).toBe("ka"); // }); // it("handles composition followed by regular text", () => { - // const events = [ - // ...insert("k", "insertCompositionText"), // ...insert("ka", "insertCompositionText"), // ...insert("b"), // ]; - // expect(getSimulatedInput(events)).toBe("kab"); + // expect(getInputFromEvents(events)).toBe("kab"); // }); }); diff --git a/frontend/src/ts/input/handlers/before-delete.ts b/frontend/src/ts/input/handlers/before-delete.ts index 3dc7c77a1c73..85ddc1c7e55a 100644 --- a/frontend/src/ts/input/handlers/before-delete.ts +++ b/frontend/src/ts/input/handlers/before-delete.ts @@ -28,6 +28,12 @@ export function onBeforeDelete(event: InputEvent): void { const inputIsEmpty = inputValue === ""; if (inputIsEmpty) { + // we are on the first word, just prevent default, nothing to go back to + if (TestState.activeWordIndex === 0) { + event.preventDefault(); + return; + } + // this is nested because we only wanna pull the element from the dom if needed const previousWordElement = TestUI.getWordElement( TestState.activeWordIndex - 1, diff --git a/frontend/src/ts/input/handlers/delete.ts b/frontend/src/ts/input/handlers/delete.ts index 8d2a44340a47..fa244fbe5454 100644 --- a/frontend/src/ts/input/handlers/delete.ts +++ b/frontend/src/ts/input/handlers/delete.ts @@ -18,6 +18,8 @@ export function onDelete(inputType: DeleteInputType, now: number): void { TestInput.input.syncWithInputElement(); + const inputAfterDelete = TestInput.input.current; + Replay.addReplayEvent("setLetterIndex", TestInput.input.current.length); TestInput.setCurrentNotAfk(); @@ -33,25 +35,52 @@ export function onDelete(inputType: DeleteInputType, now: number): void { inputBeforeDelete.length > 0 && beforeDeleteOnlyTabs && allTabsCorrect - // (TestInput.input.getHistory(TestState.activeWordIndex - 1) !== - // TestWords.words.get(TestState.activeWordIndex - 1) || - // Config.freedomMode) ) { + // Clear N+1's tabs (the word the user was in) + logTestEvent("input", now, { + inputType: "deleteWordBackward", + wordIndex: activeWordIndexBeforeDelete, + charIndex: inputBeforeDelete.length, + inputValue: "", + }); + setInputElementValue(""); - TestInput.input.syncWithInputElement(); goToPreviousWord(inputType, true); - } else { - //normal backspace - if (realInputValue === "") { - goToPreviousWord(inputType); - } + + // Record the resulting state of the previous word (newline removed) + const postNavInputValue = getInputElementValue().inputValue; + logTestEvent("input", now, { + inputType: "deleteContentBackward", + wordIndex: activeWordIndex, + charIndex: postNavInputValue.length, + inputValue: postNavInputValue, + }); + + TestUI.afterTestDelete(); + return; } - logTestEvent("input", now, { - inputType: inputType, - wordIndex: activeWordIndexBeforeDelete, - charIndex: inputBeforeDelete.length, - }); + //normal backspace + if (realInputValue === "") { + goToPreviousWord(inputType); + + // Record the resulting state of the destination word + const postNavInputValue = getInputElementValue().inputValue; + logTestEvent("input", now, { + inputType: inputType, + wordIndex: activeWordIndex, + charIndex: postNavInputValue.length, + inputValue: postNavInputValue, + }); + } else { + // Delete within current word + logTestEvent("input", now, { + inputType: inputType, + wordIndex: activeWordIndexBeforeDelete, + charIndex: inputBeforeDelete.length, + inputValue: inputAfterDelete, + }); + } TestUI.afterTestDelete(); } diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index 6145641c5a49..af461149f4ca 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -213,6 +213,24 @@ export async function onInsertText(options: OnInsertTextParams): Promise { TestInput.input.syncWithInputElement(); } + // capture DOM before goToNextWord clears it for the new word + const inputValueAfterEvent = TestInput.input.current; + + // Log the event BEFORE goToNextWord so readers inside the navigation + // (e.g. beforeTestWordChange's updateWordLetters, getWordBurst) see the + // completed event in derivation. Otherwise the just-typed trigger char + // (space/newline) is missing — visible as missing \n element in zen mode. + logTestEvent("input", now, { + inputType: "insertText", + data, + correct, + wordIndex, + charIndex: testInput.length, + isCompositionEnding: isCompositionEnding === true, + inputStopped: removeLastChar, + inputValue: inputValueAfterEvent + (charIsSpace ? " " : ""), + }); + // going to next word let increasedWordIndex: null | boolean = null; let lastBurst: null | number = null; @@ -221,21 +239,12 @@ export async function onInsertText(options: OnInsertTextParams): Promise { correctInsert: correct, isCompositionEnding: isCompositionEnding === true, zenNewline: charIsNewline && Config.mode === "zen", + now, }); lastBurst = result.lastBurst; increasedWordIndex = result.increasedWordIndex; } - logTestEvent("input", now, { - inputType: "insertText", - data, - correct, - wordIndex, - charIndex: testInput.length, - isCompositionEnding: isCompositionEnding === true, - inputStopped: removeLastChar, - }); - /* Probably a good place to explain what the heck is going on with all these space related variables: - spaceOrNewLine: did the user input a space or a new line? diff --git a/frontend/src/ts/input/helpers/word-navigation.ts b/frontend/src/ts/input/helpers/word-navigation.ts index 897d115e65c0..27ec9c8fec62 100644 --- a/frontend/src/ts/input/helpers/word-navigation.ts +++ b/frontend/src/ts/input/helpers/word-navigation.ts @@ -22,6 +22,7 @@ type GoToNextWordParams = { // this is used to tell test ui to update the word before moving to the next word (in case of a composition that ends with a space) isCompositionEnding: boolean; zenNewline?: boolean; + now: number; }; type GoToNextWordReturn = { @@ -33,6 +34,7 @@ export async function goToNextWord({ correctInsert, isCompositionEnding, zenNewline, + now, }: GoToNextWordParams): Promise { const ret = { increasedWordIndex: false, @@ -56,7 +58,7 @@ export async function goToNextWord({ } //burst calculation and fail - const burst: number = TestStats.calculateBurst(); + const burst: number = TestStats.calculateBurst(now); TestInput.pushBurstToHistory(burst); ret.lastBurst = burst; diff --git a/frontend/src/ts/test/events/data.ts b/frontend/src/ts/test/events/data.ts index ebda2b4c76ba..67a62c88e6e9 100644 --- a/frontend/src/ts/test/events/data.ts +++ b/frontend/src/ts/test/events/data.ts @@ -14,7 +14,6 @@ import { TimerEventData, } from "./types"; import { keysToTrack } from "./helpers"; -import { start } from "../test-stats"; import { Keycode } from "../../constants/keys"; import { roundTo2 } from "@monkeytype/util/numbers"; import { resultCalculating } from "../test-state"; @@ -222,6 +221,19 @@ export function cleanupData(): void { export function getAllTestEvents(): TestEvent[] { if (cachedAllEvents !== undefined) return cachedAllEvents; + const firstEventMs = Math.min( + ...[ + keydownEvents[0]?.ms, + keyupEvents[0]?.ms, + timerEvents[0]?.ms, + inputEvents[0]?.ms, + compositionEvents[0]?.ms, + ].filter((ms): ms is number => ms !== undefined), + ); + + const startEventMs = + timerEvents.find((e) => e.data.event === "start")?.ms ?? firstEventMs ?? 0; + // cachedAllEvents = testData300; // return cachedAllEvents; cachedAllEvents = [ @@ -237,7 +249,7 @@ export function getAllTestEvents(): TestEvent[] { (a.type === "timer" ? 1 : 0) - (b.type === "timer" ? 1 : 0), ) .map((event) => { - event.testMs = roundTo2(event.ms - start); + event.testMs = roundTo2(event.ms - startEventMs); return event; }); @@ -308,6 +320,18 @@ export function getPressedKeys(): Map< return pressedKeys; } +export function getInputEventsForWord(wordIndex: number): InputEvent[] { + const events = getAllTestEvents(); + const result: InputEvent[] = []; + for (const event of events) { + if (event.type !== "input") continue; + if (event.data.wordIndex === wordIndex) { + result.push(event); + } + } + return result; +} + export function getInputEventsPerWord( startMs?: number, testMsLimit?: number, @@ -327,18 +351,7 @@ export function getInputEventsPerWord( break; } - let wordIndex = event.data.wordIndex; - - //special case for delete events on the 0th index - // because they affect the previous word - so we need to attribute them to the previous word - if ( - (event.data.inputType === "deleteWordBackward" || - event.data.inputType === "deleteContentBackward") && - event.data.charIndex === 0 && - wordIndex > 0 - ) { - wordIndex -= 1; - } + const wordIndex = event.data.wordIndex; const existing = eventsPerWordIndex.get(wordIndex) ?? []; existing.push(event); diff --git a/frontend/src/ts/test/events/helpers.ts b/frontend/src/ts/test/events/helpers.ts index 415b4916520b..b5d7186f05e4 100644 --- a/frontend/src/ts/test/events/helpers.ts +++ b/frontend/src/ts/test/events/helpers.ts @@ -93,25 +93,92 @@ export function getTestEventCode(event: KeyboardEvent): Keycode | "NoCode" { return event.code as Keycode; } -export function getSimulatedInput(events: InputEvent[]): string { - let simulatedInput = ""; +export function applyOp(input: string, event: InputEvent): string { + if (event.data.inputType === "insertText") { + if (event.data.inputStopped) return input; + return input + event.data.data; + } + if (event.data.inputType === "insertCompositionText") { + if (event.data.inputStopped) return input; + return input + event.data.data; + } + if (event.data.inputType === "deleteContentBackward") { + return input.slice(0, -1); + } + if (event.data.inputType === "deleteWordBackward") { + return input.replace(/(?:\S+\s*|\s+)$/, ""); + } + return input; +} +/** + * Derives input by applying each event's operation in order. Ignores the + * recorded inputValue field. Use for verification, tests, or fallback — + * not as source of truth. + */ +export function getInputFromEvents(events: InputEvent[]): string { + let input = ""; for (const event of events) { - if (event.data.inputType === "insertText") { - if (event.data.inputStopped) continue; - simulatedInput += event.data.data; - } - if (event.data.inputType === "insertCompositionText") { - if (event.data.inputStopped) continue; - simulatedInput += event.data.data; - } - if (event.data.inputType === "deleteContentBackward") { - simulatedInput = simulatedInput.slice(0, -1); + input = applyOp(input, event); + } + return input; +} + +/** + * Reads input from the DOM snapshots captured on each event (inputValue), + * falling back to op-based derivation for events without a snapshot. + * Use this whenever you need the actual current/past input state. + * + * Walks backward to find the latest event with a captured inputValue, then + * replays any subsequent events forward — O(1) when the last event has a + * snapshot (the common case), O(n) worst case. + */ +export function getInputFromDom(events: InputEvent[]): string { + for (let i = events.length - 1; i >= 0; i--) { + const event = events[i] as InputEvent; + if (event.data.inputValue !== undefined) { + let input = event.data.inputValue; + for (let j = i + 1; j < events.length; j++) { + input = applyOp(input, events[j] as InputEvent); + } + return input; } - if (event.data.inputType === "deleteWordBackward") { - simulatedInput = ""; + } + return getInputFromEvents(events); +} + +export type InputValueMismatch = { + index: number; + derived: string; + recorded: string; +}; + +/** + * Compares event-derived input against the recorded DOM snapshot at each + * event. Returns the indices where event-derivation disagreed with what the + * DOM captured. Useful for catching op-logic bugs or capture-timing bugs. + */ +export function findInputValueMismatches( + events: InputEvent[], +): InputValueMismatch[] { + const mismatches: InputValueMismatch[] = []; + let derived = ""; + + for (let i = 0; i < events.length; i++) { + const event = events[i] as InputEvent; + derived = applyOp(derived, event); + + if ( + event.data.inputValue !== undefined && + event.data.inputValue !== derived + ) { + mismatches.push({ + index: i, + derived, + recorded: event.data.inputValue, + }); } } - return simulatedInput; + return mismatches; } diff --git a/frontend/src/ts/test/events/stats.ts b/frontend/src/ts/test/events/stats.ts index 3f8ae474ba7b..a56a34760352 100644 --- a/frontend/src/ts/test/events/stats.ts +++ b/frontend/src/ts/test/events/stats.ts @@ -1,6 +1,7 @@ import { getAllTestEvents, getInputEvents, + getInputEventsForWord, getInputEventsPerWord, getPressedKeys, logTestEvent, @@ -8,7 +9,7 @@ import { import * as TestWords from "../../test/test-words"; import { CharCounts, countChars, getLastChar } from "../../utils/strings"; import * as CustomText from "../../test/custom-text"; -import { getSimulatedInput } from "./helpers"; +import { getInputFromDom } from "./helpers"; import { activeWordIndex, bailedOut } from "../test-state"; import { calculateWpm } from "../../utils/numbers"; import { mean, roundTo2 } from "@monkeytype/util/numbers"; @@ -259,7 +260,7 @@ export function getChars(): CharCounts { for (const [wordIndex, events] of eventsPerWordIndex.entries()) { const lastWord = wordIndex === activeWordIndex; - let simulatedInput = getSimulatedInput(events); + let simulatedInput = getInputFromDom(events); if (lastWord) { //remove trailing space for last word @@ -295,6 +296,11 @@ export function getChars(): CharCounts { }; } +export function getInputForWord(wordIndex: number): string { + const events = getInputEventsForWord(wordIndex); + return getInputFromDom(events).trimEnd(); +} + export function getAccuracy(): { correct: number; incorrect: number; @@ -400,7 +406,7 @@ export function getWpmHistory(): number[] { >(); let maxWordIndex = 0; for (const [k, wordEvents] of eventsPerWord) { - const input = getSimulatedInput(wordEvents); + const input = getInputFromDom(wordEvents); wordInputs.set(k, { input, events: wordEvents }); // Only count words with non-empty input for maxWordIndex, // so that fully-deleted words don't prevent earlier words diff --git a/frontend/src/ts/test/events/types.ts b/frontend/src/ts/test/events/types.ts index 94712b8dac63..1d7b64370f94 100644 --- a/frontend/src/ts/test/events/types.ts +++ b/frontend/src/ts/test/events/types.ts @@ -69,21 +69,23 @@ export type TimerEventData = export type InputEvent = EventProps<"input", InputEventData>; -export type InputEventData = { +type BaseInputEventData = { charIndex: number; wordIndex: number; -} & ( - | { + inputValue?: string; +}; + +export type InputEventData = + | (BaseInputEventData & { inputType: InsertInputType; data: string; correct: boolean; isCompositionEnding: boolean; inputStopped: boolean; - } - | { + }) + | (BaseInputEventData & { inputType: DeleteInputType; - } -); + }); export type CompositionTestEvent = EventProps< "composition", diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index 78377be3a229..484fc3bb2053 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -425,7 +425,7 @@ const list: Partial> = { const outOf: number = TestWords.words.length; const wordsPerLayout = Math.floor(outOf / layouts.length); const index = Math.floor( - (TestInput.input.getHistory().length + 1) / wordsPerLayout, + (TestState.activeWordIndex + 1) / wordsPerLayout, ); const mod = wordsPerLayout - ((TestState.activeWordIndex + 1) % wordsPerLayout); diff --git a/frontend/src/ts/test/test-input.ts b/frontend/src/ts/test/test-input.ts index e74f8ba05b07..d1b0cf76139f 100644 --- a/frontend/src/ts/test/test-input.ts +++ b/frontend/src/ts/test/test-input.ts @@ -99,11 +99,9 @@ type ErrorHistoryObject = { class Input { current: string; private history: string[]; - koreanStatus: boolean; constructor() { this.current = ""; this.history = []; - this.koreanStatus = false; } reset(): void { @@ -115,14 +113,6 @@ class Input { this.history = []; } - setKoreanStatus(val: boolean): void { - this.koreanStatus = val; - } - - getKoreanStatus(): boolean { - return this.koreanStatus; - } - pushHistory(): void { this.history.push(this.current); this.current = ""; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 337e19eb11c9..ad43d55ff73e 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -195,7 +195,7 @@ export function startTest(now: number): boolean { } catch (e) {} //use a recursive self-adjusting timer to avoid time drift TestStats.setStart(now); - void TestTimer.start(); + void TestTimer.start(now); TestUI.onTestStart(); return true; } @@ -338,7 +338,7 @@ export function restart(options = {} as RestartOptions): void { TestState.setBailedOut(false); Caret.resetPosition(); PaceCaret.reset(); - TestInput.input.setKoreanStatus(false); + TestState.setKoreanStatus(false); clearQuoteStats(); CompositionState.setComposing(false); CompositionState.setData(""); @@ -599,7 +599,7 @@ async function init(): Promise { /[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]/g, ) ) { - TestInput.input.setKoreanStatus(true); + TestState.setKoreanStatus(true); } for (let i = 0; i < generatedWords.length; i++) { @@ -660,7 +660,7 @@ export function areAllTestWordsGenerated(): boolean { //add word during the test export async function addWord(): Promise { if (Config.mode === "zen") { - TestUI.appendEmptyWordElement(); + TestUI.appendEmptyWordElement(TestState.activeWordIndex + 1); return; } @@ -674,7 +674,7 @@ export async function addWord(): Promise { const toPushCount = funboxToPush?.split(":")[1]; if (toPushCount !== undefined) bound = +toPushCount - 1; - if (TestWords.words.length - TestInput.input.getHistory().length > bound) { + if (TestWords.words.length - TestState.activeWordIndex > bound) { console.debug("Not adding word, enough words already"); return; } diff --git a/frontend/src/ts/test/test-state.ts b/frontend/src/ts/test/test-state.ts index 82c27c083657..831c101d08ec 100644 --- a/frontend/src/ts/test/test-state.ts +++ b/frontend/src/ts/test/test-state.ts @@ -13,6 +13,11 @@ export let isDirectionReversed = false; export let testRestarting = false; export let resultVisible = false; export let resultCalculating = false; +export let koreanStatus = false; + +export function setKoreanStatus(val: boolean): void { + koreanStatus = val; +} export function setRepeated(tf: boolean): void { isRepeated = tf; diff --git a/frontend/src/ts/test/test-stats.ts b/frontend/src/ts/test/test-stats.ts index 9d7701fc45f7..807962f76b3e 100644 --- a/frontend/src/ts/test/test-stats.ts +++ b/frontend/src/ts/test/test-stats.ts @@ -179,7 +179,7 @@ export function setLastSecondNotRound(): void { } export function calculateBurst(endTime: number = performance.now()): number { - const containsKorean = TestInput.input.getKoreanStatus(); + const containsKorean = TestState.koreanStatus; const timeToWrite = (endTime - TestInput.currentBurstStart) / 1000; if (timeToWrite <= 0) return 0; let wordLength: number; @@ -213,7 +213,7 @@ export function removeAfkData(): void { } function getInputWords(): string[] { - const containsKorean = TestInput.input.getKoreanStatus(); + const containsKorean = TestState.koreanStatus; let inputWords = [...TestInput.input.getHistory()]; @@ -229,7 +229,7 @@ function getInputWords(): string[] { } function getTargetWords(): string[] { - const containsKorean = TestInput.input.getKoreanStatus(); + const containsKorean = TestState.koreanStatus; let targetWords = [ ...(Config.mode === "zen" diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index b7467e0dab62..849c4e269d7d 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -313,20 +313,20 @@ function checkIfTimerIsSlow(drift: number): void { } } -export async function start(): Promise { +export async function start(now: number): Promise { SlowTimer.clear(); slowTimerCount = 0; for (const id of slowTimerNotifIds) { removeNotification(id, "clear"); } slowTimerNotifIds = []; - void _startNew(); + void _startNew(now); // void _startOld(); } -async function _startNew(): Promise { +async function _startNew(now: number): Promise { newTimer.play(); - logTestEvent("timer", performance.now(), { + logTestEvent("timer", now, { event: "start", timer: Time.get(), }); diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index ffc105d1dfda..cd1dec9b948d 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -493,7 +493,7 @@ function showWords(): void { wordsEl.setHtml(""); if (Config.mode === "zen") { - appendEmptyWordElement(); + appendEmptyWordElement(0); } else { let wordsHTML = ""; for (let i = 0; i < TestWords.words.length; i++) { @@ -509,9 +509,7 @@ function showWords(): void { PaceCaret.resetCaretPosition(); } -export function appendEmptyWordElement( - index = TestInput.input.getHistory().length, -): void { +export function appendEmptyWordElement(index: number): void { wordsEl.appendHtml( `
`, ); diff --git a/frontend/src/ts/test/timer-progress.ts b/frontend/src/ts/test/timer-progress.ts index 89a41bc521e9..717748e9bbbc 100644 --- a/frontend/src/ts/test/timer-progress.ts +++ b/frontend/src/ts/test/timer-progress.ts @@ -2,7 +2,6 @@ import { Config } from "../config/store"; import * as CustomText from "./custom-text"; import * as DateTime from "../utils/date-and-time"; import * as TestWords from "./test-words"; -import * as TestInput from "./test-input"; import * as Time from "../legacy-states/time"; import * as TestState from "./test-state"; import { configEvent } from "../events/config"; @@ -111,12 +110,12 @@ function getCurrentCount(): number { 1 ); } else { - return TestInput.input.getHistory().length; + return TestState.activeWordIndex; } } function setTimerHtmlToInputLength(el: HTMLElement, wrapInDiv: boolean): void { - let historyLength = `${TestInput.input.getHistory().length}`; + let historyLength = `${TestState.activeWordIndex}`; if (wrapInDiv) { historyLength = `
${historyLength}
`; From 46c1ffaa9872befa2c804e14caf87e30ad06c234 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:03:17 +0200 Subject: [PATCH 2/4] chore(deps): bump @vitest/browser from 4.0.18 to 4.1.8 (#8043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@vitest/browser](https://github.com/vitest-dev/vitest/tree/HEAD/packages/browser) from 4.0.18 to 4.1.8.
Release notes

Sourced from @​vitest/browser's releases.

v4.1.8

   🐞 Bug Fixes

    View changes on GitHub

v4.1.7

   🐞 Bug Fixes

    View changes on GitHub

v4.1.6

   🐞 Bug Fixes

   🏎 Performance

    View changes on GitHub

v4.1.5

   🚀 Experimental Features

   🐞 Bug Fixes

    View changes on GitHub

... (truncated)

Commits
  • e61f2dd chore: release v4.1.8
  • e4067b3 fix(browser): disable client cdp API when allowWrite/allowExec: false [ba...
  • a09d472 chore: release v4.1.7
  • a8fd24c chore: release v4.1.6
  • 18af98c fix(browser): simplify orchestrator otel carrier (#10285)
  • 3188260 feat(browser): provide project reference in ToMatchScreenshotResolvePath (#...
  • e399846 chore: release v4.1.5
  • ac04bac chore: release v4.1.4
  • d4fbb5c feat(experimental): support aria snapshot (#9668)
  • 65c9d55 fix(browser): spread user server options into browser Vite server in project ...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@vitest/browser&package-manager=npm_and_yarn&previous-version=4.0.18&new-version=4.1.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/monkeytypegame/monkeytype/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 216 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 147 insertions(+), 69 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fcff95bbd8cd..e9d87439dc0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -607,7 +607,7 @@ importers: version: 10.4.1(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) '@storybook/addon-vitest': specifier: ^10.2.14 - version: 10.2.16(@vitest/browser-playwright@4.0.18)(@vitest/browser@4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0))(@vitest/runner@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.1.0) + version: 10.2.16(@vitest/browser-playwright@4.0.18)(@vitest/browser@4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0))(@vitest/runner@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.1.0) '@storybook/builder-vite': specifier: ^10.2.14 version: 10.2.16(esbuild@0.27.7)(rollup@4.60.1)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) @@ -616,13 +616,13 @@ importers: version: 4.2.1(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/browser': specifier: ^4.1.6 - version: 4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0) + version: 4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0) '@vitest/browser-playwright': specifier: ^4.0.18 version: 4.0.18(playwright@1.58.2)(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0) '@vitest/coverage-v8': specifier: ^4.1.5 - version: 4.1.5(@vitest/browser@4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0))(vitest@4.1.0) + version: 4.1.5(@vitest/browser@4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0))(vitest@4.1.0) playwright: specifier: ^1.58.2 version: 1.58.2 @@ -3256,48 +3256,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.49.0': resolution: {integrity: sha512-iNzkMPG18jPkwBOZ4/HEjwqfzAjq4RrUQ0CgId/fC1ENvYD5jLVAaU/gWgpiqP1ys07kxSsSggDd1fp3E7mQHw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.49.0': resolution: {integrity: sha512-BPHA/NN3LvoIXiid+iz3BHt5V0Rzx0tXAqRUovwE1NsbDaLG9e8mtv7evDGRIkVQacqTDBv0XL25THHsxSJosQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.49.0': resolution: {integrity: sha512-3Eroshe+s69htC9JIL0+zLGQczLtRKezkMhwqQC21VC5Z/fuLvzLfbAOLgJLUq601H8gDYjy7deYycfOBjCvWg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.49.0': resolution: {integrity: sha512-fnaERGgsxGm0lKAmO72EYR4BA3qBnzBTJBTi6EtUMq1D4R7EexRBMU4voXnx4TXla3SEDl9x4uNp/18SbkPjGg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.49.0': resolution: {integrity: sha512-rBwasMl1Uul1MCCeTGEFKnOTL7VUxHf+634jWStrQAbzpBJgd5Yz5m4F7exVCsoI8PHn57dNjssXagXLCLB5yA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.49.0': resolution: {integrity: sha512-BoC/F9xHe2y/deuBGA5Aw7bes07OD2gcL2wlpzTrfImR92vPP7S/k3LBTyspQZCNIVNdagkELcqKELwMLGIfAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.49.0': resolution: {integrity: sha512-umY6jFADAo/oztFKl8D/S6vSrG6oBpEskcentiRuz42kZVU2kfDXMWCYavxyZR2bwPjqkHpcHZ6EZFiH3Qj9ZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/binding-openharmony-arm64@0.49.0': resolution: {integrity: sha512-J85zQMiw2pXiGPK+OusmDvSnJ/dgpgN7VgmB2zOBtgS8F+nsOUfSg9ZEBrwbQscjZ7tkPbm38CG4VF5f53MsiA==} @@ -3400,48 +3408,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.64.0': resolution: {integrity: sha512-00QQ0h0Y7u0G69BgiH3+ky2aaq/QvkDL6DYok8htIuJHxybiux5aQ8jwmg8qIk9wha6UagUP2BAwAzbemcJbpg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.64.0': resolution: {integrity: sha512-2GaimTV6EMW+s5HS0An3oGbQme3BgHswvfVdGk3EB57Xe9+/gyT+Qd7lNVzb3rtir52vbIPzXfaYArzs5b5zcw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.64.0': resolution: {integrity: sha512-H46AtFb9wypjoVwGdlxrm0DsD809NGmtiK9HiyPKTxkSte2YjhC4S+00rOIrwCaxcyPiGid3Y3OMXp5KMAkGZw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.64.0': resolution: {integrity: sha512-HEgsidjjvvyzdg82icYkuFCf7REDV7B9JFwbIMbVwrKLBY0MrXX+bku3POn/hduZ2yW91IyVDUMq0Bf02KwXQw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.64.0': resolution: {integrity: sha512-Axvm8qryotmKN00P5w4JapaSjvP2LOSbdbBJiX+2SuHd3QzhW7TUc8skqgw+ahQZ5DmzEYeHCqauvW8f32Ns6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.64.0': resolution: {integrity: sha512-cR60vSd7+m+KRZ3GQGfDxWwahW5RMXg0qlGvAluZr0fTUYvw0H9N9AXAF/M/PMqgytyqvVNmBAkJG9l7U30Y1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-musl@1.64.0': resolution: {integrity: sha512-2u/aPZ9pEg7HnvZPDsHxUGNnrpr4qaHi+mCgLgpt+LYRzPrS4Px4wPfkIdRdr2GvKnaYyt+XSlto0Vm5sbStTg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/binding-openharmony-arm64@1.64.0': resolution: {integrity: sha512-kfhkGfCdoXLSxEkrhDlJrvBYajGmq+ma4EMc53dsOWTq+rIBOlI0vTBmpZNnM5oH2LY/K/w1HAK+UQEgjgpVUg==} @@ -3503,36 +3519,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -3675,36 +3697,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} @@ -3772,8 +3800,8 @@ packages: peerDependencies: rollup: ^1.20.0||^2.0.0 - '@rollup/pluginutils@5.3.0': - resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} engines: {node: '>=14.0.0'} peerDependencies: rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 @@ -3845,121 +3873,145 @@ packages: resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.60.1': resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.40.0': resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.40.0': resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.40.0': resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.40.0': resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.40.0': resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.40.0': resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.40.0': resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.40.0': resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.40.0': resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -4431,24 +4483,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -5064,10 +5120,10 @@ packages: peerDependencies: vitest: 4.0.18 - '@vitest/browser@4.1.6': - resolution: {integrity: sha512-ynsspTubXGSpa58JFJ24xIQt4z4A25epSbugEyaTmmrV1//Wec9EgE/LtoaC6yxUrXi5P7erGHRrkdZIHaVQuA==} + '@vitest/browser@4.1.8': + resolution: {integrity: sha512-u21VzX07HzlJYpFgkxmjEXar/tG2UqWGgyGG/46SrrPc7rSdCTPw5vuowopO9CIqF8UCUQzDFdbVnNpw6N0BfQ==} peerDependencies: - vitest: 4.1.6 + vitest: 4.1.8 '@vitest/coverage-v8@4.1.5': resolution: {integrity: sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==} @@ -5106,8 +5162,8 @@ packages: vite: optional: true - '@vitest/mocker@4.1.6': - resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -5129,8 +5185,8 @@ packages: '@vitest/pretty-format@4.1.5': resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} - '@vitest/pretty-format@4.1.6': - resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} '@vitest/runner@4.1.0': resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} @@ -5147,8 +5203,8 @@ packages: '@vitest/spy@4.1.0': resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} - '@vitest/spy@4.1.6': - resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} @@ -5162,8 +5218,8 @@ packages: '@vitest/utils@4.1.5': resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} - '@vitest/utils@4.1.6': - resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} '@vue/compiler-core@3.4.37': resolution: {integrity: sha512-ZDDT/KiLKuCRXyzWecNzC5vTcubGz4LECAtfGPENpo0nrmqJHwuWtRLxk/Sb9RAKtR9iFflFycbkjkY+W/PZUQ==} @@ -5540,8 +5596,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.32: - resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} engines: {node: '>=6.0.0'} hasBin: true @@ -6619,8 +6675,8 @@ packages: electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} - electron-to-chromium@1.5.362: - resolution: {integrity: sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w==} + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -7467,8 +7523,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} he@1.2.0: @@ -7619,8 +7675,8 @@ packages: immutable@4.3.8: resolution: {integrity: sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==} - immutable@5.1.5: - resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + immutable@5.1.6: + resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -8270,48 +8326,56 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -11145,8 +11209,8 @@ packages: resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} engines: {node: '>= 0.4'} - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} typedarray-to-buffer@3.1.5: @@ -11762,6 +11826,18 @@ packages: utf-8-validate: optional: true + ws@7.5.11: + resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -11774,8 +11850,8 @@ packages: utf-8-validate: optional: true - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -13487,7 +13563,7 @@ snapshots: '@eslint/eslintrc@3.3.3': dependencies: - ajv: 6.15.0 + ajv: 6.12.6 debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 @@ -14912,7 +14988,7 @@ snapshots: '@rollup/plugin-node-resolve@15.3.1(rollup@2.80.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@2.80.0) + '@rollup/pluginutils': 5.4.0(rollup@2.80.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 @@ -14941,7 +15017,7 @@ snapshots: picomatch: 2.3.2 rollup: 2.80.0 - '@rollup/pluginutils@5.3.0(rollup@2.80.0)': + '@rollup/pluginutils@5.4.0(rollup@2.80.0)': dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 @@ -15470,13 +15546,13 @@ snapshots: dependencies: storybook: 10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/addon-vitest@10.2.16(@vitest/browser-playwright@4.0.18)(@vitest/browser@4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0))(@vitest/runner@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.1.0)': + '@storybook/addon-vitest@10.2.16(@vitest/browser-playwright@4.0.18)(@vitest/browser@4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0))(@vitest/runner@4.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(vitest@4.1.0)': dependencies: '@storybook/global': 5.0.0 '@storybook/icons': 2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) storybook: 10.2.16(@testing-library/dom@10.4.1)(prettier@3.7.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) optionalDependencies: - '@vitest/browser': 4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0) + '@vitest/browser': 4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0) '@vitest/browser-playwright': 4.0.18(playwright@1.58.2)(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0) '@vitest/runner': 4.1.0 vitest: 4.1.0(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) @@ -15618,7 +15694,7 @@ snapshots: '@tanstack/devtools-event-bus@0.4.1': dependencies: - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -16284,33 +16360,33 @@ snapshots: pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 - tinyrainbow: 3.1.0 + tinyrainbow: 3.0.3 vitest: 4.1.0(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0)': + '@vitest/browser@4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) - '@vitest/utils': 4.1.6 + '@vitest/mocker': 4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 vitest: 4.1.0(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) - ws: 8.20.1 + ws: 8.21.0 transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/coverage-v8@4.1.5(@vitest/browser@4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0))(vitest@4.1.0)': + '@vitest/coverage-v8@4.1.5(@vitest/browser@4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0))(vitest@4.1.0)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.5 @@ -16324,7 +16400,7 @@ snapshots: tinyrainbow: 3.1.0 vitest: 4.1.0(@types/node@24.9.1)(@vitest/browser-playwright@4.0.18)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: - '@vitest/browser': 4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0) + '@vitest/browser': 4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.0) '@vitest/coverage-v8@4.1.5(vitest@4.1.0(@opentelemetry/api@1.8.0)(@types/node@24.9.1)(happy-dom@20.8.9)(jsdom@27.4.0)(vite@8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.2)))': dependencies: @@ -16383,7 +16459,7 @@ snapshots: '@vitest/spy': 4.1.0 '@vitest/utils': 4.1.0 chai: 6.2.2 - tinyrainbow: 3.1.0 + tinyrainbow: 3.0.3 '@vitest/mocker@4.0.18(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: @@ -16433,9 +16509,9 @@ snapshots: optionalDependencies: vite: 8.0.5(@emnapi/core@1.9.0)(@emnapi/runtime@1.9.0)(@types/node@24.9.1)(esbuild@0.27.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3) - '@vitest/mocker@4.1.6(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.8(vite@7.3.2(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(terser@5.48.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.1.6 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -16447,17 +16523,17 @@ snapshots: '@vitest/pretty-format@4.0.18': dependencies: - tinyrainbow: 3.1.0 + tinyrainbow: 3.0.3 '@vitest/pretty-format@4.1.0': dependencies: - tinyrainbow: 3.1.0 + tinyrainbow: 3.0.3 '@vitest/pretty-format@4.1.5': dependencies: tinyrainbow: 3.1.0 - '@vitest/pretty-format@4.1.6': + '@vitest/pretty-format@4.1.8': dependencies: tinyrainbow: 3.1.0 @@ -16481,7 +16557,7 @@ snapshots: '@vitest/spy@4.1.0': {} - '@vitest/spy@4.1.6': {} + '@vitest/spy@4.1.8': {} '@vitest/utils@3.2.4': dependencies: @@ -16492,13 +16568,13 @@ snapshots: '@vitest/utils@4.0.18': dependencies: '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.1.0 + tinyrainbow: 3.0.3 '@vitest/utils@4.1.0': dependencies: '@vitest/pretty-format': 4.1.0 convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 + tinyrainbow: 3.0.3 '@vitest/utils@4.1.5': dependencies: @@ -16506,9 +16582,9 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@vitest/utils@4.1.6': + '@vitest/utils@4.1.8': dependencies: - '@vitest/pretty-format': 4.1.6 + '@vitest/pretty-format': 4.1.8 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -16909,7 +16985,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.32: {} + baseline-browser-mapping@2.10.33: {} baseline-browser-mapping@2.9.11: {} @@ -17066,9 +17142,9 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.32 + baseline-browser-mapping: 2.10.33 caniuse-lite: 1.0.30001793 - electron-to-chromium: 1.5.362 + electron-to-chromium: 1.5.364 node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -18059,7 +18135,7 @@ snapshots: electron-to-chromium@1.5.267: {} - electron-to-chromium@1.5.362: {} + electron-to-chromium@1.5.364: {} emoji-regex@8.0.0: {} @@ -18139,7 +18215,7 @@ snapshots: has-property-descriptors: 1.0.2 has-proto: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.3 + hasown: 2.0.4 internal-slot: 1.1.0 is-array-buffer: 3.0.5 is-callable: 1.2.7 @@ -18168,7 +18244,7 @@ snapshots: typed-array-buffer: 1.0.3 typed-array-byte-length: 1.0.3 typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 + typed-array-length: 1.0.8 unbox-primitive: 1.1.0 which-typed-array: 1.1.21 @@ -19017,7 +19093,7 @@ snapshots: call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 - hasown: 2.0.3 + hasown: 2.0.4 is-callable: 1.2.7 functional-red-black-tree@1.0.1: @@ -19370,7 +19446,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hasown@2.0.3: + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -19534,7 +19610,7 @@ snapshots: immutable@4.3.8: {} - immutable@5.1.5: + immutable@5.1.6: optional: true import-fresh@3.3.1: @@ -19607,7 +19683,7 @@ snapshots: internal-slot@1.1.0: dependencies: es-errors: 1.3.0 - hasown: 2.0.3 + hasown: 2.0.4 side-channel: 1.1.0 ioredis@4.28.5: @@ -19692,7 +19768,7 @@ snapshots: is-core-module@2.16.2: dependencies: - hasown: 2.0.3 + hasown: 2.0.4 is-data-view@1.0.2: dependencies: @@ -19789,7 +19865,7 @@ snapshots: call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.4 is-regexp@1.0.0: {} @@ -22360,7 +22436,7 @@ snapshots: sass@1.98.0: dependencies: chokidar: 4.0.3 - immutable: 5.1.5 + immutable: 5.1.6 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 @@ -22623,7 +22699,7 @@ snapshots: queue-microtask: 1.2.3 randombytes: 2.1.0 readable-stream: 3.6.2 - ws: 7.5.10 + ws: 7.5.11 transitivePeerDependencies: - bufferutil - supports-color @@ -23616,7 +23692,7 @@ snapshots: is-typed-array: 1.1.15 reflect.getprototypeof: 1.0.10 - typed-array-length@1.0.7: + typed-array-length@1.0.8: dependencies: call-bind: 1.0.9 for-each: 0.3.5 @@ -24461,9 +24537,11 @@ snapshots: ws@7.5.10: {} + ws@7.5.11: {} + ws@8.19.0: {} - ws@8.20.1: {} + ws@8.21.0: {} wsl-utils@0.1.0: dependencies: From 5a172a4c54f4c4adf365bac07f0323fb65ae5011 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 1 Jun 2026 23:03:35 +0200 Subject: [PATCH 3/4] fix(settings): allow input of decimal values (@fehmer) (#8041) --- .../src/ts/components/modals/SimpleModal.tsx | 108 +++--------------- .../pages/settings/SettingsPage.tsx | 5 +- .../custom-setting/AnimationFpsLimit.tsx | 5 +- .../settings/custom-setting/MaxLineWidth.tsx | 5 +- .../pages/settings/custom-setting/MinAcc.tsx | 5 +- .../settings/custom-setting/MinBurst.tsx | 5 +- .../settings/custom-setting/MinSpeed.tsx | 5 +- .../settings/custom-setting/PaceCaret.tsx | 5 +- .../src/ts/components/ui/form/InputField.tsx | 60 +++++++++- .../ts/components/ui/form/TextareaField.tsx | 2 + 10 files changed, 99 insertions(+), 106 deletions(-) diff --git a/frontend/src/ts/components/modals/SimpleModal.tsx b/frontend/src/ts/components/modals/SimpleModal.tsx index 42303e5f0f53..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, @@ -10,14 +9,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 { @@ -38,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: { @@ -116,19 +109,13 @@ 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 ( } > @@ -156,66 +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()} - /> -
); } @@ -443,35 +400,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: {
{ - 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 === 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 +97,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?.toString()} /> @@ -93,3 +109,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; +} 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)} From 4e9c273de97dc65cc6a1add0bd07f6c239d94c72 Mon Sep 17 00:00:00 2001 From: InfiniteVoid_ <125770958+Infinite1024Void@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:09:00 -0400 Subject: [PATCH 4/4] impr(quotes): add javascript quotes (@Infinite1024Void) (#8035) ### Description Adds some JavaScript quotes from MDN and source code. --------- Co-authored-by: InfiniteVoid Co-authored-by: Jack --- frontend/static/quotes/code_javascript.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/static/quotes/code_javascript.json b/frontend/static/quotes/code_javascript.json index 68845ad636bf..52156defc707 100644 --- a/frontend/static/quotes/code_javascript.json +++ b/frontend/static/quotes/code_javascript.json @@ -276,6 +276,18 @@ "source": "Monkeytype Sourcecode", "length": 159, "id": 45 + }, + { + "text": "(function () {\n\t\"use strict\";\n\t/* Start of your code */\n\tfunction greetMe(yourName) {\n\t\talert(`Hello ${yourName}`);\n\t}\n\n\tgreetMe(\"World\");\n\t/* End of your code */\n})();", + "source": "MDN", + "length": 168, + "id": 46 + }, + { + "text": "function die(msg) { process.stderr.write(msg + '\n'); process.exit(2); }", + "source": "Julius Brussee - Caveman", + "length": 71, + "id": 47 } ] }