diff --git a/frontend/package.json b/frontend/package.json index 7bcd85ef1b08..a7231c169630 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -39,7 +39,10 @@ "@tanstack/pacer-lite": "0.2.1", "@tanstack/query-db-collection": "1.0.27", "@tanstack/solid-db": "0.2.10", + "@tanstack/solid-devtools": "0.8.0", "@tanstack/solid-form": "1.28.4", + "@tanstack/solid-hotkeys": "0.4.2", + "@tanstack/solid-hotkeys-devtools": "0.4.3", "@tanstack/solid-query": "5.90.23", "@tanstack/solid-query-devtools": "5.91.3", "@tanstack/solid-table": "8.21.3", @@ -60,7 +63,6 @@ "hangul-js": "0.2.6", "howler": "2.2.3", "idb": "8.0.3", - "konami": "1.7.0", "lz-ts": "1.1.2", "modern-screenshot": "4.6.8", "object-hash": "3.0.0", diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html index a997a27b2599..7049cbd4ee1f 100644 --- a/frontend/src/html/pages/settings.html +++ b/frontend/src/html/pages/settings.html @@ -38,13 +38,7 @@
tip: You can also change all these settings quickly using the command line ( - ctrl/cmd - + - shift - + - p - or - esc + )
diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index daa6e532be96..9a9e11c6a735 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -1549,6 +1549,7 @@ .textButton { padding: 0.5em 1em; + align-items: center; &.noInteraction { pointer-events: none; } diff --git a/frontend/src/ts/components/core/DevTools.tsx b/frontend/src/ts/components/core/DevTools.tsx index 85eb49e91b86..189a0a825a98 100644 --- a/frontend/src/ts/components/core/DevTools.tsx +++ b/frontend/src/ts/components/core/DevTools.tsx @@ -3,9 +3,9 @@ import { JSXElement, lazy, onMount, Suspense } from "solid-js"; let DevComponents: (() => JSXElement) | undefined; if (import.meta.env.DEV) { - const LazyQueryDevtools = lazy(async () => - import("@tanstack/solid-query-devtools").then((m) => ({ - default: m.SolidQueryDevtools, + const LazyTanstackDevtools = lazy(async () => + import("./TanstackDevtools").then((m) => ({ + default: m.TanStackDevtools, })), ); const LazyDevOptionsModal = lazy(async () => @@ -19,7 +19,7 @@ if (import.meta.env.DEV) { default: () => { onMount(() => { m.attachDevtoolsOverlay({ - defaultOpen: true, + defaultOpen: false, noPadding: true, }); }); @@ -31,7 +31,7 @@ if (import.meta.env.DEV) { DevComponents = () => ( - + diff --git a/frontend/src/ts/components/core/TanstackDevtools.tsx b/frontend/src/ts/components/core/TanstackDevtools.tsx new file mode 100644 index 000000000000..3f906ea91313 --- /dev/null +++ b/frontend/src/ts/components/core/TanstackDevtools.tsx @@ -0,0 +1,23 @@ +import { TanStackDevtools as TsDevTools } from "@tanstack/solid-devtools"; +import { hotkeysDevtoolsPlugin } from "@tanstack/solid-hotkeys-devtools"; +import { SolidQueryDevtoolsPanel } from "@tanstack/solid-query-devtools"; +import { JSXElement } from "solid-js"; + +import { queryClient } from "../../queries"; + +export function TanStackDevtools(): JSXElement { + return ( + , + defaultOpen: true, + }, + hotkeysDevtoolsPlugin(), + ]} + config={{ defaultOpen: false }} + /> + ); +} diff --git a/frontend/src/ts/components/hotkeys/CommandlineHotkey.tsx b/frontend/src/ts/components/hotkeys/CommandlineHotkey.tsx new file mode 100644 index 000000000000..544f5b3b415e --- /dev/null +++ b/frontend/src/ts/components/hotkeys/CommandlineHotkey.tsx @@ -0,0 +1,17 @@ +import { Show } from "solid-js"; + +import { hotkeys } from "../../states/hotkeys"; +import { isFirefox } from "../../utils/misc"; +import { Kbd } from "./Kbd"; + +export function CommandlineHotkey() { + return ( + <> + + +  or  + + + + ); +} diff --git a/frontend/src/ts/components/hotkeys/Kbd.tsx b/frontend/src/ts/components/hotkeys/Kbd.tsx new file mode 100644 index 000000000000..e9d44600f3c8 --- /dev/null +++ b/frontend/src/ts/components/hotkeys/Kbd.tsx @@ -0,0 +1,10 @@ +import { formatWithLabels, Hotkey } from "@tanstack/solid-hotkeys"; +import { JSXElement } from "solid-js"; + +export function Kbd(props: { hotkey: Hotkey }): JSXElement { + return ( + + {formatWithLabels(props.hotkey).toLowerCase().replace(/\+/g, " + ")} + + ); +} diff --git a/frontend/src/ts/components/hotkeys/QuickRestartHotkey.tsx b/frontend/src/ts/components/hotkeys/QuickRestartHotkey.tsx new file mode 100644 index 000000000000..48702b462bc0 --- /dev/null +++ b/frontend/src/ts/components/hotkeys/QuickRestartHotkey.tsx @@ -0,0 +1,16 @@ +import { JSXElement, Show } from "solid-js"; + +import { getConfig } from "../../config/store"; +import { hotkeys } from "../../states/hotkeys"; +import { Kbd } from "./Kbd"; + +export function QuickRestartHotkey(): JSXElement { + return ( + tab {">"} enter + > + + + ); +} diff --git a/frontend/src/ts/components/layout/footer/Keytips.tsx b/frontend/src/ts/components/layout/footer/Keytips.tsx index 3c703888f71f..4ccc8a98cc41 100644 --- a/frontend/src/ts/components/layout/footer/Keytips.tsx +++ b/frontend/src/ts/components/layout/footer/Keytips.tsx @@ -2,42 +2,27 @@ import { JSXElement, Show } from "solid-js"; import { getConfig } from "../../../config/store"; import { getFocus } from "../../../states/core"; -import { Conditional } from "../../common/Conditional"; +import { CommandlineHotkey } from "../../hotkeys/CommandlineHotkey"; +import { QuickRestartHotkey } from "../../hotkeys/QuickRestartHotkey"; export function Keytips(): JSXElement { - const userAgent = window.navigator.userAgent.toLowerCase(); - const modifierKey = - userAgent.includes("mac") && !userAgent.includes("firefox") - ? "cmd" - : "ctrl"; - - const commandKey = (): string => - getConfig.quickRestart === "esc" ? "tab" : "esc"; - return (
- - tab + enter - restart test - - } - else={ - <> - {getConfig.quickRestart} - restart test - - } - /> -
- {commandKey()} or {modifierKey} + shift{" "} - + p - command line +
+ + - restart test +
+ +
+ + - command line +
); diff --git a/frontend/src/ts/components/layout/header/Logo.tsx b/frontend/src/ts/components/layout/header/Logo.tsx index 41127879c8c2..e985eb8f8cd5 100644 --- a/frontend/src/ts/components/layout/header/Logo.tsx +++ b/frontend/src/ts/components/layout/header/Logo.tsx @@ -1,10 +1,7 @@ import { JSXElement } from "solid-js"; -import { - restartTestEvent, - getActivePage, - getFocus, -} from "../../../states/core"; +import { restartTestEvent } from "../../../events/test"; +import { getActivePage, getFocus } from "../../../states/core"; import { cn } from "../../../utils/cn"; import { isDevEnvironment } from "../../../utils/env"; diff --git a/frontend/src/ts/components/layout/header/Nav.tsx b/frontend/src/ts/components/layout/header/Nav.tsx index fbebe1e954d0..641d6981f1e1 100644 --- a/frontend/src/ts/components/layout/header/Nav.tsx +++ b/frontend/src/ts/components/layout/header/Nav.tsx @@ -1,17 +1,14 @@ import { useQuery } from "@tanstack/solid-query"; import { createMemo, JSXElement, Show } from "solid-js"; +import { restartTestEvent } from "../../../events/test"; import { createEffectOn } from "../../../hooks/effects"; import { prefetchAboutPage, prefetchLeaderboardPage, } from "../../../queries/prefetch"; import { getServerConfigurationQueryOptions } from "../../../queries/server-configuration"; -import { - restartTestEvent, - getActivePage, - getFocus, -} from "../../../states/core"; +import { getActivePage, getFocus } from "../../../states/core"; import { getAccountButtonSpinner, getAnimatedLevel, diff --git a/frontend/src/ts/components/mount.tsx b/frontend/src/ts/components/mount.tsx index 7e96184bfe23..206bc9eeec84 100644 --- a/frontend/src/ts/components/mount.tsx +++ b/frontend/src/ts/components/mount.tsx @@ -6,6 +6,7 @@ import { queryClient } from "../queries"; import { qsa } from "../utils/dom"; import { DevTools } from "./core/DevTools"; import { Theme } from "./core/Theme"; +import { CommandlineHotkey } from "./hotkeys/CommandlineHotkey"; import { Footer } from "./layout/footer/Footer"; import { Header } from "./layout/header/Header"; import { Overlays } from "./layout/overlays/Overlays"; @@ -32,6 +33,7 @@ const components: Record JSXElement> = { theme: () => , header: () =>
, devtools: () => , + commandlinehotkey: () => , }; function mountToMountpoint(name: string, component: () => JSXElement): void { diff --git a/frontend/src/ts/components/pages/AboutPage.tsx b/frontend/src/ts/components/pages/AboutPage.tsx index f14caf161fb8..b365c71dc9f3 100644 --- a/frontend/src/ts/components/pages/AboutPage.tsx +++ b/frontend/src/ts/components/pages/AboutPage.tsx @@ -17,6 +17,8 @@ import { Button } from "../common/Button"; import { ChartJs } from "../common/ChartJs"; import { Fa } from "../common/Fa"; import { H2, H3 } from "../common/Headers"; +import { CommandlineHotkey } from "../hotkeys/CommandlineHotkey"; +import { QuickRestartHotkey } from "../hotkeys/QuickRestartHotkey"; export function AboutPage(): JSXElement { const isOpen = () => getActivePage() === "about"; @@ -201,10 +203,8 @@ export function AboutPage(): JSXElement {

- You can use tab and enter (or just{" "} - tab if you have quick tab mode enabled) to restart the - typing test. Open the command line by pressing ctrl/cmd +{" "} - shift + p or esc - there you can + You can use to restart the typing test. Open + the command line by pressing - there you can access all the functionality you need without touching your mouse.

diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts index 6f670371044c..e763f0f04168 100644 --- a/frontend/src/ts/elements/modes-notice.ts +++ b/frontend/src/ts/elements/modes-notice.ts @@ -12,6 +12,7 @@ import Format from "../singletons/format"; import { getActiveFunboxes, getActiveFunboxNames } from "../test/funbox/list"; import { escapeHTML, getMode2 } from "../utils/misc"; import { qsr } from "../utils/dom"; +import { wordsHasNewline, wordsHasTab } from "../states/test"; configEvent.subscribe(({ key }) => { const configKeys: ConfigEventKey[] = [ @@ -56,28 +57,28 @@ export async function update(): Promise { ); } - if (TestWords.hasTab) { + if (wordsHasTab()) { if (Config.quickRestart === "esc") { testModesNotice.appendHtml( - `
shift + tab to open commandline
`, + `
shift + tab to open commandline
`, ); testModesNotice.appendHtml( - `
shift + esc to restart
`, + `
esc to restart
`, ); } if (Config.quickRestart === "tab") { testModesNotice.appendHtml( - `
shift + tab to restart
`, + `
shift + tab to restart
`, ); } } if ( - (TestWords.hasNewline || Config.funbox.includes("58008")) && + (wordsHasNewline() || Config.funbox.includes("58008")) && Config.quickRestart === "enter" ) { testModesNotice.appendHtml( - `
shift + enter to restart
`, + `
shift + enter to restart
`, ); } @@ -87,7 +88,7 @@ export async function update(): Promise { testModesNotice.appendHtml( `
${escapeHTML( customTextName, - )} (shift + enter to save progress)
`, + )} (shift + enter to save progress)`, ); } @@ -99,7 +100,7 @@ export async function update(): Promise { if (Config.mode === "zen") { testModesNotice.appendHtml( - `
shift + enter to finish zen
`, + `
shift + enter to finish zen
`, ); } diff --git a/frontend/src/ts/event-handlers/global.ts b/frontend/src/ts/event-handlers/global.ts index 34262c190d5c..a4bec8b27c3f 100644 --- a/frontend/src/ts/event-handlers/global.ts +++ b/frontend/src/ts/event-handlers/global.ts @@ -1,14 +1,10 @@ import * as Misc from "../utils/misc"; import * as PageTransition from "../legacy-states/page-transition"; import { Config } from "../config/store"; -import * as TestWords from "../test/test-words"; -import * as Commandline from "../commandline/commandline"; import { showErrorNotification } from "../states/notifications"; import { getActivePage } from "../states/core"; import { ModifierKeys } from "../constants/modifier-keys"; import { focusWords } from "../test/test-ui"; -import * as TestLogic from "../test/test-logic"; -import { navigate } from "../controllers/route-controller"; import { isInputElementFocused } from "../input/input-element"; import * as TestState from "../test/test-state"; import { isDevEnvironment } from "../utils/env"; @@ -35,52 +31,6 @@ document.addEventListener("keydown", (e) => { } } } - - if ( - (e.key === "Escape" && Config.quickRestart !== "esc") || - (e.key === "Tab" && - Config.quickRestart === "esc" && - !TestWords.hasTab && - !e.shiftKey) || - (e.key === "Tab" && - Config.quickRestart === "esc" && - TestWords.hasTab && - e.shiftKey) || - (e.key.toLowerCase() === "p" && (e.metaKey || e.ctrlKey) && e.shiftKey) - ) { - const popupVisible = Misc.isAnyPopupVisible(); - if (!popupVisible) { - e.preventDefault(); - Commandline.show(); - } - } - - if (!isInputElementFocused()) { - const isInteractiveElement = - document.activeElement?.tagName === "INPUT" || - document.activeElement?.tagName === "TEXTAREA" || - document.activeElement?.tagName === "SELECT" || - document.activeElement?.tagName === "BUTTON" || - document.activeElement?.classList.contains("button") === true || - document.activeElement?.classList.contains("textButton") === true; - - if ( - (e.key === "Tab" && - Config.quickRestart === "tab" && - !isInteractiveElement) || - (e.key === "Escape" && Config.quickRestart === "esc") || - (e.key === "Enter" && - Config.quickRestart === "enter" && - !isInteractiveElement) - ) { - e.preventDefault(); - if (getActivePage() === "test") { - TestLogic.restart({ isQuickRestart: !e.shiftKey }); - } else { - void navigate(""); - } - } - } }); //stop space scrolling diff --git a/frontend/src/ts/events/test.ts b/frontend/src/ts/events/test.ts new file mode 100644 index 000000000000..9f3519199b41 --- /dev/null +++ b/frontend/src/ts/events/test.ts @@ -0,0 +1,5 @@ +import { createEvent } from "../hooks/createEvent"; + +export const restartTestEvent = createEvent< + { isQuickRestart?: boolean } | undefined +>(); diff --git a/frontend/src/ts/index.ts b/frontend/src/ts/index.ts index 3dfc3859b2d0..d6b4a96dbe5a 100644 --- a/frontend/src/ts/index.ts +++ b/frontend/src/ts/index.ts @@ -46,6 +46,8 @@ import "./ready"; import { setVersion } from "./states/core"; import { loadFromLocalStorage } from "./config/lifecycle"; +import "./input/hotkeys"; + // Lock Math.random Object.defineProperty(Math, "random", { value: Math.random, diff --git a/frontend/src/ts/input/handlers/before-insert-text.ts b/frontend/src/ts/input/handlers/before-insert-text.ts index d06b90e35da1..692da300a832 100644 --- a/frontend/src/ts/input/handlers/before-insert-text.ts +++ b/frontend/src/ts/input/handlers/before-insert-text.ts @@ -9,6 +9,7 @@ import { getInputElementValue } from "../input-element"; import { isAwaitingNextWord } from "../state"; import { shouldInsertSpaceCharacter } from "../helpers/validation"; import * as SlowTimer from "../../legacy-states/slow-timer"; +import { wordsHasNewline } from "../../states/test"; /** * Handles logic before inserting text into the input element. @@ -53,7 +54,7 @@ export function onBeforeInsertText(data: string): boolean { } //only allow newlines if the test has newlines or in zen mode - if (data === "\n" && !TestWords.hasNewline && Config.mode !== "zen") { + if (data === "\n" && !wordsHasNewline() && Config.mode !== "zen") { return true; } diff --git a/frontend/src/ts/input/handlers/keydown.ts b/frontend/src/ts/input/handlers/keydown.ts index d27373dff7ff..af206c3f731f 100644 --- a/frontend/src/ts/input/handlers/keydown.ts +++ b/frontend/src/ts/input/handlers/keydown.ts @@ -5,7 +5,6 @@ import { getCharFromEvent } from "../../test/layout-emulator"; import * as Monkey from "../../test/monkey"; import { emulateInsertText } from "./insert-text"; import * as TestState from "../../test/test-state"; -import * as TestWords from "../../test/test-words"; import * as JSONData from "../../utils/json-data"; import { showNoticeNotification, @@ -26,16 +25,10 @@ import { getActiveFunboxNames, } from "../../test/funbox/list"; import { Keycode } from "../../constants/keys"; +import { wordsHasTab } from "../../states/test"; export async function handleTab(e: KeyboardEvent, now: number): Promise { - if (Config.quickRestart === "tab") { - e.preventDefault(); - if ((TestWords.hasTab && e.shiftKey) || !TestWords.hasTab) { - TestLogic.restart({ isQuickRestart: !e.shiftKey }); - return; - } - } - if (TestWords.hasTab) { + if (wordsHasTab()) { await emulateInsertText({ data: "\t", now }); e.preventDefault(); return; @@ -80,14 +73,6 @@ export async function handleEnter( } } } - - if (Config.quickRestart === "enter") { - e.preventDefault(); - if ((TestWords.hasNewline && e.shiftKey) || !TestWords.hasNewline) { - TestLogic.restart({ isQuickRestart: !e.shiftKey }); - return; - } - } } export async function handleOppositeShift(event: KeyboardEvent): Promise { @@ -192,10 +177,4 @@ export async function onKeydown(event: KeyboardEvent): Promise { await handleEnter(event, now); return; } - - if (event.key === "Escape" && Config.quickRestart === "esc") { - event.preventDefault(); - TestLogic.restart({ isQuickRestart: !event.shiftKey }); - return; - } } diff --git a/frontend/src/ts/input/hotkeys/commandline.ts b/frontend/src/ts/input/hotkeys/commandline.ts new file mode 100644 index 000000000000..a34711ecbc48 --- /dev/null +++ b/frontend/src/ts/input/hotkeys/commandline.ts @@ -0,0 +1,10 @@ +import { hotkeys } from "../../states/hotkeys"; +import { showModal } from "../../states/modals"; +import { createHotkey } from "./utils"; + +function openCommandline(): void { + showModal("Commandline"); +} + +createHotkey(() => hotkeys.commandline, openCommandline); +createHotkey("Mod+Shift+P", openCommandline); diff --git a/frontend/src/ts/input/hotkeys/index.ts b/frontend/src/ts/input/hotkeys/index.ts new file mode 100644 index 000000000000..2dd26876377a --- /dev/null +++ b/frontend/src/ts/input/hotkeys/index.ts @@ -0,0 +1,3 @@ +import "./quickrestart"; +import "./commandline"; +import "./konami"; diff --git a/frontend/src/ts/input/hotkeys/konami.ts b/frontend/src/ts/input/hotkeys/konami.ts new file mode 100644 index 000000000000..e4c496386913 --- /dev/null +++ b/frontend/src/ts/input/hotkeys/konami.ts @@ -0,0 +1,19 @@ +import { createHotkeySequence } from "@tanstack/solid-hotkeys"; + +createHotkeySequence( + [ + "ArrowUp", + "ArrowUp", + "ArrowDown", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "ArrowLeft", + "ArrowRight", + "B", + "A", + ], + () => { + window.open("https://keymash.io/", "_blank"); + }, +); diff --git a/frontend/src/ts/input/hotkeys/quickrestart.ts b/frontend/src/ts/input/hotkeys/quickrestart.ts new file mode 100644 index 000000000000..eb0ca51c7869 --- /dev/null +++ b/frontend/src/ts/input/hotkeys/quickrestart.ts @@ -0,0 +1,21 @@ +import { isAnyPopupVisible } from "../../utils/misc"; + +import { navigate } from "../../controllers/route-controller"; +import { restartTestEvent } from "../../events/test"; +import { getActivePage } from "../../states/core"; +import { hotkeys } from "../../states/hotkeys"; +import { createHotkey } from "./utils"; + +function quickRestart(e: KeyboardEvent): void { + if (isAnyPopupVisible()) { + return; + } + + if (getActivePage() === "test") { + restartTestEvent.dispatch({ isQuickRestart: !e.shiftKey }); + } else { + void navigate(""); + } +} + +createHotkey(() => hotkeys.quickRestart, quickRestart); diff --git a/frontend/src/ts/input/hotkeys/utils.ts b/frontend/src/ts/input/hotkeys/utils.ts new file mode 100644 index 000000000000..d306d157bdc2 --- /dev/null +++ b/frontend/src/ts/input/hotkeys/utils.ts @@ -0,0 +1,57 @@ +import { + Hotkey, + HotkeyCallback, + HotkeyCallbackContext, + createHotkey as registerHotkey, +} from "@tanstack/solid-hotkeys"; +import { isAnyPopupVisible } from "../../utils/misc"; +import { isInputElementFocused } from "../input-element"; + +export function createHotkey( + hotkey: Hotkey | (() => Hotkey), + callback: HotkeyCallback, +): void { + registerHotkey( + hotkey, + (e, context) => { + if (handleHotkeyOnInteractiveElement(e, context)) return; + e.stopPropagation(); + e.preventDefault(); + callback(e, context); + }, + { + ignoreInputs: false, //hotkeys are active on the words input, but not on other interactive elements + stopPropagation: false, //we set stopPropagation in the callback if the hotkey executes + preventDefault: false, //we set preventDefault in the callback if the hotkey executes + requireReset: true, + conflictBehavior: "replace", + }, + ); +} +function isInteractiveElementFocused(): boolean { + if (isInputElementFocused()) return false; + + return ( + document.activeElement?.tagName === "INPUT" || + document.activeElement?.tagName === "TEXTAREA" || + document.activeElement?.tagName === "SELECT" || + document.activeElement?.tagName === "BUTTON" || + document.activeElement?.classList.contains("button") === true || + document.activeElement?.classList.contains("textButton") === true + ); +} + +function handleHotkeyOnInteractiveElement( + e: KeyboardEvent, + { hotkey }: HotkeyCallbackContext, +): boolean { + if ( + (hotkey === "Tab" || hotkey === "Enter") && + isInteractiveElementFocused() + ) { + return true; + } else if (hotkey === "Escape" && isAnyPopupVisible()) { + return true; + } + return false; +} diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 3cf3e11f6bc2..62fe8c4f5057 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -723,17 +723,6 @@ export async function update( CustomBackgroundFilter.updateUI(); - const userAgent = window.navigator.userAgent.toLowerCase(); - const modifierKey = - userAgent.includes("mac") && !userAgent.includes("firefox") - ? "cmd" - : "ctrl"; - - const commandKey = Config.quickRestart === "esc" ? "tab" : "esc"; - qs(".pageSettings .tip")?.setHtml(` - tip: You can also change all these settings quickly using the - command line (${commandKey} or ${modifierKey} + shift + p)`); - if ( customLayoutFluidSelect !== undefined && //checking equal with order, because customLayoutFluid is ordered diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts index 9acc9a79e53e..3f3c306a26cb 100644 --- a/frontend/src/ts/ready.ts +++ b/frontend/src/ts/ready.ts @@ -1,8 +1,6 @@ import * as Misc from "./utils/misc"; import * as MonkeyPower from "./elements/monkey-power"; import * as MerchBanner from "./elements/merch-banner"; -//@ts-expect-error no types for this package -import Konami from "konami"; import * as ServerConfiguration from "./ape/server-configuration"; import { configLoadPromise } from "./config/lifecycle"; import { authPromise } from "./firebase"; @@ -32,10 +30,6 @@ onDOMReady(async () => { MonkeyPower.init(); - // untyped, need to ignore - // oxlint-disable-next-line no-unsafe-call - new Konami("https://keymash.io/"); - if (isDevEnvironment()) { void navigator.serviceWorker .getRegistrations() diff --git a/frontend/src/ts/states/core.ts b/frontend/src/ts/states/core.ts index 00695db0a8cc..4fdd2803e925 100644 --- a/frontend/src/ts/states/core.ts +++ b/frontend/src/ts/states/core.ts @@ -1,5 +1,4 @@ import { createSignal } from "solid-js"; -import { createEvent } from "../hooks/createEvent"; import { PageName } from "../pages/page"; export const [getActivePage, setActivePage] = createSignal("loading"); @@ -37,5 +36,3 @@ export const isLoggedIn = (): boolean => getUserId() !== null; export const [getSelectedProfileName, setSelectedProfileName] = createSignal< string | undefined >(undefined); - -export const restartTestEvent = createEvent(); diff --git a/frontend/src/ts/states/hotkeys.ts b/frontend/src/ts/states/hotkeys.ts new file mode 100644 index 000000000000..978e40f1b494 --- /dev/null +++ b/frontend/src/ts/states/hotkeys.ts @@ -0,0 +1,59 @@ +import { Config, QuickRestart } from "@monkeytype/schemas/configs"; +import { Hotkey } from "@tanstack/solid-hotkeys"; +import { createEffect } from "solid-js"; +import { createStore } from "solid-js/store"; +import { getConfig } from "../config/store"; +import { wordsHasNewline, wordsHasTab } from "./test"; +import { getActivePage } from "./core"; + +const hotkeyMapping: Record = { + off: "Meta+Mod+Alt+Shift+F22" as Hotkey, //Dummy + esc: "Escape", + tab: "Tab", + enter: "Enter", +}; + +type Hotkeys = { + quickRestart: Hotkey; + commandline: Hotkey; +}; + +export const [hotkeys, setHotkeys] = createStore( + calcHotkeys(getConfig, { + shiftTab: false, + shiftEnter: false, + }), +); + +createEffect(() => { + const isOnTestPage = getActivePage() === "test"; + setHotkeys( + calcHotkeys(getConfig, { + shiftTab: isOnTestPage && wordsHasTab(), + shiftEnter: isOnTestPage && wordsHasNewline(), + }), + ); +}); + +function calcHotkeys( + config: Config, + options: { shiftTab: boolean; shiftEnter: boolean }, +): Hotkeys { + return { + quickRestart: shiftedHotkey(hotkeyMapping[config.quickRestart], options), + commandline: shiftedHotkey( + getConfig.quickRestart === "esc" ? "Tab" : "Escape", + options, + ), + }; +} + +function shiftedHotkey( + hotkey: Hotkey, + options: { shiftTab: boolean; shiftEnter: boolean }, +): Hotkey { + if (hotkey === "Tab" && options.shiftTab) return "Shift+Tab"; + if (hotkey === "Enter" && options.shiftEnter) return "Shift+Enter"; + + return hotkey; +} diff --git a/frontend/src/ts/states/test.ts b/frontend/src/ts/states/test.ts new file mode 100644 index 000000000000..197a99da0dc7 --- /dev/null +++ b/frontend/src/ts/states/test.ts @@ -0,0 +1,4 @@ +import { createSignal } from "solid-js"; + +export const [wordsHasNewline, setWordsHasNewline] = createSignal(false); +export const [wordsHasTab, setWordsHasTab] = createSignal(false); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 8db390113010..c8956eb8e182 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -26,7 +26,8 @@ import * as TodayTracker from "./today-tracker"; import * as ChallengeContoller from "../controllers/challenge-controller"; import * as QuoteRateModal from "../modals/quote-rate"; import * as Result from "./result"; -import { getActivePage, restartTestEvent } from "../states/core"; +import { getActivePage } from "../states/core"; +import { restartTestEvent } from "../events/test"; import * as TestInput from "./test-input"; import * as TestWords from "./test-words"; import * as WordsGenerator from "./words-generator"; @@ -72,6 +73,7 @@ import { qs } from "../utils/dom"; import { setAccountButtonSpinner } from "../states/header"; import { Config } from "../config/store"; import { setQuoteLengthAll, toggleFunbox, setConfig } from "../config/setters"; +import { setWordsHasNewline, setWordsHasTab } from "../states/test"; let failReason = ""; export async function syncNotSignedInLastResult(uid: string): Promise { @@ -549,8 +551,8 @@ async function init(): Promise { } TestWords.setHasNumbers(hasNumbers); - TestWords.setHasTab(wordsHaveTab); - TestWords.setHasNewline(wordsHaveNewline); + setWordsHasTab(wordsHaveTab); + setWordsHasNewline(wordsHaveNewline); if ( generatedWords @@ -1432,7 +1434,7 @@ qs(".pageTest")?.onChild("click", "#restartTestButtonWithSameWordset", () => { }); }); -restartTestEvent.subscribe(() => restart()); +restartTestEvent.subscribe((event) => restart(event)); qs(".pageTest")?.onChild("click", "#testConfig .mode .textButton", (e) => { if (TestState.testRestarting) return; diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 5bd1f63d7463..808cb027c540 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -69,6 +69,7 @@ import { } from "../utils/dom"; import { getTheme } from "../states/theme"; import { skipBreakdownEvent } from "../states/header"; +import { wordsHasNewline } from "../states/test"; export const updateHintsPositionDebounced = Misc.debounceUntilResolved( updateHintsPosition, @@ -658,7 +659,7 @@ export function updateWordsWrapperHeight(force = false): void { wordsWrapperEl.setStyle({ height: wrapperHeight + "px" }); } else { //show 3 lines if tape mode is on and has newlines, otherwise use words height (because of indicate typos: below) - if (TestWords.hasNewline) { + if (wordsHasNewline()) { wordsWrapperEl.setStyle({ height: wordHeight * 3 + "px" }); } else { const wordsHeight = wordsEl.getOffsetHeight() ?? wordHeight; diff --git a/frontend/src/ts/test/test-words.ts b/frontend/src/ts/test/test-words.ts index 4b87130c9ea5..1f618e0fd0fe 100644 --- a/frontend/src/ts/test/test-words.ts +++ b/frontend/src/ts/test/test-words.ts @@ -57,8 +57,6 @@ class Words { } export const words = new Words(); -export let hasTab = false; -export let hasNewline = false; export let hasNumbers = false; export let currentQuote = null as QuoteWithTextSplit | null; @@ -66,14 +64,6 @@ export function setCurrentQuote(rq: QuoteWithTextSplit | null): void { currentQuote = rq; } -export function setHasTab(tf: boolean): void { - hasTab = tf; -} - -export function setHasNewline(tf: boolean): void { - hasNewline = tf; -} - export function setHasNumbers(tf: boolean): void { hasNumbers = tf; } diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 631ffe597dd1..7428c0d6fca9 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -685,6 +685,11 @@ export function isMacLike(): boolean { return isPlatform(/Mac|iPod|iPhone|iPad/); } +export function isFirefox(): boolean { + const userAgent = window.navigator.userAgent.toLowerCase(); + return userAgent.includes("firefox"); +} + export function scrollToCenterOrTop(el: HTMLElement | null): void { if (!el) return; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 462f6b2ce6b7..e181bef19bec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -312,9 +312,18 @@ importers: '@tanstack/solid-db': specifier: 0.2.10 version: 0.2.10(solid-js@1.9.10)(typescript@6.0.0-beta) + '@tanstack/solid-devtools': + specifier: 0.8.0 + version: 0.8.0(csstype@3.2.3)(solid-js@1.9.10) '@tanstack/solid-form': specifier: 1.28.4 version: 1.28.4(solid-js@1.9.10) + '@tanstack/solid-hotkeys': + specifier: 0.4.2 + version: 0.4.2(solid-js@1.9.10) + '@tanstack/solid-hotkeys-devtools': + specifier: 0.4.3 + version: 0.4.3(@tanstack/hotkeys@0.4.2)(@types/react@19.2.14)(csstype@3.2.3)(react@18.3.1)(solid-js@1.9.10) '@tanstack/solid-query': specifier: 5.90.23 version: 5.90.23(solid-js@1.9.10) @@ -375,9 +384,6 @@ importers: idb: specifier: 8.0.3 version: 8.0.3 - konami: - specifier: 1.7.0 - version: 1.7.0 lz-ts: specifier: 1.1.2 version: 1.1.2 @@ -649,7 +655,7 @@ importers: version: 6.0.0-beta vitest: specifier: 4.1.0 - version: 4.1.0(@opentelemetry/api@1.8.0)(@types/node@24.9.1)(happy-dom@20.0.10)(jsdom@27.4.0)(vite@8.0.1(@types/node@24.9.1)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.1.0(@types/node@24.9.1)(happy-dom@20.0.10)(jsdom@27.4.0)(vite@8.0.1(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) packages/funbox: dependencies: @@ -3758,10 +3764,53 @@ packages: peerDependencies: typescript: '>=4.7' + '@tanstack/devtools-client@0.0.6': + resolution: {integrity: sha512-f85ZJXJnDIFOoykG/BFIixuAevJovCvJF391LPs6YjBAPhGYC50NWlx1y4iF/UmK5/cCMx+/JqI5SBOz7FanQQ==} + engines: {node: '>=18'} + + '@tanstack/devtools-event-bus@0.4.1': + resolution: {integrity: sha512-cNnJ89Q021Zf883rlbBTfsaxTfi2r73/qejGtyTa7ksErF3hyDyAq1aTbo5crK9dAL7zSHh9viKY1BtMls1QOA==} + engines: {node: '>=18'} + '@tanstack/devtools-event-client@0.4.1': resolution: {integrity: sha512-GRxmPw4OHZ2oZeIEUkEwt/NDvuEqzEYRAjzUVMs+I0pd4C7k1ySOiuJK2CqF+K/yEAR3YZNkW3ExrpDarh9Vwg==} engines: {node: '>=18'} + '@tanstack/devtools-ui@0.5.1': + resolution: {integrity: sha512-T9JjAdqMSnxsVO6AQykD5vhxPF4iFLKtbYxee/bU3OLlk446F5C1220GdCmhDSz7y4lx+m8AvIS0bq6zzvdDUA==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.9.7' + + '@tanstack/devtools-utils@0.4.0': + resolution: {integrity: sha512-KsGzYhA8L/fCNgyyMyoUy+TKtx+DjNbzWwqH6wXL48Llzo7kvV9RynYJlaO8Qkzwm+NdHXSgsljQNjQ3CKPpZA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@types/react': '>=17.0.0' + preact: '>=10.0.0' + react: '>=17.0.0' + solid-js: '>=1.9.7' + vue: '>=3.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + preact: + optional: true + react: + optional: true + solid-js: + optional: true + vue: + optional: true + + '@tanstack/devtools@0.11.0': + resolution: {integrity: sha512-ARRAnEm0HYjKlB2adC9YyDG3fbq5LVjpxPe6Jz583SanXRM1aKrZIGHIA//oRldX3mWIpM4kB6mCyd+CXCLqhA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + solid-js: '>=1.9.7' + '@tanstack/eslint-plugin-query@5.91.4': resolution: {integrity: sha512-8a+GAeR7oxJ5laNyYBQ6miPK09Hi18o5Oie/jx8zioXODv/AUFLZQecKabPdpQSLmuDXEBPKFh+W5DKbWlahjQ==} peerDependencies: @@ -3774,6 +3823,16 @@ packages: '@tanstack/form-core@1.28.4': resolution: {integrity: sha512-2eox5ePrJ6kvA1DXD5QHk/GeGr3VFZ0uYR63UgQOe7bUg6h1JfXaIMqTjZK9sdGyE4oRNqFpoW54H0pZM7nObQ==} + '@tanstack/hotkeys-devtools@0.4.3': + resolution: {integrity: sha512-PdIQBau5qUHZnDi1jIeIyCBcT/ALW5FvpsWp5GD6PnIhdvseGnMfzLLAvZkc8zrD1eb6pjDRcwpsPY9ITZ0/3w==} + engines: {node: '>=18'} + peerDependencies: + '@tanstack/hotkeys': 0.4.2 + + '@tanstack/hotkeys@0.4.2': + resolution: {integrity: sha512-dCCu6Q91wZ2Mz7Vb+tzzpbKH0cSY9JXqJS7ZyouxewNL8oVmI228P9BmP94/1255g5WjPS+njenyrbWVeEQP5Q==} + engines: {node: '>=18'} + '@tanstack/pacer-lite@0.1.1': resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} @@ -3799,11 +3858,29 @@ packages: peerDependencies: solid-js: '>=1.9.0' + '@tanstack/solid-devtools@0.8.0': + resolution: {integrity: sha512-mJn8j6DYvyayyweeLbdaHwjhKFB1lpCU6cN1+KygluLX6etmrBBC/oDMKQTTbnrQk6EbI9Xa9kmyOREroIaXzA==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.9.7' + '@tanstack/solid-form@1.28.4': resolution: {integrity: sha512-ZhA/oQQJzfY0Un5XFiHbfCwuWyShATHIajdx6xjcm34g0xpwoioGyxAPm7pXLpnUAzln0liCkKX+L77rKj3/6A==} peerDependencies: solid-js: '>=1.9.9' + '@tanstack/solid-hotkeys-devtools@0.4.3': + resolution: {integrity: sha512-iP0u4BxkcPQeB2bhWzrO/ZhFrsazB9cAa9xq/6+q4GnC5c/jQQ15yDxp1UEeP4BLVX3kQ4cwpkHG9CVkopYz/A==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.7.0' + + '@tanstack/solid-hotkeys@0.4.2': + resolution: {integrity: sha512-HelOz/mnCZ6Q5n4udwVr/U6o7gEWt8mIKdzxxsMKS42nLK64moTZrO5FLFTsiCgUZS9Vm2N14mQ+6a7ha3Y0yQ==} + engines: {node: '>=18'} + peerDependencies: + solid-js: '>=1.7.0' + '@tanstack/solid-query-devtools@5.91.3': resolution: {integrity: sha512-xzVwIIxQPbiublZP3RkGp8KVjt8zenv5y1YTSRarP32mLUHJgfdofvjsDvMEhhL/lomz90qa0jCIGsSxoSTyYQ==} peerDependencies: @@ -5387,6 +5464,9 @@ packages: date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -6378,6 +6458,11 @@ packages: engines: {node: '>=0.6.0'} hasBin: true + goober@2.1.18: + resolution: {integrity: sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==} + peerDependencies: + csstype: ^3.0.10 + google-auth-library@9.12.0: resolution: {integrity: sha512-5pWjpxJMNJ5UTuhK7QPD5KFPsbosWkX4ajMDeZwXllTtwwqeiIzPWbHIddkLBkkn0mUPboTmukT5rd30Ec9igQ==} engines: {node: '>=14'} @@ -7156,9 +7241,6 @@ packages: known-css-properties@0.30.0: resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==} - konami@1.7.0: - resolution: {integrity: sha512-lfw/bXyPcOYVP0Vt05PKVYWp/Rx92eUIg48Kb2jkY2Ko00jWMTzk6jt5bRdkVRfvPr9RLTl1DM6vVrjWLXnQyg==} - kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} @@ -13928,8 +14010,65 @@ snapshots: '@tanstack/pacer-lite': 0.2.1 typescript: 6.0.0-beta + '@tanstack/devtools-client@0.0.6': + dependencies: + '@tanstack/devtools-event-client': 0.4.1 + + '@tanstack/devtools-event-bus@0.4.1': + dependencies: + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@tanstack/devtools-event-client@0.4.1': {} + '@tanstack/devtools-ui@0.5.1(csstype@3.2.3)(solid-js@1.9.10)': + dependencies: + clsx: 2.1.1 + dayjs: 1.11.20 + goober: 2.1.18(csstype@3.2.3) + solid-js: 1.9.10 + transitivePeerDependencies: + - csstype + + '@tanstack/devtools-ui@0.5.1(csstype@3.2.3)(solid-js@1.9.11)': + dependencies: + clsx: 2.1.1 + dayjs: 1.11.20 + goober: 2.1.18(csstype@3.2.3) + solid-js: 1.9.11 + transitivePeerDependencies: + - csstype + + '@tanstack/devtools-utils@0.4.0(@types/react@19.2.14)(react@18.3.1)(solid-js@1.9.10)': + optionalDependencies: + '@types/react': 19.2.14 + react: 18.3.1 + solid-js: 1.9.10 + + '@tanstack/devtools-utils@0.4.0(@types/react@19.2.14)(react@18.3.1)(solid-js@1.9.11)': + optionalDependencies: + '@types/react': 19.2.14 + react: 18.3.1 + solid-js: 1.9.11 + + '@tanstack/devtools@0.11.0(csstype@3.2.3)(solid-js@1.9.10)': + dependencies: + '@solid-primitives/event-listener': 2.4.5(solid-js@1.9.10) + '@solid-primitives/keyboard': 1.3.5(solid-js@1.9.10) + '@solid-primitives/resize-observer': 2.1.5(solid-js@1.9.10) + '@tanstack/devtools-client': 0.0.6 + '@tanstack/devtools-event-bus': 0.4.1 + '@tanstack/devtools-ui': 0.5.1(csstype@3.2.3)(solid-js@1.9.10) + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + solid-js: 1.9.10 + transitivePeerDependencies: + - bufferutil + - csstype + - utf-8-validate + '@tanstack/eslint-plugin-query@5.91.4(eslint@9.39.1(jiti@2.6.1))(typescript@6.0.0-beta)': dependencies: '@typescript-eslint/utils': 8.52.0(eslint@9.39.1(jiti@2.6.1))(typescript@6.0.0-beta) @@ -13945,6 +14084,25 @@ snapshots: '@tanstack/pacer-lite': 0.1.1 '@tanstack/store': 0.9.2 + '@tanstack/hotkeys-devtools@0.4.3(@tanstack/hotkeys@0.4.2)(@types/react@19.2.14)(csstype@3.2.3)(react@18.3.1)': + dependencies: + '@tanstack/devtools-ui': 0.5.1(csstype@3.2.3)(solid-js@1.9.11) + '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@18.3.1)(solid-js@1.9.11) + '@tanstack/hotkeys': 0.4.2 + clsx: 2.1.1 + goober: 2.1.18(csstype@3.2.3) + solid-js: 1.9.11 + transitivePeerDependencies: + - '@types/react' + - csstype + - preact + - react + - vue + + '@tanstack/hotkeys@0.4.2': + dependencies: + '@tanstack/store': 0.9.2 + '@tanstack/pacer-lite@0.1.1': {} '@tanstack/pacer-lite@0.2.1': {} @@ -13968,12 +14126,40 @@ snapshots: transitivePeerDependencies: - typescript + '@tanstack/solid-devtools@0.8.0(csstype@3.2.3)(solid-js@1.9.10)': + dependencies: + '@tanstack/devtools': 0.11.0(csstype@3.2.3)(solid-js@1.9.10) + solid-js: 1.9.10 + transitivePeerDependencies: + - bufferutil + - csstype + - utf-8-validate + '@tanstack/solid-form@1.28.4(solid-js@1.9.10)': dependencies: '@tanstack/form-core': 1.28.4 '@tanstack/solid-store': 0.9.2(solid-js@1.9.10) solid-js: 1.9.10 + '@tanstack/solid-hotkeys-devtools@0.4.3(@tanstack/hotkeys@0.4.2)(@types/react@19.2.14)(csstype@3.2.3)(react@18.3.1)(solid-js@1.9.10)': + dependencies: + '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@18.3.1)(solid-js@1.9.10) + '@tanstack/hotkeys-devtools': 0.4.3(@tanstack/hotkeys@0.4.2)(@types/react@19.2.14)(csstype@3.2.3)(react@18.3.1) + solid-js: 1.9.10 + transitivePeerDependencies: + - '@tanstack/hotkeys' + - '@types/react' + - csstype + - preact + - react + - vue + + '@tanstack/solid-hotkeys@0.4.2(solid-js@1.9.10)': + dependencies: + '@tanstack/hotkeys': 0.4.2 + '@tanstack/solid-store': 0.9.2(solid-js@1.9.10) + solid-js: 1.9.10 + '@tanstack/solid-query-devtools@5.91.3(@tanstack/solid-query@5.90.23(solid-js@1.9.10))(solid-js@1.9.10)': dependencies: '@tanstack/query-devtools': 5.93.0 @@ -14545,6 +14731,14 @@ snapshots: optionalDependencies: vite: 8.0.1(@types/node@20.5.1)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + '@vitest/mocker@4.1.0(vite@8.0.1(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.1(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + '@vitest/mocker@4.1.0(vite@8.0.1(@types/node@24.9.1)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.70.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.1.0 @@ -15795,6 +15989,8 @@ snapshots: date-fns@3.6.0: {} + dayjs@1.11.20: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -17187,6 +17383,10 @@ snapshots: dependencies: minimist: 1.2.8 + goober@2.1.18(csstype@3.2.3): + dependencies: + csstype: 3.2.3 + google-auth-library@9.12.0(encoding@0.1.13): dependencies: base64-js: 1.5.1 @@ -18037,8 +18237,6 @@ snapshots: known-css-properties@0.30.0: {} - konami@1.7.0: {} - kuler@2.0.0: {} lazystream@1.0.1: @@ -21687,6 +21885,23 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 + vite@8.0.1(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.3 + postcss: 8.5.8 + rolldown: 1.0.0-rc.10 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.9.1 + esbuild: 0.25.11 + fsevents: 2.3.3 + jiti: 2.6.1 + sass: 1.98.0 + terser: 5.46.1 + tsx: 4.21.0 + yaml: 2.8.2 + vite@8.0.1(@types/node@24.9.1)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.70.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: lightningcss: 1.32.0 @@ -21818,6 +22033,35 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.0(@types/node@24.9.1)(happy-dom@20.0.10)(jsdom@27.4.0)(vite@8.0.1(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@8.0.1(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 8.0.1(@types/node@24.9.1)(esbuild@0.25.11)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.9.1 + happy-dom: 20.0.10 + jsdom: 27.4.0 + transitivePeerDependencies: + - msw + vitest@4.1.0(@types/node@24.9.1)(happy-dom@20.0.10)(jsdom@27.4.0)(vite@8.0.1(@types/node@24.9.1)(esbuild@0.27.3)(jiti@2.6.1)(sass@1.70.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@vitest/expect': 4.1.0