| 1 | import { |
| 2 | BUILT_IN_THEMES, |
| 3 | T3_CHAT_THEME, |
| 4 | T3_CODE_LIGHT_THEME_COLORS, |
| 5 | T3_CODE_DARK_THEME_COLORS, |
| 6 | getThemeColorsForAppearance, |
| 7 | MOBILE_DEFAULT_THEME_ID, |
| 8 | MOBILE_THEME_IDS as SHARED_MOBILE_THEME_IDS, |
| 9 | type MobileThemeId as SharedMobileThemeId, |
| 10 | type ThemeAppearance, |
| 11 | type ThemeColors, |
| 12 | } from "@t3tools/shared/themePalettes"; |
| 13 | import { |
| 14 | STANDARD_THEME_PREVIEW_COLORS, |
| 15 | type ThemePreviewColors, |
| 16 | } from "@t3tools/shared/themePreview"; |
| 17 | |
| 18 | export const DEFAULT_MOBILE_THEME_ID = MOBILE_DEFAULT_THEME_ID; |
| 19 | export const MOBILE_THEME_IDS = [...SHARED_MOBILE_THEME_IDS, "material-you"] as const; |
| 20 | export type MobileThemeId = SharedMobileThemeId | "material-you"; |
| 21 | export type MobileThemeAppearance = ThemeAppearance; |
| 22 | export type MobileThemeMode = MobileThemeAppearance | "system"; |
| 23 | export type MobileThemeIds = Readonly<Record<MobileThemeAppearance, MobileThemeId>>; |
| 24 | |
| 25 | export const MOBILE_THEME_OPTIONS: ReadonlyArray<{ |
| 26 | readonly id: MobileThemeId; |
| 27 | readonly label: string; |
| 28 | }> = [ |
| 29 | { id: DEFAULT_MOBILE_THEME_ID, label: "The Knife" }, |
| 30 | { id: "material-you", label: "Material You" }, |
| 31 | ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })), |
| 32 | ]; |
| 33 | |
| 34 | // Closed set: every key `createMobileThemeVariables` writes. Reads of a |
| 35 | // misspelled variable then fail to compile instead of yielding undefined. |
| 36 | export type MobileThemeVariable = keyof ReturnType<typeof createMobileThemeVariables>; |
| 37 | export type MobileThemeVariables = Readonly<Record<MobileThemeVariable, string>>; |
| 38 | |
| 39 | export function normalizeMobileThemeId(value: unknown): MobileThemeId { |
| 40 | return typeof value === "string" && (MOBILE_THEME_IDS as readonly string[]).includes(value) |
| 41 | ? (value as MobileThemeId) |
| 42 | : DEFAULT_MOBILE_THEME_ID; |
| 43 | } |
| 44 | |
| 45 | export function normalizeMobileThemeMode(value: unknown): MobileThemeMode { |
| 46 | return value === "light" || value === "dark" || value === "system" ? value : "system"; |
| 47 | } |
| 48 | |
| 49 | export function resolveMobileThemeIds(preferences: { |
| 50 | readonly themeId?: unknown; |
| 51 | readonly lightThemeId?: unknown; |
| 52 | readonly darkThemeId?: unknown; |
| 53 | }): MobileThemeIds { |
| 54 | const legacyThemeId = normalizeMobileThemeId(preferences.themeId); |
| 55 | return { |
| 56 | light: |
| 57 | preferences.lightThemeId === undefined |
| 58 | ? legacyThemeId |
| 59 | : normalizeMobileThemeId(preferences.lightThemeId), |
| 60 | dark: |
| 61 | preferences.darkThemeId === undefined |
| 62 | ? legacyThemeId |
| 63 | : normalizeMobileThemeId(preferences.darkThemeId), |
| 64 | }; |
| 65 | } |
| 66 | |
| 67 | export function createMobileThemeSelectionPatch( |
| 68 | themeIds: MobileThemeIds, |
| 69 | activeAppearance: MobileThemeAppearance, |
| 70 | selectedAppearance: MobileThemeAppearance, |
| 71 | value: MobileThemeId, |
| 72 | ) { |
| 73 | const nextThemeIds: MobileThemeIds = { |
| 74 | light: selectedAppearance === "light" ? value : themeIds.light, |
| 75 | dark: selectedAppearance === "dark" ? value : themeIds.dark, |
| 76 | }; |
| 77 | return { |
| 78 | lightThemeId: nextThemeIds.light, |
| 79 | darkThemeId: nextThemeIds.dark, |
| 80 | // Keep older OTA bundles on the theme for the appearance currently in use. |
| 81 | themeId: nextThemeIds[activeAppearance], |
| 82 | }; |
| 83 | } |
| 84 | |
| 85 | export function createMobileThemePairPatch(value: MobileThemeId) { |
| 86 | return { |
| 87 | lightThemeId: value, |
| 88 | darkThemeId: value, |
| 89 | themeId: value, |
| 90 | }; |
| 91 | } |
| 92 | |
| 93 | const OKLCH_PATTERN = /^oklch\(\s*([\d.]+)\s+([\d.]+)\s+(-?[\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)$/; |
| 94 | |
| 95 | function linearToSrgb(value: number): number { |
| 96 | const converted = value <= 0.0031308 ? 12.92 * value : 1.055 * value ** (1 / 2.4) - 0.055; |
| 97 | return Math.round(Math.min(1, Math.max(0, converted)) * 255); |
| 98 | } |
| 99 | |
| 100 | /** React Native does not accept OKLCH ColorValues, so palettes cross the app boundary as sRGB. */ |
| 101 | export function themeColorToNativeColor(value: string): string { |
| 102 | const match = OKLCH_PATTERN.exec(value); |
| 103 | if (!match) return value; |
| 104 | |
| 105 | const lightness = Number(match[1]); |
| 106 | const chroma = Number(match[2]); |
| 107 | const hue = (Number(match[3]) * Math.PI) / 180; |
| 108 | const alpha = match[4] === undefined ? 1 : Number(match[4]); |
| 109 | const a = chroma * Math.cos(hue); |
| 110 | const b = chroma * Math.sin(hue); |
| 111 | const lPrime = lightness + 0.3963377774 * a + 0.2158037573 * b; |
| 112 | const mPrime = lightness - 0.1055613458 * a - 0.0638541728 * b; |
| 113 | const sPrime = lightness - 0.0894841775 * a - 1.291485548 * b; |
| 114 | const l = lPrime ** 3; |
| 115 | const m = mPrime ** 3; |
| 116 | const s = sPrime ** 3; |
| 117 | const red = linearToSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s); |
| 118 | const green = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s); |
| 119 | const blue = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s); |
| 120 | |
| 121 | return alpha < 1 |
| 122 | ? `rgba(${red}, ${green}, ${blue}, ${Number(alpha.toFixed(4))})` |
| 123 | : `#${[red, green, blue].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`; |
| 124 | } |
| 125 | |
| 126 | export function nativeColors(colors: ThemeColors): ThemeColors { |
| 127 | return Object.fromEntries( |
| 128 | Object.entries(colors).map(([role, color]) => [role, themeColorToNativeColor(color)]), |
| 129 | ) as ThemeColors; |
| 130 | } |
| 131 | |
| 132 | function withAlpha(color: string, alpha: number): string { |
| 133 | const hex = color.startsWith("#") ? color.slice(1) : ""; |
| 134 | if (hex.length !== 6) return color; |
| 135 | const [red, green, blue] = [0, 2, 4].map((offset) => |
| 136 | Number.parseInt(hex.slice(offset, offset + 2), 16), |
| 137 | ); |
| 138 | return `rgba(${red}, ${green}, ${blue}, ${alpha})`; |
| 139 | } |
| 140 | |
| 141 | function rgbChannels(color: string): readonly [number, number, number] | null { |
| 142 | const match = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})(?:[\da-f]{2})?$/i.exec(color); |
| 143 | if (!match) return null; |
| 144 | const [, red = "0", green = "0", blue = "0"] = match; |
| 145 | return [Number.parseInt(red, 16), Number.parseInt(green, 16), Number.parseInt(blue, 16)]; |
| 146 | } |
| 147 | |
| 148 | /** |
| 149 | * An opaque form of a theme colour, composited over the surface behind it. Native chip drawing |
| 150 | * parses only opaque hex — an `rgba()` string falls back to a default that is nothing like the |
| 151 | * colour asked for — so a translucent role like `--color-border` has to be flattened first. |
| 152 | */ |
| 153 | export function flattenThemeColor(color: string, surface: string): string { |
| 154 | const alphaHex = /^#([\da-f]{6})([\da-f]{2})$/i.exec(color); |
| 155 | if (alphaHex) { |
| 156 | const channels = rgbChannels(color)!; |
| 157 | return flattenThemeColor( |
| 158 | `rgba(${channels[0]}, ${channels[1]}, ${channels[2]}, ${Number.parseInt(alphaHex[2]!, 16) / 255})`, |
| 159 | surface, |
| 160 | ); |
| 161 | } |
| 162 | const match = /^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+))?\s*\)$/i.exec( |
| 163 | color.trim(), |
| 164 | ); |
| 165 | if (!match) return color; |
| 166 | const alpha = match[4] === undefined ? 1 : Number(match[4]); |
| 167 | const behind = rgbChannels(surface) ?? [0, 0, 0]; |
| 168 | const channels = [match[1], match[2], match[3]].map((channel, index) => |
| 169 | Math.max(0, Math.min(255, Math.round(Number(channel) * alpha + behind[index]! * (1 - alpha)))), |
| 170 | ); |
| 171 | return `#${channels.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`; |
| 172 | } |
| 173 | |
| 174 | function relativeLuminance(channels: readonly [number, number, number]): number { |
| 175 | const [red, green, blue] = channels.map((channel) => { |
| 176 | const value = channel / 255; |
| 177 | return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; |
| 178 | }); |
| 179 | return 0.2126 * red! + 0.7152 * green! + 0.0722 * blue!; |
| 180 | } |
| 181 | |
| 182 | function contrastRatio( |
| 183 | first: readonly [number, number, number], |
| 184 | second: readonly [number, number, number], |
| 185 | ): number { |
| 186 | const firstLuminance = relativeLuminance(first); |
| 187 | const secondLuminance = relativeLuminance(second); |
| 188 | return ( |
| 189 | (Math.max(firstLuminance, secondLuminance) + 0.05) / |
| 190 | (Math.min(firstLuminance, secondLuminance) + 0.05) |
| 191 | ); |
| 192 | } |
| 193 | |
| 194 | /** Keep the theme tint while adjusting lightness to contrast with every supplied surface. */ |
| 195 | export function themeColorWithContrast( |
| 196 | accent: string, |
| 197 | surface: string | ReadonlyArray<string>, |
| 198 | minimum = 4.5, |
| 199 | ): string { |
| 200 | const accentChannels = rgbChannels(accent); |
| 201 | const surfaceChannels = (typeof surface === "string" ? [surface] : surface) |
| 202 | .map(rgbChannels) |
| 203 | .filter((channels) => channels !== null); |
| 204 | const minimumContrast = (channels: readonly [number, number, number]) => |
| 205 | Math.min(...surfaceChannels.map((surface) => contrastRatio(channels, surface))); |
| 206 | if ( |
| 207 | !accentChannels || |
| 208 | surfaceChannels.length === 0 || |
| 209 | minimumContrast(accentChannels) >= minimum |
| 210 | ) { |
| 211 | return accent; |
| 212 | } |
| 213 | |
| 214 | const black = [0, 0, 0] as const; |
| 215 | const white = [255, 255, 255] as const; |
| 216 | const target = minimumContrast(black) >= minimumContrast(white) ? black : white; |
| 217 | let readable: readonly [number, number, number] = target; |
| 218 | let lowerAmount = 0; |
| 219 | let upperAmount = 1; |
| 220 | for (let index = 0; index < 12; index += 1) { |
| 221 | const amount = (lowerAmount + upperAmount) / 2; |
| 222 | const candidate: readonly [number, number, number] = [ |
| 223 | Math.round(accentChannels[0] + (target[0] - accentChannels[0]) * amount), |
| 224 | Math.round(accentChannels[1] + (target[1] - accentChannels[1]) * amount), |
| 225 | Math.round(accentChannels[2] + (target[2] - accentChannels[2]) * amount), |
| 226 | ]; |
| 227 | if (minimumContrast(candidate) >= minimum) { |
| 228 | readable = candidate; |
| 229 | upperAmount = amount; |
| 230 | } else { |
| 231 | lowerAmount = amount; |
| 232 | } |
| 233 | } |
| 234 | return `#${readable.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`; |
| 235 | } |
| 236 | |
| 237 | export function themeColorWithAlpha(color: string, alpha: number): string { |
| 238 | const channels = rgbChannels(color); |
| 239 | if (channels) { |
| 240 | return `rgba(${channels[0]}, ${channels[1]}, ${channels[2]}, ${alpha})`; |
| 241 | } |
| 242 | const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/.exec(color); |
| 243 | return rgb ? `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${alpha})` : color; |
| 244 | } |
| 245 | |
| 246 | export function createMobileThemeVariables( |
| 247 | colors: ThemeColors, |
| 248 | appearance: MobileThemeAppearance, |
| 249 | groupedCardColor = colors.surface, |
| 250 | prominentAction: readonly [string, string] = [colors.text, colors.canvas], |
| 251 | ) { |
| 252 | const c = nativeColors(colors); |
| 253 | const groupedCard = themeColorToNativeColor(groupedCardColor); |
| 254 | const textSurfaces = [ |
| 255 | c.canvas, |
| 256 | c.surface, |
| 257 | c.surfaceRaised, |
| 258 | c.chrome, |
| 259 | c.sidebar, |
| 260 | c.secondary, |
| 261 | groupedCard, |
| 262 | ]; |
| 263 | return { |
| 264 | "--color-screen": c.canvas, |
| 265 | "--color-sheet": withAlpha(c.chrome, 0.98), |
| 266 | "--color-sheet-solid": c.chrome, |
| 267 | "--color-card": c.surface, |
| 268 | "--color-grouped-card": groupedCard, |
| 269 | "--color-card-alt": c.surfaceRaised, |
| 270 | "--color-card-translucent": withAlpha(c.surface, 0.8), |
| 271 | "--color-thread-canvas": c.canvas, |
| 272 | "--color-thread-selected": c.sidebarRowActive, |
| 273 | "--color-thread-selected-foreground": c.sidebarForeground, |
| 274 | "--color-thread-selected-foreground-muted": themeColorWithContrast( |
| 275 | c.sidebarMutedForeground, |
| 276 | c.sidebarRowActive, |
| 277 | ), |
| 278 | "--color-thread-hover": c.sidebarRowHover, |
| 279 | "--color-row-hover": c.toolbarControlHover, |
| 280 | "--color-composer-panel": themeColorWithAlpha(c.canvas, appearance === "dark" ? 0.92 : 0.88), |
| 281 | "--color-composer-surface": themeColorWithAlpha( |
| 282 | groupedCard, |
| 283 | appearance === "dark" ? 0.9 : 0.94, |
| 284 | ), |
| 285 | "--color-composer-border": themeColorWithAlpha( |
| 286 | c.border, |
| 287 | groupedCard === c.surface ? (appearance === "dark" ? 0.46 : 0.54) : 0.8, |
| 288 | ), |
| 289 | "--color-foreground": c.text, |
| 290 | "--color-foreground-secondary": themeColorWithContrast(c.textMuted, textSurfaces), |
| 291 | "--color-foreground-muted": themeColorWithContrast(c.mutedForeground, textSurfaces), |
| 292 | "--color-foreground-tertiary": c.secondaryLabel, |
| 293 | "--color-border": c.border, |
| 294 | "--color-focus": c.focus, |
| 295 | "--color-border-subtle": withAlpha(c.border, 0.7), |
| 296 | "--color-separator": withAlpha(c.border, 0.55), |
| 297 | "--color-subtle": c.muted, |
| 298 | "--color-subtle-strong": c.secondary, |
| 299 | "--color-inline-skill-background": c.accentSurface, |
| 300 | "--color-inline-skill-border": withAlpha(c.accent, 0.42), |
| 301 | "--color-inline-skill-foreground": c.accentSurfaceForeground, |
| 302 | "--color-primary": c.messageAction, |
| 303 | "--color-prominent-action": themeColorToNativeColor(prominentAction[0]), |
| 304 | "--color-prominent-action-foreground": themeColorToNativeColor(prominentAction[1]), |
| 305 | "--color-primary-foreground": c.messageActionForeground, |
| 306 | "--color-primary-text": themeColorWithContrast(c.messageAction, textSurfaces), |
| 307 | "--color-primary-shadow": "#000000", |
| 308 | "--color-secondary": c.secondary, |
| 309 | "--color-secondary-foreground": c.secondaryForeground, |
| 310 | "--color-secondary-border": c.border, |
| 311 | "--color-switch-active-track": c.messageAction, |
| 312 | "--color-switch-active-thumb": c.messageActionForeground, |
| 313 | "--color-switch-inactive-track": c.secondary, |
| 314 | "--color-switch-inactive-thumb": c.mutedForeground, |
| 315 | "--color-warning": c.warningSurface, |
| 316 | "--color-warning-border": withAlpha(c.warning, 0.32), |
| 317 | "--color-warning-foreground": c.warningForeground, |
| 318 | "--color-danger": c.errorSurface, |
| 319 | "--color-danger-border": withAlpha(c.error, 0.32), |
| 320 | "--color-danger-foreground": c.errorForeground, |
| 321 | "--color-update": c.updateSurface, |
| 322 | "--color-update-foreground": c.updateForeground, |
| 323 | "--color-input": c.surface, |
| 324 | "--color-input-border": c.input, |
| 325 | "--color-sidebar-search": c.sidebarControlSurface, |
| 326 | "--color-placeholder": themeColorWithContrast(c.placeholder, textSurfaces), |
| 327 | "--color-icon": c.text, |
| 328 | "--color-icon-muted": c.iconMuted, |
| 329 | "--color-icon-subtle": c.secondaryLabel, |
| 330 | "--color-header": withAlpha(c.toolbar, 0.97), |
| 331 | "--color-header-foreground": c.toolbarForeground, |
| 332 | "--color-header-border": c.toolbarBorder, |
| 333 | "--color-glass-surface": withAlpha(c.surfaceOverlay, 0.74), |
| 334 | "--color-glass-fallback": themeColorWithAlpha(groupedCard, appearance === "dark" ? 0.9 : 0.94), |
| 335 | "--color-glass-tint": withAlpha(c.surfaceOverlay, 0.22), |
| 336 | "--color-status-bar": c.canvas, |
| 337 | "--color-md-body": c.text, |
| 338 | "--color-md-strong": c.text, |
| 339 | "--color-md-link": themeColorWithContrast(c.messageAction, c.canvas), |
| 340 | "--color-md-blockquote-border": c.border, |
| 341 | "--color-md-blockquote-bg": c.muted, |
| 342 | "--color-md-code-bg": c.codeBackground, |
| 343 | "--color-md-code-text": c.codeForeground, |
| 344 | "--color-md-user-code-bg": withAlpha(c.messageForeground, 0.18), |
| 345 | "--color-md-user-code-text": c.messageForeground, |
| 346 | "--color-md-user-fence-bg": withAlpha("#000000", appearance === "dark" ? 0.28 : 0.16), |
| 347 | "--color-md-user-fence-text": c.messageForeground, |
| 348 | "--color-md-hr": c.border, |
| 349 | "--color-user-bubble": c.messageSurface, |
| 350 | "--color-user-bubble-foreground": c.messageForeground, |
| 351 | "--color-user-bubble-foreground-muted": withAlpha(c.messageForeground, 0.78), |
| 352 | "--color-user-bubble-skill-foreground": themeColorWithContrast( |
| 353 | c.messageAction, |
| 354 | c.messageSurface, |
| 355 | ), |
| 356 | "--color-backdrop": withAlpha("#000000", appearance === "dark" ? 0.48 : 0.22), |
| 357 | "--color-drawer": c.sidebar, |
| 358 | "--color-drawer-foreground": c.sidebarForeground, |
| 359 | "--color-drawer-foreground-muted": themeColorWithContrast(c.sidebarMutedForeground, [ |
| 360 | c.sidebar, |
| 361 | c.sidebarRowHover, |
| 362 | ]), |
| 363 | "--color-drawer-border": c.sidebarBorder, |
| 364 | "--color-drawer-shadow": withAlpha("#000000", appearance === "dark" ? 0.32 : 0.12), |
| 365 | "--color-dot-separator": withAlpha(c.textMuted, 0.35), |
| 366 | "--color-wordmark": c.text, |
| 367 | "--color-chevron": withAlpha(c.textMuted, 0.42), |
| 368 | }; |
| 369 | } |
| 370 | |
| 371 | export const MOBILE_THEME_VARIABLE_NAMES = Object.keys( |
| 372 | createMobileThemeVariables(T3_CHAT_THEME.colors, "light"), |
| 373 | ) as ReadonlyArray<MobileThemeVariable>; |
| 374 | |
| 375 | export function getMobileThemeColors( |
| 376 | themeId: SharedMobileThemeId, |
| 377 | appearance: MobileThemeAppearance, |
| 378 | ): ThemeColors { |
| 379 | if (themeId === DEFAULT_MOBILE_THEME_ID) { |
| 380 | return appearance === "dark" ? T3_CODE_DARK_THEME_COLORS : T3_CODE_LIGHT_THEME_COLORS; |
| 381 | } |
| 382 | const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? T3_CHAT_THEME; |
| 383 | return getThemeColorsForAppearance(theme, appearance) ?? theme.colors; |
| 384 | } |
| 385 | |
| 386 | export function getMobileThemeVariables( |
| 387 | themeId: SharedMobileThemeId, |
| 388 | appearance: MobileThemeAppearance, |
| 389 | overrides: Partial<MobileThemeVariables> | null = null, |
| 390 | ): MobileThemeVariables { |
| 391 | const colors = getMobileThemeColors(themeId, appearance); |
| 392 | // Mobile groups and floating controls need a tonal fill, rather than the |
| 393 | // nearly neutral raised surface used by desktop outlined cards. |
| 394 | const groupedCard = |
| 395 | themeId === DEFAULT_MOBILE_THEME_ID |
| 396 | ? appearance === "light" |
| 397 | ? colors.toolbarControlHover |
| 398 | : colors.sidebarRowActive |
| 399 | : themeId === "t3-chat" |
| 400 | ? appearance === "light" |
| 401 | ? colors.toolbarControl |
| 402 | : colors.surface |
| 403 | : colors.muted; |
| 404 | const mobileColors = |
| 405 | themeId === DEFAULT_MOBILE_THEME_ID |
| 406 | ? { |
| 407 | ...colors, |
| 408 | messageSurface: flattenThemeColor( |
| 409 | themeColorWithAlpha( |
| 410 | appearance === "dark" ? colors.sidebarRowActive : colors.border, |
| 411 | 0.3, |
| 412 | ), |
| 413 | colors.messageSurface, |
| 414 | ), |
| 415 | } |
| 416 | : colors; |
| 417 | const baseVariables = createMobileThemeVariables( |
| 418 | mobileColors, |
| 419 | appearance, |
| 420 | groupedCard, |
| 421 | themeId === "t3-chat" && appearance === "dark" |
| 422 | ? [colors.messageActionForeground, colors.messageAction] |
| 423 | : themeId !== DEFAULT_MOBILE_THEME_ID && themeId !== "t3-chat" |
| 424 | ? [colors.messageAction, colors.messageActionForeground] |
| 425 | : undefined, |
| 426 | ); |
| 427 | |
| 428 | // The complete base record guarantees that optional overrides cannot leave a token undefined. |
| 429 | return overrides ? ({ ...baseVariables, ...overrides } as MobileThemeVariables) : baseVariables; |
| 430 | } |
| 431 | |
| 432 | export function getMobileThemePreviewColors( |
| 433 | themeId: MobileThemeId, |
| 434 | appearance: MobileThemeAppearance, |
| 435 | ): ThemePreviewColors { |
| 436 | if (themeId === DEFAULT_MOBILE_THEME_ID || themeId === "material-you") |
| 437 | return STANDARD_THEME_PREVIEW_COLORS[appearance]; |
| 438 | const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? T3_CHAT_THEME; |
| 439 | const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors; |
| 440 | return { |
| 441 | canvas: themeColorToNativeColor(colors.canvas), |
| 442 | accent: themeColorToNativeColor(colors.accent), |
| 443 | messageAction: themeColorToNativeColor(colors.messageAction), |
| 444 | }; |
| 445 | } |