From 29cc1392405283a0015aca318b1d06af5757c807 Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 11:58:33 +0800 Subject: [PATCH 1/5] 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 e53a8d6e758643aae0b8353d24ed168a62f54f70 Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 12:17:08 +0800 Subject: [PATCH 2/5] Create usDarkMode.ts --- .../template/src/hooks/usDarkMode.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/cra-template-typescript/template/src/hooks/usDarkMode.ts diff --git a/packages/cra-template-typescript/template/src/hooks/usDarkMode.ts b/packages/cra-template-typescript/template/src/hooks/usDarkMode.ts new file mode 100644 index 00000000000..82a487fe744 --- /dev/null +++ b/packages/cra-template-typescript/template/src/hooks/usDarkMode.ts @@ -0,0 +1,71 @@ +import { useState, useEffect, useCallback } from 'react' + +type Theme = 'light' | 'dark' + +const STORAGE_KEY = 'theme-preference' + +function getSystemTheme(): Theme { + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' +} + +function getStoredTheme(): Theme | null { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored === 'light' || stored === 'dark') { + return stored + } + return null +} + +function applyTheme(theme: Theme) { + document.documentElement.setAttribute('data-theme', theme) +} + +export function useDarkMode() { + const [theme, setThemeState] = useState(() => { + const stored = getStoredTheme() + return stored ?? getSystemTheme() + }) + + useEffect(() => { + applyTheme(theme) + localStorage.setItem(STORAGE_KEY, theme) + }, [theme]) + + useEffect(() => { + const stored = getStoredTheme() + if (stored !== null) return + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + + const handler = (e: MediaQueryListEvent) => { + setThemeState(e.matches ? 'dark' : 'light') + } + + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener('change', handler) + return () => mediaQuery.removeEventListener('change', handler) + } + + mediaQuery.addListener(handler) + return () => mediaQuery.removeListener(handler) + }, []) + + const toggleTheme = useCallback(() => { + setThemeState(prev => (prev === 'light' ? 'dark' : 'light')) + }, []) + + const setTheme = useCallback((theme: Theme) => { + setThemeState(theme) + }, []) + + return { + theme, + isDark: theme === 'dark', + toggleTheme, + setTheme + } +} + +export default useDarkMode From 4ad2d68ed18cd06a8d34a9ae6972c99f1744bb49 Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 12:18:44 +0800 Subject: [PATCH 3/5] Create Header.tsx --- .../template/src/components/Header.tsx | 30 +++++++++++++++++++ 1 file changed, 30 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..c5f36160ae8 --- /dev/null +++ b/packages/cra-template-typescript/template/src/components/Header.tsx @@ -0,0 +1,30 @@ +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. +

+ + + Learn React + +
+
+ ) +} + +export default App From c0a474df2d6eb744ca2872f56f849fdef3cb6285 Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 12:19:21 +0800 Subject: [PATCH 4/5] Update App.tsx --- .../cra-template-typescript/template/src/App.tsx | 14 +++++++++----- 1 file changed, 9 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..c5f36160ae8 100644 --- a/packages/cra-template-typescript/template/src/App.tsx +++ b/packages/cra-template-typescript/template/src/App.tsx @@ -1,15 +1,19 @@ -import React from 'react'; -import logo from './logo.svg'; -import './App.css'; +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 From 9259d03efa0b4c5bdc6917903a6ab67513d1e188 Mon Sep 17 00:00:00 2001 From: Un7corn Date: Thu, 12 Mar 2026 12:19:45 +0800 Subject: [PATCH 5/5] Update index.css --- .../template/src/index.css | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/cra-template-typescript/template/src/index.css b/packages/cra-template-typescript/template/src/index.css index ec2585e8c0b..a9f17f065c8 100644 --- a/packages/cra-template-typescript/template/src/index.css +++ b/packages/cra-template-typescript/template/src/index.css @@ -1,13 +1,73 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', + 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', + 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + + background-color: #ffffff; + color: #333333; + + transition: background-color 0.3s ease, color 0.3s ease; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: source-code-pro, Menlo, Monaco, Consolas, + 'Courier New', monospace; +} + +body[data-theme='dark'] { + background-color: #1a1a2e; + color: #eaeaea; +} + +.app-header { + padding: 12px 24px; + background-color: #f8f9fa; + border-bottom: 1px solid #e9ecef; + transition: background-color 0.3s ease; +} + +body[data-theme='dark'] .app-header { + background-color: #16213e; +} + +.header-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.header-title { + font-size: 1.25rem; + font-weight: 600; +} + +.theme-toggle { + background: none; + border: 2px solid #dee2e6; + border-radius: 50%; + width: 40px; + height: 40px; + + cursor: pointer; + + font-size: 1.1rem; + + display: flex; + align-items: center; + justify-content: center; + + transition: border-color 0.2s ease, transform 0.2s ease; +} + +.theme-toggle:hover { + border-color: #61dafb; + transform: scale(1.05); +} + +body[data-theme='dark'] .theme-toggle { + border-color: #495057; }