@@ -43,7 +43,7 @@ import { Icon, href as iconsHref } from './components/ui/icon.tsx'
43
43
import fontStylestylesheetUrl from './styles/font.css'
44
44
import tailwindStylesheetUrl from './styles/tailwind.css'
45
45
import { authenticator , getUserId } from './utils/auth.server.ts'
46
- import { ClientHintCheck , getHints } from './utils/client-hints.tsx'
46
+ import { ClientHintCheck , getHints , useHints } from './utils/client-hints.tsx'
47
47
import { getConfetti } from './utils/confetti.server.ts'
48
48
import { prisma } from './utils/db.server.ts'
49
49
import { getEnv } from './utils/env.server.ts'
@@ -54,6 +54,7 @@ import {
54
54
invariantResponse ,
55
55
} from './utils/misc.tsx'
56
56
import { useNonce } from './utils/nonce-provider.ts'
57
+ import { useRequestInfo } from './utils/request-info.ts'
57
58
import { type Theme , setTheme , getTheme } from './utils/theme.server.ts'
58
59
import { makeTimings , time } from './utils/timing.server.ts'
59
60
import { getToast } from './utils/toast.server.ts'
@@ -167,7 +168,7 @@ export const headers: HeadersFunction = ({ loaderHeaders }) => {
167
168
}
168
169
169
170
const ThemeFormSchema = z . object ( {
170
- theme : z . enum ( [ 'light' , 'dark' ] ) ,
171
+ theme : z . enum ( [ 'system' , ' light', 'dark' ] ) ,
171
172
} )
172
173
173
174
export async function action ( { request } : DataFunctionArgs ) {
@@ -202,7 +203,7 @@ function Document({
202
203
} : {
203
204
children : React . ReactNode
204
205
nonce : string
205
- theme ?: 'dark' | 'light'
206
+ theme ?: Theme
206
207
env ?: Record < string , string >
207
208
} ) {
208
209
return (
@@ -344,20 +345,40 @@ function UserDropdown() {
344
345
)
345
346
}
346
347
347
- function useTheme ( ) {
348
- const data = useLoaderData < typeof loader > ( )
348
+ /**
349
+ * @returns the user's theme preference, or the client hint theme if the user
350
+ * has not set a preference.
351
+ */
352
+ export function useTheme ( ) {
353
+ const hints = useHints ( )
354
+ const requestInfo = useRequestInfo ( )
355
+ const optimisticMode = useOptimisticThemeMode ( )
356
+ if ( optimisticMode ) {
357
+ return optimisticMode === 'system' ? hints . theme : optimisticMode
358
+ }
359
+ return requestInfo . userPrefs . theme ?? hints . theme
360
+ }
361
+
362
+ /**
363
+ * If the user's changing their theme mode preference, this will return the
364
+ * value it's being changed to.
365
+ */
366
+ export function useOptimisticThemeMode ( ) {
349
367
const fetchers = useFetchers ( )
368
+
350
369
const themeFetcher = fetchers . find (
351
370
f => f . formData ?. get ( 'intent' ) === 'update-theme' ,
352
371
)
353
- const optimisticTheme = themeFetcher ?. formData ?. get ( 'theme' )
354
- if ( optimisticTheme === 'light' || optimisticTheme === 'dark' ) {
355
- return optimisticTheme
372
+
373
+ if ( themeFetcher && themeFetcher . formData ) {
374
+ const submission = parse ( themeFetcher . formData , {
375
+ schema : ThemeFormSchema ,
376
+ } )
377
+ return submission . value ?. theme
356
378
}
357
- return data . requestInfo . userPrefs . theme
358
379
}
359
380
360
- function ThemeSwitch ( { userPreference } : { userPreference ?: Theme } ) {
381
+ function ThemeSwitch ( { userPreference } : { userPreference ?: Theme | null } ) {
361
382
const fetcher = useFetcher < typeof action > ( )
362
383
363
384
const [ form ] = useForm ( {
@@ -368,8 +389,10 @@ function ThemeSwitch({ userPreference }: { userPreference?: Theme }) {
368
389
} ,
369
390
} )
370
391
371
- const mode = userPreference ?? 'light'
372
- const nextMode = mode === 'light' ? 'dark' : 'light'
392
+ const optimisticMode = useOptimisticThemeMode ( )
393
+ const mode = optimisticMode ?? userPreference ?? 'system'
394
+ const nextMode =
395
+ mode === 'system' ? 'light' : mode === 'light' ? 'dark' : 'system'
373
396
const modeLabel = {
374
397
light : (
375
398
< Icon name = "sun" >
@@ -381,6 +404,11 @@ function ThemeSwitch({ userPreference }: { userPreference?: Theme }) {
381
404
< span className = "sr-only" > Dark</ span >
382
405
</ Icon >
383
406
) ,
407
+ system : (
408
+ < Icon name = "laptop" >
409
+ < span className = "sr-only" > System</ span >
410
+ </ Icon >
411
+ ) ,
384
412
}
385
413
386
414
return (
0 commit comments