Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 2 additions & 24 deletions src/context/SettingsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useLocalStorage } from '../hooks/useLocalStorage'
import { useThemeDetector, type ThemeMode } from '../hooks/useThemeDetector'
import { QUIET_HOURS_DEFAULTS, parseHHmm } from '../lib/quietHours'

type ThemeMode = 'light' | 'dark' | 'system'
/** Network option literal union */
export type NetworkOption = 'public' | 'test'
/** Address display option literal union */
Expand Down Expand Up @@ -258,28 +257,7 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) {
}

// Apply theme to document and keep it in sync with the system preference.
useEffect(() => {
if (typeof window === 'undefined') return
const root = window.document.documentElement

const apply = () => {
if (themeMode === 'system') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
root.setAttribute('data-theme', isDark ? 'dark' : 'light')
} else {
root.setAttribute('data-theme', themeMode)
}
}

apply()

if (themeMode !== 'system') return

const mql = window.matchMedia('(prefers-color-scheme: dark)')
const handler = () => apply()
mql.addEventListener?.('change', handler)
return () => mql.removeEventListener?.('change', handler)
}, [themeMode])
useThemeDetector(themeMode)

const value: SettingsState = {
themeMode,
Expand Down
53 changes: 53 additions & 0 deletions src/hooks/useThemeDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useEffect } from 'react'

/**
* Resolved theme literal — always 'light' or 'dark' regardless of the
* `themeMode` setting.
*/
export type ResolvedTheme = 'light' | 'dark'

/**
* Theme mode preference — 'light' and 'dark' are explicit; 'system' defers to
* the OS `prefers-color-scheme` media query.
*/
export type ThemeMode = 'light' | 'dark' | 'system'

/**
* Apply a theme to `document.documentElement` and keep it in sync with the OS
* preference when the mode is `'system'`.
*
* The effect:
* 1. Resolves the effective theme (queries `matchMedia` when `themeMode` is
* `'system'`).
* 2. Writes `data-theme` on `<html>`.
* 3. When `themeMode === 'system'`, subscribes to `prefers-color-scheme: dark`
* changes and re-applies on every flip. The listener is removed when the mode
* changes away from `'system'` or on unmount.
*
* @param themeMode - The user's theme preference (`'light'` | `'dark'` | `'system'`).
* When `'system'` the OS preference is used to determine the actual theme.
*/
export function useThemeDetector(themeMode: ThemeMode): void {
useEffect(() => {
if (typeof window === 'undefined') return
const root = window.document.documentElement

const apply = () => {
if (themeMode === 'system') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
root.setAttribute('data-theme', isDark ? 'dark' : 'light')
} else {
root.setAttribute('data-theme', themeMode)
}
}

apply()

if (themeMode !== 'system') return

const mql = window.matchMedia('(prefers-color-scheme: dark)')
const handler = () => apply()
mql.addEventListener?.('change', handler)
return () => mql.removeEventListener?.('change', handler)
}, [themeMode])
}