feat(appearance): add display preference models

This commit is contained in:
Maofeng
2026-09-29 20:57:59 +08:00
parent 68f142dfa3
commit 75d8cbbfc3
2 changed files with 180 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
import type { ThemeScheme } from "./themes"
/**
* Color-vision assistance modes.
*
* These modes select palettes that are easier to distinguish. They are not
* simulations of how another person sees the interface and intentionally do
* not apply a filter to images or the complete document.
*/
export type ColorVisionMode = "standard" | "protan" | "deutan" | "tritan"
export const colorVisionModes = [
"standard",
"protan",
"deutan",
"tritan",
] as const satisfies readonly ColorVisionMode[]
export const COLOR_VISION_ATTRIBUTE = "data-ui-color-vision"
type ColorVisionVariables = Readonly<
Record<
"destructive" | "chart-1" | "chart-2" | "chart-3" | "chart-4" | "chart-5",
string
>
>
/**
* Palettes for semantic data colors that commonly need to be distinguished.
* Components must still pair color with text, icons, shapes, or patterns.
*/
export const colorVisionPalettes: Readonly<
Record<
Exclude<ColorVisionMode, "standard">,
Record<ThemeScheme, ColorVisionVariables>
>
> = {
protan: {
light: {
destructive: "#b54a00",
"chart-1": "#0072b2",
"chart-2": "#e69f00",
"chart-3": "#009e73",
"chart-4": "#cc79a7",
"chart-5": "#6b5ca5",
},
dark: {
destructive: "#ff9d66",
"chart-1": "#56b4e9",
"chart-2": "#f0e442",
"chart-3": "#65d5b3",
"chart-4": "#e8a4ce",
"chart-5": "#afa4e8",
},
},
deutan: {
light: {
destructive: "#a64b00",
"chart-1": "#005f9e",
"chart-2": "#d88c00",
"chart-3": "#8a6f00",
"chart-4": "#bd63a0",
"chart-5": "#168c9e",
},
dark: {
destructive: "#ff9d66",
"chart-1": "#65bff0",
"chart-2": "#f3c969",
"chart-3": "#d7c85d",
"chart-4": "#e5a0cf",
"chart-5": "#63c7d5",
},
},
tritan: {
light: {
destructive: "#b83261",
"chart-1": "#b83280",
"chart-2": "#008f72",
"chart-3": "#d55e00",
"chart-4": "#6b5ca5",
"chart-5": "#53606d",
},
dark: {
destructive: "#f27ca5",
"chart-1": "#ed8bc9",
"chart-2": "#62d1b3",
"chart-3": "#ff9d66",
"chart-4": "#afa4e8",
"chart-5": "#b8c2cc",
},
},
}
export function isColorVisionMode(value: unknown): value is ColorVisionMode {
return colorVisionModes.some((mode) => mode === value)
}
export function getColorVisionCssVariables(
mode: ColorVisionMode,
scheme: ThemeScheme
): ColorVisionVariables | undefined {
return mode === "standard" ? undefined : colorVisionPalettes[mode][scheme]
}
+77
View File
@@ -0,0 +1,77 @@
import type { ThemeScheme } from "./themes"
export type Brightness = "dim" | "normal" | "bright"
export type MotionMode = "system" | "reduce" | "full"
export type ResolvedMotionMode = Exclude<MotionMode, "system">
export type TextScale = "100" | "125" | "150" | "200"
export const brightnessModes = [
"dim",
"normal",
"bright",
] as const satisfies readonly Brightness[]
export const motionModes = [
"system",
"reduce",
"full",
] as const satisfies readonly MotionMode[]
export const textScales = [
"100",
"125",
"150",
"200",
] as const satisfies readonly TextScale[]
export const BRIGHTNESS_ATTRIBUTE = "data-ui-brightness"
export const MOTION_ATTRIBUTE = "data-ui-motion"
export const TEXT_SCALE_ATTRIBUTE = "data-ui-text-scale"
const surfaceVariableNames = [
"background",
"card",
"popover",
"secondary",
"muted",
"highlight",
"sidebar",
] as const
export function isBrightness(value: unknown): value is Brightness {
return brightnessModes.some((mode) => mode === value)
}
export function isMotionMode(value: unknown): value is MotionMode {
return motionModes.some((mode) => mode === value)
}
export function isTextScale(value: unknown): value is TextScale {
return textScales.some((scale) => scale === value)
}
/**
* Adjusts interface surfaces without filtering media, illustrations, or brand
* colors. Foreground colors are preserved so dimming does not reduce contrast.
*/
export function applyBrightnessToTheme(
variables: Record<string, string>,
brightness: Brightness,
scheme: ThemeScheme
): Record<string, string> {
if (brightness === "normal") return variables
const target = brightness === "dim" ? "black" : "white"
const amount = {
bright: scheme === "light" ? 12 : 8,
dim: scheme === "light" ? 5 : 12,
}[brightness]
const adjusted = { ...variables }
for (const name of surfaceVariableNames) {
const value = variables[name]
if (value) {
adjusted[name] = `color-mix(in oklch,${value},${target} ${amount}%)`
}
}
return adjusted
}