From 29cc1392405283a0015aca318b1d06af5757c807 Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 11:58:33 +0800 Subject: [PATCH 1/4] Create useDarkMode.ts --- .../template/src/hooks/useDarkMode.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 packages/cra-template-typescript/template/src/hooks/useDarkMode.ts diff --git a/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts b/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts new file mode 100644 index 00000000000..872b1e4ad54 --- /dev/null +++ b/packages/cra-template-typescript/template/src/hooks/useDarkMode.ts @@ -0,0 +1,78 @@ +import { useEffect, useState } from 'react' + +type Theme = 'light' | 'dark' + +const STORAGE_KEY = 'theme' +const MEDIA_QUERY = '(prefers-color-scheme: dark)' + +function getStoredTheme(): Theme | null { + try { + const value = window.localStorage.getItem(STORAGE_KEY) + return value === 'light' || value === 'dark' ? value : null + } catch { + return null + } +} + +function getSystemTheme(): Theme { + return window.matchMedia(MEDIA_QUERY).matches ? 'dark' : 'light' +} + +export function useDarkMode() { + const [theme, setThemeState] = useState(() => { + if (typeof window === 'undefined') { + return 'light' + } + + return getStoredTheme() ?? getSystemTheme() + }) + + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme) + + try { + window.localStorage.setItem(STORAGE_KEY, theme) + } catch { + // ignore storage errors + } + }, [theme]) + + useEffect(() => { + if (typeof window === 'undefined') { + return + } + + const mediaQuery = window.matchMedia(MEDIA_QUERY) + + const handleChange = (event: MediaQueryListEvent) => { + const storedTheme = getStoredTheme() + + if (!storedTheme) { + setThemeState(event.matches ? 'dark' : 'light') + } + } + + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', handleChange) + return () => mediaQuery.removeEventListener('change', handleChange) + } + + mediaQuery.addListener(handleChange) + return () => mediaQuery.removeListener(handleChange) + }, []) + + const setTheme = (nextTheme: Theme) => { + setThemeState(nextTheme) + } + + const toggleTheme = () => { + setThemeState(prev => (prev === 'light' ? 'dark' : 'light')) + } + + return { + theme, + isDark: theme === 'dark', + setTheme, + toggleTheme + } +} From fec723088cccc1c35c1560df97f7e18ced53c762 Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 12:28:42 +0800 Subject: [PATCH 2/4] Create uDarkMode.ts --- .../template/src/hooks/uDarkMode.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 packages/cra-template-typescript/template/src/hooks/uDarkMode.ts diff --git a/packages/cra-template-typescript/template/src/hooks/uDarkMode.ts b/packages/cra-template-typescript/template/src/hooks/uDarkMode.ts new file mode 100644 index 00000000000..d3d844f6ade --- /dev/null +++ b/packages/cra-template-typescript/template/src/hooks/uDarkMode.ts @@ -0,0 +1,62 @@ +import { useState, useEffect, useCallback } from 'react' + +type Theme = 'light' | 'dark' + +const STORAGE_KEY = 'theme' + +function getInitialTheme(): Theme { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored === 'light' || stored === 'dark') { + return stored + } + + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + return 'dark' + } + + return 'light' +} + +export function useDarkMode() { + const [theme, setThemeState] = useState(() => getInitialTheme()) + + useEffect(() => { + const root = document.documentElement + root.setAttribute('data-theme', theme) + localStorage.setItem(STORAGE_KEY, theme) + }, [theme]) + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + + const handleChange = (e: MediaQueryListEvent) => { + const stored = localStorage.getItem(STORAGE_KEY) + if (!stored) { + setThemeState(e.matches ? 'dark' : 'light') + } + } + + mediaQuery.addEventListener('change', handleChange) + + return () => { + mediaQuery.removeEventListener('change', handleChange) + } + }, []) + + const toggle = useCallback(() => { + setThemeState(prev => (prev === 'light' ? 'dark' : 'light')) + }, []) + + const setTheme = useCallback((theme: Theme) => { + setThemeState(theme) + }, []) + + return { + theme, + isDark: theme === 'dark', + toggle, + setTheme + } +} + +export default useDarkMode From cad2a6de99b672a875808440b79eaea166bb97f1 Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 12:29:26 +0800 Subject: [PATCH 3/4] Create Header.tsx --- .../template/src/components/Header.tsx | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 packages/cra-template-typescript/template/src/components/Header.tsx diff --git a/packages/cra-template-typescript/template/src/components/Header.tsx b/packages/cra-template-typescript/template/src/components/Header.tsx new file mode 100644 index 00000000000..7e6777e2af7 --- /dev/null +++ b/packages/cra-template-typescript/template/src/components/Header.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { useDarkMode } from '../hooks/useDarkMode' + +const Header: React.FC = () => { + const { isDark, toggle } = useDarkMode() + + return ( +
+ +
+ ) +} + +export default Header From 5b3959a0af724c395409423f8d4c8c0fa193deea Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 12:30:02 +0800 Subject: [PATCH 4/4] Update App.tsx --- .../cra-template-typescript/template/src/App.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/cra-template-typescript/template/src/App.tsx b/packages/cra-template-typescript/template/src/App.tsx index a53698aab3c..775e7ab4c34 100644 --- a/packages/cra-template-typescript/template/src/App.tsx +++ b/packages/cra-template-typescript/template/src/App.tsx @@ -1,15 +1,20 @@ -import React from 'react'; -import logo from './logo.svg'; -import './App.css'; +import React from 'react' +import logo from './logo.svg' +import './App.css' +import Header from './components/Header' function App() { return (
+
+
logo +

Edit src/App.tsx and save to reload.

+
- ); + ) } -export default App; +export default App