From fad3c1084f9b50e200dc37eb9170ece120d423d8 Mon Sep 17 00:00:00 2001 From: Miodec Date: Wed, 25 Mar 2026 23:53:09 +0100 Subject: [PATCH 1/9] add plugin, reorder files --- .../ts/components/{core => dev}/DevTools.tsx | 0 .../src/ts/components/dev/SignalsDevtools.tsx | 281 ++++++++++++++++++ .../{core => dev}/TanstackDevtools.tsx | 2 + frontend/src/ts/components/mount.tsx | 2 +- frontend/src/ts/states/modals.ts | 2 +- 5 files changed, 285 insertions(+), 2 deletions(-) rename frontend/src/ts/components/{core => dev}/DevTools.tsx (100%) create mode 100644 frontend/src/ts/components/dev/SignalsDevtools.tsx rename frontend/src/ts/components/{core => dev}/TanstackDevtools.tsx (88%) diff --git a/frontend/src/ts/components/core/DevTools.tsx b/frontend/src/ts/components/dev/DevTools.tsx similarity index 100% rename from frontend/src/ts/components/core/DevTools.tsx rename to frontend/src/ts/components/dev/DevTools.tsx diff --git a/frontend/src/ts/components/dev/SignalsDevtools.tsx b/frontend/src/ts/components/dev/SignalsDevtools.tsx new file mode 100644 index 000000000000..d68143748663 --- /dev/null +++ b/frontend/src/ts/components/dev/SignalsDevtools.tsx @@ -0,0 +1,281 @@ +import type { TanStackDevtoolsSolidPlugin } from "@tanstack/solid-devtools"; + +import { createEffect, createSignal, For, JSXElement, on } from "solid-js"; + +import { getConfig } from "../../config/store"; +import { getBanners } from "../../states/banners"; +import { bp } from "../../states/breakpoints"; +import { + getActivePage, + getVersion, + getThemeIndicator, + getCommandlineSubgroup, + getGlobalOffsetTop, + getIsScreenshotting, + getUserId, + isLoggedIn, + getSelectedProfileName, +} from "../../states/core"; +import { + getAnimatedLevel, + getAccountButtonSpinner, + getXpBarData, +} from "../../states/header"; +import { hotkeys } from "../../states/hotkeys"; +import { + getSelection, + getPage, + getGoToUserPage, +} from "../../states/leaderboard-selection"; +import { getLoaderBarSignal } from "../../states/loader-bar"; +import { getLoginPageInputsEnabled } from "../../states/login"; +import { modalState } from "../../states/modals"; +import { + getNotifications, + getNotificationHistory, +} from "../../states/notifications"; +import { getPsas } from "../../states/psas"; +import { currentQuote, quoteStats } from "../../states/quote-rate"; +import { quoteId } from "../../states/quote-report"; +import { simpleModalConfig } from "../../states/simple-modal"; +import { getSnapshot, getLastResult } from "../../states/snapshot"; +import { + wordsHaveNewline, + wordsHaveTab, + getLoadedChallenge, + getResultVisible, + getFocus, +} from "../../states/test"; +import { getTheme } from "../../states/theme"; + +type SignalEntry = { name: string; get: () => unknown }; +type SignalGroup = { file: string; signals: SignalEntry[] }; + +const groups: SignalGroup[] = [ + { + file: "banners", + signals: [{ name: "banners", get: getBanners }], + }, + { + file: "breakpoints", + signals: [{ name: "bp", get: bp }], + }, + { + file: "config/store", + signals: [{ name: "config", get: () => ({ ...getConfig }) }], + }, + { + file: "core", + signals: [ + { name: "activePage", get: getActivePage }, + { name: "version", get: getVersion }, + { name: "themeIndicator", get: getThemeIndicator }, + { name: "commandlineSubgroup", get: getCommandlineSubgroup }, + { name: "globalOffsetTop", get: getGlobalOffsetTop }, + { name: "isScreenshotting", get: getIsScreenshotting }, + { name: "userId", get: getUserId }, + { name: "isLoggedIn", get: isLoggedIn }, + { name: "selectedProfileName", get: getSelectedProfileName }, + ], + }, + { + file: "header", + signals: [ + { name: "animatedLevel", get: getAnimatedLevel }, + { name: "accountButtonSpinner", get: getAccountButtonSpinner }, + { name: "xpBarData", get: getXpBarData }, + ], + }, + { + file: "hotkeys", + signals: [{ name: "hotkeys", get: () => ({ ...hotkeys }) }], + }, + { + file: "leaderboard-selection", + signals: [ + { name: "selection", get: getSelection }, + { name: "page", get: getPage }, + { name: "goToUserPage", get: getGoToUserPage }, + ], + }, + { + file: "loader-bar", + signals: [{ name: "loaderBarSignal", get: getLoaderBarSignal }], + }, + { + file: "login", + signals: [ + { name: "loginPageInputsEnabled", get: getLoginPageInputsEnabled }, + ], + }, + { + file: "modals", + signals: [ + { name: "openModals", get: () => ({ ...modalState.openModals }) }, + { name: "modalStack", get: () => [...modalState.modalStack] }, + { name: "pendingModal", get: () => modalState.pendingModal }, + { name: "pendingIsChained", get: () => modalState.pendingIsChained }, + ], + }, + { + file: "notifications", + signals: [ + { name: "notifications", get: getNotifications }, + { name: "notificationHistory", get: getNotificationHistory }, + ], + }, + { + file: "psas", + signals: [{ name: "psas", get: getPsas }], + }, + { + file: "quote-rate", + signals: [ + { name: "currentQuote", get: currentQuote }, + { name: "quoteStats", get: quoteStats }, + ], + }, + { + file: "quote-report", + signals: [{ name: "quoteId", get: quoteId }], + }, + { + file: "simple-modal", + signals: [{ name: "simpleModalConfig", get: simpleModalConfig }], + }, + { + file: "snapshot", + signals: [ + { name: "snapshot", get: getSnapshot }, + { name: "lastResult", get: getLastResult }, + ], + }, + { + file: "theme", + signals: [{ name: "theme", get: getTheme }], + }, + { + file: "test", + signals: [ + { name: "wordsHaveNewline", get: wordsHaveNewline }, + { name: "wordsHaveTab", get: wordsHaveTab }, + { name: "loadedChallenge", get: getLoadedChallenge }, + { name: "resultVisible", get: getResultVisible }, + { name: "focus", get: getFocus }, + ], + }, +]; + +function formatValue(value: unknown): string { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") { + return `${value}`; + } + return JSON.stringify(value); +} + +function SignalRow(props: { signal: SignalEntry }): JSXElement { + const [flashing, setFlashing] = createSignal(false); + let initialized = false; + + createEffect( + on( + () => formatValue(props.signal.get()), + () => { + if (!initialized) { + initialized = true; + return; + } + setFlashing(true); + setTimeout(() => setFlashing(false), 125); + }, + ), + ); + + return ( + + {props.signal.name} + {formatValue(props.signal.get())} + + ); +} + +function SignalsPanel(): JSXElement { + const [search, setSearch] = createSignal(""); + + const filteredGroups = (): SignalGroup[] => { + const query = search().toLowerCase(); + if (query === "") return groups; + return groups + .map((group) => { + if (group.file.toLowerCase().includes(query)) return group; + const filtered = group.signals.filter((s) => + s.name.toLowerCase().includes(query), + ); + return { file: group.file, signals: filtered }; + }) + .filter((group) => group.signals.length > 0); + }; + + return ( +
+
+ { + e.preventDefault(); + e.stopImmediatePropagation(); + setSearch(e.currentTarget.value); + }} + class="mb-3 w-full rounded border border-sub bg-bg px-2 py-1 text-xs text-text outline-none placeholder:text-sub focus:border-main" + style={{ + "background-color": "#313749", + "border-color": "#414962", + }} + /> +
+
+ + {(group) => ( +
+
+ {group.file} +
+ + + + {(signal) => } + + +
+
+ )} +
+
+
+ ); +} + +export function SignalsDevtoolsPlugin(): TanStackDevtoolsSolidPlugin { + return { + id: "core-signals", + name: "Core Signals", + render: () => , + }; +} diff --git a/frontend/src/ts/components/core/TanstackDevtools.tsx b/frontend/src/ts/components/dev/TanstackDevtools.tsx similarity index 88% rename from frontend/src/ts/components/core/TanstackDevtools.tsx rename to frontend/src/ts/components/dev/TanstackDevtools.tsx index 3f906ea91313..6e31b0559d23 100644 --- a/frontend/src/ts/components/core/TanstackDevtools.tsx +++ b/frontend/src/ts/components/dev/TanstackDevtools.tsx @@ -4,6 +4,7 @@ import { SolidQueryDevtoolsPanel } from "@tanstack/solid-query-devtools"; import { JSXElement } from "solid-js"; import { queryClient } from "../../queries"; +import { SignalsDevtoolsPlugin } from "./SignalsDevtools"; export function TanStackDevtools(): JSXElement { return ( @@ -16,6 +17,7 @@ export function TanStackDevtools(): JSXElement { defaultOpen: true, }, hotkeysDevtoolsPlugin(), + SignalsDevtoolsPlugin(), ]} config={{ defaultOpen: false }} /> diff --git a/frontend/src/ts/components/mount.tsx b/frontend/src/ts/components/mount.tsx index 9fbcf78c5eb3..432c8c884f28 100644 --- a/frontend/src/ts/components/mount.tsx +++ b/frontend/src/ts/components/mount.tsx @@ -4,8 +4,8 @@ import { render } from "solid-js/web"; import { queryClient } from "../queries"; import { qsa } from "../utils/dom"; -import { DevTools } from "./core/DevTools"; import { Theme } from "./core/Theme"; +import { DevTools } from "./dev/DevTools"; import { CommandlineHotkey } from "./hotkeys/CommandlineHotkey"; import { Footer } from "./layout/footer/Footer"; import { Header } from "./layout/header/Header"; diff --git a/frontend/src/ts/states/modals.ts b/frontend/src/ts/states/modals.ts index fa2fac25505e..2eb936cc8005 100644 --- a/frontend/src/ts/states/modals.ts +++ b/frontend/src/ts/states/modals.ts @@ -37,7 +37,7 @@ type ModalState = { pendingIsChained: boolean; }; -const [modalState, setModalState] = createStore({ +export const [modalState, setModalState] = createStore({ openModals: {}, modalStack: [], pendingModal: null, From c7dcb3051886fe949929102a0a1c56490ff57138 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 26 Mar 2026 00:16:57 +0100 Subject: [PATCH 2/9] br --- .../src/ts/components/dev/SignalsDevtools.tsx | 383 +++++++++++++++--- 1 file changed, 336 insertions(+), 47 deletions(-) diff --git a/frontend/src/ts/components/dev/SignalsDevtools.tsx b/frontend/src/ts/components/dev/SignalsDevtools.tsx index d68143748663..3ee8bab5569b 100644 --- a/frontend/src/ts/components/dev/SignalsDevtools.tsx +++ b/frontend/src/ts/components/dev/SignalsDevtools.tsx @@ -1,34 +1,62 @@ import type { TanStackDevtoolsSolidPlugin } from "@tanstack/solid-devtools"; -import { createEffect, createSignal, For, JSXElement, on } from "solid-js"; +import { + createEffect, + createSignal, + For, + JSXElement, + on, + Show, +} from "solid-js"; import { getConfig } from "../../config/store"; import { getBanners } from "../../states/banners"; import { bp } from "../../states/breakpoints"; import { getActivePage, + setActivePage, getVersion, + setVersion, getThemeIndicator, + setThemeIndicator, getCommandlineSubgroup, + setCommandlineSubgroup, getGlobalOffsetTop, + setGlobalOffsetTop, getIsScreenshotting, + setIsScreenshotting, getUserId, + setUserId, isLoggedIn, getSelectedProfileName, + setSelectedProfileName, } from "../../states/core"; import { getAnimatedLevel, + setAnimatedLevel, getAccountButtonSpinner, + setAccountButtonSpinner, getXpBarData, + setXpBarData, } from "../../states/header"; import { hotkeys } from "../../states/hotkeys"; import { getSelection, getPage, + setPage, getGoToUserPage, + setGoToUserPage, } from "../../states/leaderboard-selection"; -import { getLoaderBarSignal } from "../../states/loader-bar"; -import { getLoginPageInputsEnabled } from "../../states/login"; +import { + getLoaderBarSignal, + showLoaderBar, + hideLoaderBar, +} from "../../states/loader-bar"; +import { + getLoginPageInputsEnabled, + enableLoginPageInputs, + disableLoginPageInputs, +} from "../../states/login"; import { modalState } from "../../states/modals"; import { getNotifications, @@ -38,17 +66,31 @@ import { getPsas } from "../../states/psas"; import { currentQuote, quoteStats } from "../../states/quote-rate"; import { quoteId } from "../../states/quote-report"; import { simpleModalConfig } from "../../states/simple-modal"; -import { getSnapshot, getLastResult } from "../../states/snapshot"; +import { + getSnapshot, + getLastResult, + setLastResult, +} from "../../states/snapshot"; import { wordsHaveNewline, + setWordsHaveNewline, wordsHaveTab, + setWordsHaveTab, getLoadedChallenge, + setLoadedChallenge, getResultVisible, + setResultVisible, getFocus, + setFocus, } from "../../states/test"; -import { getTheme } from "../../states/theme"; +import { setTheme, getTheme } from "../../states/theme"; -type SignalEntry = { name: string; get: () => unknown }; +type SignalEntry = { + name: string; + get: () => unknown; + set?: (value: unknown) => void; + actions?: Record void>; +}; type SignalGroup = { file: string; signals: SignalEntry[] }; const groups: SignalGroup[] = [ @@ -67,23 +109,67 @@ const groups: SignalGroup[] = [ { file: "core", signals: [ - { name: "activePage", get: getActivePage }, - { name: "version", get: getVersion }, - { name: "themeIndicator", get: getThemeIndicator }, - { name: "commandlineSubgroup", get: getCommandlineSubgroup }, - { name: "globalOffsetTop", get: getGlobalOffsetTop }, - { name: "isScreenshotting", get: getIsScreenshotting }, - { name: "userId", get: getUserId }, + { + name: "activePage", + get: getActivePage, + set: setActivePage as (v: unknown) => void, + }, + { + name: "version", + get: getVersion, + set: setVersion as (v: unknown) => void, + }, + { + name: "themeIndicator", + get: getThemeIndicator, + set: setThemeIndicator as (v: unknown) => void, + }, + { + name: "commandlineSubgroup", + get: getCommandlineSubgroup, + set: setCommandlineSubgroup as (v: unknown) => void, + }, + { + name: "globalOffsetTop", + get: getGlobalOffsetTop, + set: setGlobalOffsetTop as (v: unknown) => void, + }, + { + name: "isScreenshotting", + get: getIsScreenshotting, + set: setIsScreenshotting as (v: unknown) => void, + }, + { + name: "userId", + get: getUserId, + set: setUserId as (v: unknown) => void, + }, { name: "isLoggedIn", get: isLoggedIn }, - { name: "selectedProfileName", get: getSelectedProfileName }, + { + name: "selectedProfileName", + get: getSelectedProfileName, + set: setSelectedProfileName as (v: unknown) => void, + }, ], }, { file: "header", signals: [ - { name: "animatedLevel", get: getAnimatedLevel }, - { name: "accountButtonSpinner", get: getAccountButtonSpinner }, - { name: "xpBarData", get: getXpBarData }, + { + name: "animatedLevel", + get: getAnimatedLevel, + set: setAnimatedLevel as (v: unknown) => void, + }, + { + name: "accountButtonSpinner", + get: getAccountButtonSpinner, + set: setAccountButtonSpinner as (v: unknown) => void, + }, + { + name: "xpBarData", + get: getXpBarData, + set: setXpBarData as (v: unknown) => void, + }, ], }, { @@ -94,18 +180,38 @@ const groups: SignalGroup[] = [ file: "leaderboard-selection", signals: [ { name: "selection", get: getSelection }, - { name: "page", get: getPage }, - { name: "goToUserPage", get: getGoToUserPage }, + { name: "page", get: getPage, set: setPage as (v: unknown) => void }, + { + name: "goToUserPage", + get: getGoToUserPage, + set: setGoToUserPage as (v: unknown) => void, + }, ], }, { file: "loader-bar", - signals: [{ name: "loaderBarSignal", get: getLoaderBarSignal }], + signals: [ + { + name: "loaderBarSignal", + get: getLoaderBarSignal, + actions: { + showLoaderBar: () => showLoaderBar(), + hideLoaderBar: () => hideLoaderBar(), + }, + }, + ], }, { file: "login", signals: [ - { name: "loginPageInputsEnabled", get: getLoginPageInputsEnabled }, + { + name: "loginPageInputsEnabled", + get: getLoginPageInputsEnabled, + actions: { + enable: () => enableLoginPageInputs(), + disable: () => disableLoginPageInputs(), + }, + }, ], }, { @@ -147,21 +253,43 @@ const groups: SignalGroup[] = [ file: "snapshot", signals: [ { name: "snapshot", get: getSnapshot }, - { name: "lastResult", get: getLastResult }, + { + name: "lastResult", + get: getLastResult, + set: setLastResult as (v: unknown) => void, + }, ], }, { file: "theme", - signals: [{ name: "theme", get: getTheme }], + signals: [ + { name: "theme", get: getTheme, set: setTheme as (v: unknown) => void }, + ], }, { file: "test", signals: [ - { name: "wordsHaveNewline", get: wordsHaveNewline }, - { name: "wordsHaveTab", get: wordsHaveTab }, - { name: "loadedChallenge", get: getLoadedChallenge }, - { name: "resultVisible", get: getResultVisible }, - { name: "focus", get: getFocus }, + { + name: "wordsHaveNewline", + get: wordsHaveNewline, + set: setWordsHaveNewline as (v: unknown) => void, + }, + { + name: "wordsHaveTab", + get: wordsHaveTab, + set: setWordsHaveTab as (v: unknown) => void, + }, + { + name: "loadedChallenge", + get: getLoadedChallenge, + set: setLoadedChallenge as (v: unknown) => void, + }, + { + name: "resultVisible", + get: getResultVisible, + set: setResultVisible as (v: unknown) => void, + }, + { name: "focus", get: getFocus, set: setFocus as (v: unknown) => void }, ], }, ]; @@ -176,10 +304,30 @@ function formatValue(value: unknown): string { return JSON.stringify(value); } +function parseValue(input: string): unknown { + const trimmed = input.trim(); + if (trimmed === "null") return null; + if (trimmed === "undefined") return undefined; + if (trimmed === "true") return true; + if (trimmed === "false") return false; + const num = Number(trimmed); + if (trimmed !== "" && !Number.isNaN(num)) return num; + try { + return JSON.parse(trimmed) as unknown; + } catch { + return trimmed; + } +} + function SignalRow(props: { signal: SignalEntry }): JSXElement { const [flashing, setFlashing] = createSignal(false); + const [editing, setEditing] = createSignal(false); + const [editValue, setEditValue] = createSignal(""); let initialized = false; + const isEditable = (): boolean => + props.signal.set !== undefined || props.signal.actions !== undefined; + createEffect( on( () => formatValue(props.signal.get()), @@ -194,6 +342,23 @@ function SignalRow(props: { signal: SignalEntry }): JSXElement { ), ); + function startEditing(): void { + if (!isEditable()) return; + setEditValue(formatValue(props.signal.get())); + setEditing(true); + } + + function commitEdit(): void { + if (props.signal.set !== undefined) { + props.signal.set(parseValue(editValue())); + } + setEditing(false); + } + + function cancelEdit(): void { + setEditing(false); + } + return ( - {props.signal.name} - {formatValue(props.signal.get())} + {props.signal.name} + + { + if (typeof props.signal.get() === "boolean") { + props.signal.set!(!props.signal.get()); + } else { + startEditing(); + } + } + : undefined + } + > + {formatValue(props.signal.get())} + + } + > + { + e.preventDefault(); + e.stopImmediatePropagation(); + setEditValue(e.currentTarget.value); + }} + onKeyDown={(e) => { + if (e.key === "Enter") commitEdit(); + if (e.key === "Escape") cancelEdit(); + }} + ref={(el) => setTimeout(() => el.focus())} + class="w-full rounded border px-1 py-0.5 text-xs text-text outline-none focus:border-main" + style={{ + "background-color": "#313749", + "border-color": "#414962", + }} + /> + + + + + + edit + + } + > +
+ + +
+
+
+ +
+ + {([name, fn]) => ( + + )} + +
+
+ ); } +function SignalGroupSection(props: { group: SignalGroup }): JSXElement { + const [collapsed, setCollapsed] = createSignal(false); + + return ( +
+ + + + + + {(signal) => } + + +
+
+
+ ); +} + function SignalsPanel(): JSXElement { const [search, setSearch] = createSignal(""); @@ -249,23 +554,7 @@ function SignalsPanel(): JSXElement {
- {(group) => ( -
-
- {group.file} -
- - - - {(signal) => } - - -
-
- )} + {(group) => }
From 61cc6e5ddea1743d429ade0114c5a4ec31351be7 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 26 Mar 2026 00:53:50 +0100 Subject: [PATCH 3/9] brr --- .../src/ts/components/dev/SignalsDevtools.tsx | 467 ++++-------------- frontend/src/ts/dev/signal-tracker.ts | 153 ++++++ frontend/src/ts/index.ts | 3 + frontend/src/ts/states/modals.ts | 2 +- 4 files changed, 253 insertions(+), 372 deletions(-) create mode 100644 frontend/src/ts/dev/signal-tracker.ts diff --git a/frontend/src/ts/components/dev/SignalsDevtools.tsx b/frontend/src/ts/components/dev/SignalsDevtools.tsx index 3ee8bab5569b..db82bcb11958 100644 --- a/frontend/src/ts/components/dev/SignalsDevtools.tsx +++ b/frontend/src/ts/components/dev/SignalsDevtools.tsx @@ -9,290 +9,28 @@ import { Show, } from "solid-js"; -import { getConfig } from "../../config/store"; -import { getBanners } from "../../states/banners"; -import { bp } from "../../states/breakpoints"; -import { - getActivePage, - setActivePage, - getVersion, - setVersion, - getThemeIndicator, - setThemeIndicator, - getCommandlineSubgroup, - setCommandlineSubgroup, - getGlobalOffsetTop, - setGlobalOffsetTop, - getIsScreenshotting, - setIsScreenshotting, - getUserId, - setUserId, - isLoggedIn, - getSelectedProfileName, - setSelectedProfileName, -} from "../../states/core"; -import { - getAnimatedLevel, - setAnimatedLevel, - getAccountButtonSpinner, - setAccountButtonSpinner, - getXpBarData, - setXpBarData, -} from "../../states/header"; -import { hotkeys } from "../../states/hotkeys"; -import { - getSelection, - getPage, - setPage, - getGoToUserPage, - setGoToUserPage, -} from "../../states/leaderboard-selection"; -import { - getLoaderBarSignal, - showLoaderBar, - hideLoaderBar, -} from "../../states/loader-bar"; -import { - getLoginPageInputsEnabled, - enableLoginPageInputs, - disableLoginPageInputs, -} from "../../states/login"; -import { modalState } from "../../states/modals"; -import { - getNotifications, - getNotificationHistory, -} from "../../states/notifications"; -import { getPsas } from "../../states/psas"; -import { currentQuote, quoteStats } from "../../states/quote-rate"; -import { quoteId } from "../../states/quote-report"; -import { simpleModalConfig } from "../../states/simple-modal"; -import { - getSnapshot, - getLastResult, - setLastResult, -} from "../../states/snapshot"; -import { - wordsHaveNewline, - setWordsHaveNewline, - wordsHaveTab, - setWordsHaveTab, - getLoadedChallenge, - setLoadedChallenge, - getResultVisible, - setResultVisible, - getFocus, - setFocus, -} from "../../states/test"; -import { setTheme, getTheme } from "../../states/theme"; +import { trackedSignals, type TrackedSignal } from "../../dev/signal-tracker"; +import { Balloon } from "../common/Balloon"; -type SignalEntry = { - name: string; - get: () => unknown; - set?: (value: unknown) => void; - actions?: Record void>; -}; -type SignalGroup = { file: string; signals: SignalEntry[] }; +type SignalGroup = { file: string; signals: TrackedSignal[] }; -const groups: SignalGroup[] = [ - { - file: "banners", - signals: [{ name: "banners", get: getBanners }], - }, - { - file: "breakpoints", - signals: [{ name: "bp", get: bp }], - }, - { - file: "config/store", - signals: [{ name: "config", get: () => ({ ...getConfig }) }], - }, - { - file: "core", - signals: [ - { - name: "activePage", - get: getActivePage, - set: setActivePage as (v: unknown) => void, - }, - { - name: "version", - get: getVersion, - set: setVersion as (v: unknown) => void, - }, - { - name: "themeIndicator", - get: getThemeIndicator, - set: setThemeIndicator as (v: unknown) => void, - }, - { - name: "commandlineSubgroup", - get: getCommandlineSubgroup, - set: setCommandlineSubgroup as (v: unknown) => void, - }, - { - name: "globalOffsetTop", - get: getGlobalOffsetTop, - set: setGlobalOffsetTop as (v: unknown) => void, - }, - { - name: "isScreenshotting", - get: getIsScreenshotting, - set: setIsScreenshotting as (v: unknown) => void, - }, - { - name: "userId", - get: getUserId, - set: setUserId as (v: unknown) => void, - }, - { name: "isLoggedIn", get: isLoggedIn }, - { - name: "selectedProfileName", - get: getSelectedProfileName, - set: setSelectedProfileName as (v: unknown) => void, - }, - ], - }, - { - file: "header", - signals: [ - { - name: "animatedLevel", - get: getAnimatedLevel, - set: setAnimatedLevel as (v: unknown) => void, - }, - { - name: "accountButtonSpinner", - get: getAccountButtonSpinner, - set: setAccountButtonSpinner as (v: unknown) => void, - }, - { - name: "xpBarData", - get: getXpBarData, - set: setXpBarData as (v: unknown) => void, - }, - ], - }, - { - file: "hotkeys", - signals: [{ name: "hotkeys", get: () => ({ ...hotkeys }) }], - }, - { - file: "leaderboard-selection", - signals: [ - { name: "selection", get: getSelection }, - { name: "page", get: getPage, set: setPage as (v: unknown) => void }, - { - name: "goToUserPage", - get: getGoToUserPage, - set: setGoToUserPage as (v: unknown) => void, - }, - ], - }, - { - file: "loader-bar", - signals: [ - { - name: "loaderBarSignal", - get: getLoaderBarSignal, - actions: { - showLoaderBar: () => showLoaderBar(), - hideLoaderBar: () => hideLoaderBar(), - }, - }, - ], - }, - { - file: "login", - signals: [ - { - name: "loginPageInputsEnabled", - get: getLoginPageInputsEnabled, - actions: { - enable: () => enableLoginPageInputs(), - disable: () => disableLoginPageInputs(), - }, - }, - ], - }, - { - file: "modals", - signals: [ - { name: "openModals", get: () => ({ ...modalState.openModals }) }, - { name: "modalStack", get: () => [...modalState.modalStack] }, - { name: "pendingModal", get: () => modalState.pendingModal }, - { name: "pendingIsChained", get: () => modalState.pendingIsChained }, - ], - }, - { - file: "notifications", - signals: [ - { name: "notifications", get: getNotifications }, - { name: "notificationHistory", get: getNotificationHistory }, - ], - }, - { - file: "psas", - signals: [{ name: "psas", get: getPsas }], - }, - { - file: "quote-rate", - signals: [ - { name: "currentQuote", get: currentQuote }, - { name: "quoteStats", get: quoteStats }, - ], - }, - { - file: "quote-report", - signals: [{ name: "quoteId", get: quoteId }], - }, - { - file: "simple-modal", - signals: [{ name: "simpleModalConfig", get: simpleModalConfig }], - }, - { - file: "snapshot", - signals: [ - { name: "snapshot", get: getSnapshot }, - { - name: "lastResult", - get: getLastResult, - set: setLastResult as (v: unknown) => void, - }, - ], - }, - { - file: "theme", - signals: [ - { name: "theme", get: getTheme, set: setTheme as (v: unknown) => void }, - ], - }, - { - file: "test", - signals: [ - { - name: "wordsHaveNewline", - get: wordsHaveNewline, - set: setWordsHaveNewline as (v: unknown) => void, - }, - { - name: "wordsHaveTab", - get: wordsHaveTab, - set: setWordsHaveTab as (v: unknown) => void, - }, - { - name: "loadedChallenge", - get: getLoadedChallenge, - set: setLoadedChallenge as (v: unknown) => void, - }, - { - name: "resultVisible", - get: getResultVisible, - set: setResultVisible as (v: unknown) => void, - }, - { name: "focus", get: getFocus, set: setFocus as (v: unknown) => void }, - ], - }, -]; +function buildGroups(): SignalGroup[] { + const groupMap = new Map(); + + for (const s of trackedSignals) { + // extract filename from source path (e.g. "/ts/states/core.ts:4:44" -> "states/core.ts") + const match = /\/ts\/(.+?)(?::\d+)*(?:\)?)$/.exec(s.source); + const group = match?.[1] ?? (s.source !== "" ? s.source : s.owner); + const entries = groupMap.get(group) ?? []; + entries.push(s); + groupMap.set(group, entries); + } + + return Array.from(groupMap.entries()).map(([file, signals]) => ({ + file, + signals, + })); +} function formatValue(value: unknown): string { if (value === null) return "null"; @@ -301,7 +39,11 @@ function formatValue(value: unknown): string { if (typeof value === "number" || typeof value === "boolean") { return `${value}`; } - return JSON.stringify(value); + try { + return JSON.stringify(value); + } catch { + return `[${typeof value}]`; + } } function parseValue(input: string): unknown { @@ -319,15 +61,12 @@ function parseValue(input: string): unknown { } } -function SignalRow(props: { signal: SignalEntry }): JSXElement { +function SignalRow(props: { signal: TrackedSignal }): JSXElement { const [flashing, setFlashing] = createSignal(false); const [editing, setEditing] = createSignal(false); const [editValue, setEditValue] = createSignal(""); let initialized = false; - const isEditable = (): boolean => - props.signal.set !== undefined || props.signal.actions !== undefined; - createEffect( on( () => formatValue(props.signal.get()), @@ -342,22 +81,19 @@ function SignalRow(props: { signal: SignalEntry }): JSXElement { ), ); - function startEditing(): void { - if (!isEditable()) return; + const startEditing = (): void => { setEditValue(formatValue(props.signal.get())); setEditing(true); - } + }; - function commitEdit(): void { - if (props.signal.set !== undefined) { - props.signal.set(parseValue(editValue())); - } + const commitEdit = (): void => { + props.signal.set(parseValue(editValue())); setEditing(false); - } + }; - function cancelEdit(): void { + const cancelEdit = (): void => { setEditing(false); - } + }; return ( - {props.signal.name} + + {props.signal.name} + + + ? + + + { - if (typeof props.signal.get() === "boolean") { - props.signal.set!(!props.signal.get()); - } else { - startEditing(); - } - } - : undefined - } + class="cursor-pointer hover:underline" + onClick={() => { + const current = props.signal.get(); + if (typeof current === "boolean") { + props.signal.set(!current); + } else { + startEditing(); + } + }} > {formatValue(props.signal.get())} @@ -416,62 +158,41 @@ function SignalRow(props: { signal: SignalEntry }): JSXElement { - - - edit - - } - > -
- - -
-
-
- -
- - {([name, fn]) => ( - - )} - + + edit + + } + > +
+ +
@@ -499,6 +220,9 @@ function SignalGroupSection(props: { group: SignalGroup }): JSXElement { ▼ {props.group.file} + + {props.group.signals.length} + @@ -515,6 +239,7 @@ function SignalGroupSection(props: { group: SignalGroup }): JSXElement { function SignalsPanel(): JSXElement { const [search, setSearch] = createSignal(""); + const groups = buildGroups(); const filteredGroups = (): SignalGroup[] => { const query = search().toLowerCase(); @@ -564,7 +289,7 @@ function SignalsPanel(): JSXElement { export function SignalsDevtoolsPlugin(): TanStackDevtoolsSolidPlugin { return { id: "core-signals", - name: "Core Signals", + name: "Signals", render: () => , }; } diff --git a/frontend/src/ts/dev/signal-tracker.ts b/frontend/src/ts/dev/signal-tracker.ts new file mode 100644 index 000000000000..bdac8947e9f1 --- /dev/null +++ b/frontend/src/ts/dev/signal-tracker.ts @@ -0,0 +1,153 @@ +import { createSignal, DEV } from "solid-js"; + +export type TrackedSignal = { + name: string; + type: "signal" | "store"; + owner: string; + ownerChain: string; + source: string; + initialValue: string; + get: () => unknown; + set: (v: unknown) => void; + getObserverCount: () => number; +}; + +export const trackedSignals: TrackedSignal[] = []; + +/** Internal Solid signal/store node shape (subset used by devtools) */ +type SolidNode = { + name?: string; + value?: unknown; + observers?: unknown[]; + fn?: unknown; + graph?: SolidOwner; +}; + +type SolidOwner = { + name?: string; + component?: { name?: string }; + owner?: SolidOwner | null; +}; + +function getCallerInfo(): { isUserCode: boolean; source: string } { + const stack = new Error().stack; + if (stack === undefined) return { isUserCode: false, source: "" }; + const frames = stack.split("\n").slice(1); + for (const frame of frames) { + if (frame.includes("signal-tracker")) continue; + if (frame.includes("solid-js")) continue; + const isUserCode = !frame.includes("node_modules"); + const urlMatch = /https?:\/\/[^/]+(\/[^?)]+)(?:\?[^:]*)?(:[\d:]+)/.exec( + frame, + ); + const source = + urlMatch !== null ? `${urlMatch[1]}${urlMatch[2]}` : frame.trim(); + return { isUserCode, source }; + } + return { isUserCode: false, source: "" }; +} + +function getOwnerChain(node: SolidNode): string { + const names: string[] = []; + let owner: SolidOwner | undefined | null = node.graph; + while (owner !== undefined && owner !== null) { + const ownerName = owner.name ?? owner.component?.name; + if (ownerName !== undefined) names.push(ownerName); + owner = owner.owner; + } + return names.join(" > "); +} + +function getOwnerName(node: SolidNode): string { + return node.graph?.name ?? node.graph?.component?.name ?? "unknown"; +} + +function getNodeName(node: SolidNode): string | undefined { + return node.name ?? node.graph?.name ?? node.graph?.component?.name; +} + +function formatInitialValue(value: unknown): string { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") return `"${value}"`; + if (typeof value === "number" || typeof value === "boolean") { + return `${value}`; + } + try { + const str = JSON.stringify(value); + return str.length > 80 ? `${str.slice(0, 80)}...` : str; + } catch { + return `[${typeof value}]`; + } +} + +if (DEV) { + type NodeInfo = { + name: string; + type: "signal" | "store"; + source: string; + ownerChain: string; + initialValue: string; + }; + const pendingNodes = new Map(); + + const writeSignal = DEV.writeSignal as ( + node: SolidNode, + value: unknown, + ) => unknown; + + DEV.hooks.afterRegisterGraph = (rawNode: object) => { + const node = rawNode as SolidNode; + const isSignal = "observers" in node; + const isStore = !isSignal && "value" in node && !("fn" in node); + if (!isSignal && !isStore) return; + + // cheap check: must have a name + const name = getNodeName(node); + if (name === undefined) return; + + // expensive check: stack trace for user code filtering + const { isUserCode, source } = getCallerInfo(); + if (!isUserCode) return; + + pendingNodes.set(node, { + name, + type: isSignal ? "signal" : "store", + source, + ownerChain: getOwnerChain(node), + initialValue: formatInitialValue(node.value), + }); + }; + + queueMicrotask(() => { + const mirrors: { node: SolidNode; set: (v: unknown) => void }[] = []; + + for (const [node, info] of pendingNodes) { + const [get, set] = createSignal(node.value); + + trackedSignals.push({ + name: info.name, + type: info.type, + owner: getOwnerName(node), + ownerChain: info.ownerChain, + source: info.source, + initialValue: info.initialValue, + get, + set: (v: unknown) => writeSignal(node, v), + getObserverCount: () => node.observers?.length ?? 0, + }); + + mirrors.push({ node, set }); + } + pendingNodes.clear(); + + // Sync node values to reactive mirrors, only when tab is visible + const syncLoop = (): void => { + for (const mirror of mirrors) { + mirror.set(() => mirror.node.value); + } + requestAnimationFrame(syncLoop); + }; + requestAnimationFrame(syncLoop); + }); +} diff --git a/frontend/src/ts/index.ts b/frontend/src/ts/index.ts index d6b4a96dbe5a..7cc56fdd05f4 100644 --- a/frontend/src/ts/index.ts +++ b/frontend/src/ts/index.ts @@ -1,3 +1,6 @@ +// register signal tracking hook before any signals are created +import "./dev/signal-tracker"; + //enable solidjs-devtools import "solid-devtools"; diff --git a/frontend/src/ts/states/modals.ts b/frontend/src/ts/states/modals.ts index 2eb936cc8005..fa2fac25505e 100644 --- a/frontend/src/ts/states/modals.ts +++ b/frontend/src/ts/states/modals.ts @@ -37,7 +37,7 @@ type ModalState = { pendingIsChained: boolean; }; -export const [modalState, setModalState] = createStore({ +const [modalState, setModalState] = createStore({ openModals: {}, modalStack: [], pendingModal: null, From 1c2938db5d0d29d02caf327ddc43a0b4b022cc66 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 26 Mar 2026 09:22:53 +0100 Subject: [PATCH 4/9] impr --- .../src/ts/components/dev/SignalsDevtools.tsx | 137 ++++++++++-------- 1 file changed, 76 insertions(+), 61 deletions(-) diff --git a/frontend/src/ts/components/dev/SignalsDevtools.tsx b/frontend/src/ts/components/dev/SignalsDevtools.tsx index db82bcb11958..0cab13fb3a6f 100644 --- a/frontend/src/ts/components/dev/SignalsDevtools.tsx +++ b/frontend/src/ts/components/dev/SignalsDevtools.tsx @@ -10,6 +10,7 @@ import { } from "solid-js"; import { trackedSignals, type TrackedSignal } from "../../dev/signal-tracker"; +import { cn } from "../../utils/cn"; import { Balloon } from "../common/Balloon"; type SignalGroup = { file: string; signals: TrackedSignal[] }; @@ -104,25 +105,69 @@ function SignalRow(props: { signal: TrackedSignal }): JSXElement { }} > + - - + {/* */} ); } @@ -256,11 +265,17 @@ function SignalsPanel(): JSXElement { }; return ( -
-
+
+
Date: Thu, 26 Mar 2026 20:16:56 +0100 Subject: [PATCH 5/9] group by usage not by source --- frontend/src/ts/dev/signal-tracker.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/dev/signal-tracker.ts b/frontend/src/ts/dev/signal-tracker.ts index bdac8947e9f1..f495e559a9a4 100644 --- a/frontend/src/ts/dev/signal-tracker.ts +++ b/frontend/src/ts/dev/signal-tracker.ts @@ -33,15 +33,24 @@ function getCallerInfo(): { isUserCode: boolean; source: string } { const stack = new Error().stack; if (stack === undefined) return { isUserCode: false, source: "" }; const frames = stack.split("\n").slice(1); - for (const frame of frames) { + + if (frames.some((f) => f.includes("useRefWithUtils"))) { + return { isUserCode: false, source: "" }; + } + + for (const frame of frames.toReversed()) { if (frame.includes("signal-tracker")) continue; if (frame.includes("solid-js")) continue; + if (frame.includes("@solid-refresh")) continue; const isUserCode = !frame.includes("node_modules"); const urlMatch = /https?:\/\/[^/]+(\/[^?)]+)(?:\?[^:]*)?(:[\d:]+)/.exec( frame, ); const source = urlMatch !== null ? `${urlMatch[1]}${urlMatch[2]}` : frame.trim(); + if (source.includes("AnimePresence")) { + console.log(source, frames); + } return { isUserCode, source }; } return { isUserCode: false, source: "" }; From dd0a75806eb60f7bf960049e5682303b4c672685 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 26 Mar 2026 20:28:41 +0100 Subject: [PATCH 6/9] fix searching not working on test page --- frontend/src/ts/components/dev/SignalsDevtools.tsx | 1 + frontend/src/ts/event-handlers/global.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/frontend/src/ts/components/dev/SignalsDevtools.tsx b/frontend/src/ts/components/dev/SignalsDevtools.tsx index 0cab13fb3a6f..86ac5dfed0a8 100644 --- a/frontend/src/ts/components/dev/SignalsDevtools.tsx +++ b/frontend/src/ts/components/dev/SignalsDevtools.tsx @@ -286,6 +286,7 @@ function SignalsPanel(): JSXElement { setSearch(e.currentTarget.value); }} class="mb-3 w-full rounded border border-sub bg-bg px-2 py-1 text-xs text-text outline-none placeholder:text-[#6f748d] focus:border-main" + data-ui-element="signalDevtoolsInput" style={{ "background-color": "#313749", "border-color": "#414962", diff --git a/frontend/src/ts/event-handlers/global.ts b/frontend/src/ts/event-handlers/global.ts index a4bec8b27c3f..907e6be695b0 100644 --- a/frontend/src/ts/event-handlers/global.ts +++ b/frontend/src/ts/event-handlers/global.ts @@ -13,6 +13,16 @@ document.addEventListener("keydown", (e) => { if (PageTransition.get()) return; if (e.key === undefined) return; + if (isDevEnvironment()) { + if ( + (document.activeElement as HTMLElement | undefined)?.dataset[ + "uiElement" + ] === "signalDevtoolsInput" + ) { + return; + } + } + const pageTestActive: boolean = getActivePage() === "test"; if (pageTestActive && !TestState.resultVisible && !isInputElementFocused()) { const popupVisible: boolean = Misc.isAnyPopupVisible(); From 3cf03326e6d2509d4e4a2a7a0e9dd27b8e2b92d5 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 26 Mar 2026 20:32:26 +0100 Subject: [PATCH 7/9] buttons --- frontend/src/ts/components/dev/SignalsDevtools.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/components/dev/SignalsDevtools.tsx b/frontend/src/ts/components/dev/SignalsDevtools.tsx index 86ac5dfed0a8..9a40502a60e0 100644 --- a/frontend/src/ts/components/dev/SignalsDevtools.tsx +++ b/frontend/src/ts/components/dev/SignalsDevtools.tsx @@ -120,8 +120,8 @@ function SignalRow(props: { signal: TrackedSignal }): JSXElement {
-
- {props.signal.name} - - - ? - - +
+ {props.signal.name} + + + ? + + +
+
+
+ +
+ + + } + > + + +
+
+ { const current = props.signal.get(); if (typeof current === "boolean") { @@ -133,7 +178,7 @@ function SignalRow(props: { signal: TrackedSignal }): JSXElement { }} > {formatValue(props.signal.get())} - + } > - - edit - - } - > -
- - -
-
-
+ +
-
+
+