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 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 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 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 + } +}