import { createPreferenceUpdateGuard, type PreferenceDefinitions, type PreferenceEffect, type PreferenceUpdate, type Preferences, } from "@workspace/preferences" import { defaultDocLocale, docLocales, type DocLocale } from "../content/types" import { accentColors, applyAppearance, backgroundShades, foregroundShades, subscribeToSystemTheme, themeModes, type AccentColor, type Appearance, type BackgroundShade, type ForegroundShade, type ThemeMode, } from "./appearance" export type LocaleMode = "auto" | "manual" export type DocsPreferences = { "accent-color": AccentColor "background-shade": BackgroundShade "foreground-shade": ForegroundShade "locale-mode": LocaleMode locale: DocLocale "theme-mode": ThemeMode } declare module "@workspace/preferences" { interface PreferencesCustom extends DocsPreferences {} } export const docsPreferenceDefinitions = { "accent-color": createStringPreference( "docs-accent-color", "indigo", accentColors ), "background-shade": createStringPreference( "docs-background-shade", "neutral", backgroundShades ), "foreground-shade": createStringPreference( "docs-foreground-shade", "slate", foregroundShades ), "locale-mode": { cookie: "docs-locale-auto-detect", defaultValue: "manual", is: (value): value is LocaleMode => value === "auto" || value === "manual", parse: (value) => (value === "1" || value === "auto" ? "auto" : "manual"), serialize: (value) => (value === "auto" ? "1" : "0"), }, locale: createStringPreference("docs-locale", defaultDocLocale, docLocales), "theme-mode": createStringPreference("docs-theme", "system", themeModes), } satisfies PreferenceDefinitions export const isDocsPreferenceUpdate = createPreferenceUpdateGuard( docsPreferenceDefinitions ) export const docsPreferenceEffects = [ { layoutEffect: ({ preferences }) => { applyAppearance(toAppearance(preferences)) }, effect: ({ preferences, store }) => { if (preferences["theme-mode"] !== "system") return return subscribeToSystemTheme(() => { applyAppearance(toAppearance(store.getSnapshot())) }) }, }, ] satisfies readonly PreferenceEffect[] export function initializeDocsPreferences() { applyAppearance(toAppearance(readDocsPreferences())) } export function readDocsPreferences( fallbackLocale: DocLocale = defaultDocLocale ): Preferences { const preferences = Object.fromEntries( ( Object.keys(docsPreferenceDefinitions) as Array ).map((key) => { const definition = docsPreferenceDefinitions[key] return [key, readStoredValue(definition.cookie, definition.parse)] }) ) as DocsPreferences if (!readStoredString(docsPreferenceDefinitions.locale.cookie)) { preferences.locale = fallbackLocale } return preferences } export function createDefaultDocsPreferences( locale: DocLocale = defaultDocLocale ): Preferences { return Object.fromEntries( ( Object.keys(docsPreferenceDefinitions) as Array ).map((key) => [ key, key === "locale" ? locale : docsPreferenceDefinitions[key].defaultValue, ]) ) as DocsPreferences } export function persistDocsPreference(update: PreferenceUpdate) { const definition = docsPreferenceDefinitions[update.key] const serializedValue = update.key === "locale-mode" ? docsPreferenceDefinitions["locale-mode"].serialize(update.value) : update.value writeStoredString(definition.cookie, serializedValue) } export function parseDocsPreferences(value: unknown): Preferences | undefined { if (!value || typeof value !== "object") return undefined const candidate = value as Partial> for (const key of Object.keys(docsPreferenceDefinitions) as Array< keyof DocsPreferences >) { if (!docsPreferenceDefinitions[key].is(candidate[key])) return undefined } return candidate as DocsPreferences } export function toAppearance(preferences: Preferences): Appearance { return { accentColor: preferences["accent-color"], backgroundShade: preferences["background-shade"], foregroundShade: preferences["foreground-shade"], themeMode: preferences["theme-mode"], } } export function detectLocale(): DocLocale { const languages = navigator.languages.length ? navigator.languages : [navigator.language] return languages.some((language) => language.toLowerCase().startsWith("zh")) ? "zh-Hans" : "en-US" } function createStringPreference( cookie: string, defaultValue: Options[number], options: Options ) { const is = (value: unknown): value is Options[number] => typeof value === "string" && options.some((option) => option === value) return { cookie, defaultValue, is, parse: (value: string | undefined) => (is(value) ? value : defaultValue), serialize: (value: Options[number]) => value, } } function readStoredValue( key: string, parse: (value: string | undefined) => Value ) { return parse(readStoredString(key)) } function readStoredString(key: string) { if (typeof window === "undefined") return undefined try { return window.sessionStorage.getItem(key) ?? undefined } catch { return undefined } } function writeStoredString(key: string, value: string) { try { window.sessionStorage.setItem(key, value) } catch { // The in-memory preference remains active when storage is unavailable. } }