Files
simple-react-app-kit/docs/src/lib/preferences.ts
T

202 lines
5.5 KiB
TypeScript
Raw Normal View History

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<DocsPreferences>
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<keyof DocsPreferences>
).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<keyof DocsPreferences>
).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<Record<keyof DocsPreferences, unknown>>
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<const Options extends readonly string[]>(
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<Value>(
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.
}
}