From 10dbca8177e0c4257b53246071b3061b2a8d0ae9 Mon Sep 17 00:00:00 2001 From: Pavel Date: Sat, 26 Sep 2026 14:18:15 +0400 Subject: [PATCH] Fix logo --- frontend/src/hooks/index.ts | 20 +++++++- frontend/src/hooks/useDarkTheme.test.tsx | 63 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 frontend/src/hooks/useDarkTheme.test.tsx diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts index de695f5d..eab5fabe 100644 --- a/frontend/src/hooks/index.ts +++ b/frontend/src/hooks/index.ts @@ -95,6 +95,11 @@ export function usePacedText(text: string, enabled: boolean): string { return enabled ? text.slice(0, Math.min(visibleLength, text.length)) : text; } +// Each useDarkTheme() caller holds its own state; a toggle broadcasts the new +// value so every other caller (logo, Mermaid, code blocks) follows without a +// reload. +const THEME_CHANGE_EVENT = 'docsgpt:themechange'; + export function useDarkTheme() { const getSystemThemePreference = () => { return ( @@ -126,6 +131,15 @@ export function useDarkTheme() { return () => mediaQuery.removeListener(handleChange); }, []); + useEffect(() => { + const handleThemeChange = (event: Event) => { + setIsDarkTheme((event as CustomEvent).detail); + }; + window.addEventListener(THEME_CHANGE_EVENT, handleThemeChange); + return () => + window.removeEventListener(THEME_CHANGE_EVENT, handleThemeChange); + }, []); + useEffect(() => { localStorage.setItem('selectedTheme', isDarkTheme ? 'Dark' : 'Light'); const action = isDarkTheme ? 'add' : 'remove'; @@ -148,7 +162,11 @@ export function useDarkTheme() { }, [isDarkTheme]); const toggleTheme = () => { - setIsDarkTheme(!isDarkTheme); + const next = !isDarkTheme; + setIsDarkTheme(next); + window.dispatchEvent( + new CustomEvent(THEME_CHANGE_EVENT, { detail: next }), + ); }; return [isDarkTheme, toggleTheme, componentMounted] as const; diff --git a/frontend/src/hooks/useDarkTheme.test.tsx b/frontend/src/hooks/useDarkTheme.test.tsx new file mode 100644 index 00000000..0866b625 --- /dev/null +++ b/frontend/src/hooks/useDarkTheme.test.tsx @@ -0,0 +1,63 @@ +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useDarkTheme } from './index'; + +Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); + +describe('useDarkTheme', () => { + let container: HTMLDivElement; + + beforeEach(() => { + localStorage.setItem('selectedTheme', 'Light'); + vi.stubGlobal('matchMedia', (query: string) => ({ + matches: false, + media: query, + addListener: () => {}, + removeListener: () => {}, + })); + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + localStorage.clear(); + vi.unstubAllGlobals(); + }); + + it('updates every consumer when one of them toggles the theme', async () => { + let toggle: (() => void) | undefined; + let logoIsDark: boolean | undefined; + + function Settings() { + const [, toggleTheme] = useDarkTheme(); + toggle = toggleTheme; + return null; + } + function Logo() { + [logoIsDark] = useDarkTheme(); + return null; + } + + const root = createRoot(container); + await act(async () => { + root.render( + <> + + + , + ); + }); + expect(logoIsDark).toBe(false); + + await act(async () => toggle?.()); + expect(logoIsDark).toBe(true); + + await act(async () => toggle?.()); + expect(logoIsDark).toBe(false); + + await act(async () => root.unmount()); + }); +});