Something went wrong. Try again.
ATProto-based AI Assistant Chat interface.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102import { useSyncExternalStore } from 'react'
/** The scheme a device shows: the system's light or dark, or one fixed in Preferences. */export type ThemeChoice = 'system' | 'light' | 'dark'
/** Each choice with its name, in the order the pickers show them. */export const themeChoices: { value: ThemeChoice; label: string }[] = [ { value: 'system', label: 'System' }, { value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' },]
export const themeKey = 'scn-chat:theme'
/** * `--color-chrome` in each scheme, as the browser's `theme-color` takes only a plain color. An * installed app's title bar shows it. The server's manifest repeats the light one, and the theme * test fails when any of them drift from the token. */export const chromeColors = { light: '#f7f7f7', dark: '#0e0e0e' }
/** The shell's `theme-color` tags, one per scheme, each naming its own in `data-scheme`. */const themeColorTags = 'meta[name="theme-color"]'
/** * Runs in the page's head before the first paint, so a fixed choice never flashes the other * scheme, in the page or in an installed app's title bar. It reads the store the same way * storedTheme does. */export const themeScript = `try{var t=localStorage.getItem('${themeKey}');if(t==='light'||t==='dark'){document.documentElement.dataset.theme=t;var c=${JSON.stringify(chromeColors)};document.querySelectorAll('${themeColorTags}').forEach(function(m){m.content=c[t]})}}catch(e){}`
export function themeOf(value: string | null): ThemeChoice { return value === 'light' || value === 'dark' ? value : 'system'}
/** The choice stored on this device, or system when there is none or the store can't be read. */export function storedTheme(): ThemeChoice { try { return themeOf(localStorage.getItem(themeKey)) } catch { return 'system' }}
// What this page shows, which stands in for the store when it can't be kept.let shown: ThemeChoice | null = nullconst listeners = new Set<() => void>()
/** Stores the choice on this device and shows it at once. */export function chooseTheme(choice: ThemeChoice) { try { if (choice === 'system') localStorage.removeItem(themeKey) else localStorage.setItem(themeKey, choice) } catch { // The choice still applies to this page when it can't be kept. } showTheme(choice)}
/** The choice this device shows, kept in step as it changes here or in another tab. */export function useThemeChoice(): ThemeChoice { return useSyncExternalStore( (listener) => { listeners.add(listener) return () => listeners.delete(listener) }, () => shown ?? storedTheme(), // The server can't know the device's choice, so the page starts from the system's. () => 'system', )}
/** Sets the scheme the page's colors follow, through the data-theme attribute on the root. */export function showTheme(choice: ThemeChoice) { shown = choice for (const listener of listeners) listener() switchInstantly(() => { const root = document.documentElement if (choice === 'system') delete root.dataset.theme else root.dataset.theme = choice for (const meta of document.querySelectorAll<HTMLMetaElement>(themeColorTags)) { const scheme = choice === 'system' ? meta.dataset.scheme : choice if (scheme === 'light' || scheme === 'dark') meta.content = chromeColors[scheme] } })}
/** * Changes the scheme with transitions held off until the new colors have painted. The colors follow * the scheme through light-dark(), so otherwise every color transition on the page would run * together and the switch would smear. */export function switchInstantly(change: () => void) { const style = document.createElement('style') style.textContent = '*,*::before,*::after{transition:none !important}' document.head.append(style) change() // Reading layout makes the browser apply the new colors while transitions are off. void document.body.offsetHeight requestAnimationFrame(() => requestAnimationFrame(() => style.remove()))}