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/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)} 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}
`; 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 } ] } 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: