202 lines
5.5 KiB
TypeScript
202 lines
5.5 KiB
TypeScript
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.
|
|
}
|
|
}
|