// Copyright (c) Meta Platforms, Inc. and affiliates. /** * @file onMediaTokens.ts * @input Theme token values from defineTheme * @output Default on-dark / on-light token overrides for MediaTheme * @position Theme system utility; consumed by defineTheme and generateThemeRules * * Generates semantic token overrides for content rendered on inverted surfaces. * "onDark" = content on a dark background (light text, white-tinted interactions) * "onLight" = content on a light background (dark text, black-tinted interactions) * * The primary mechanism is `color-scheme` — setting `color-scheme: dark` on the * media element makes all `light-dark()` tokens resolve to their dark-side values. * Only a small set of tokens need explicit overrides (text/icon primary use * `var(--color-on-dark)` instead of the dark-mode grey, accent collapses to * on-color, etc.). * * Themes can provide additional token and component overrides via the * `onDark`/`onLight` fields in `defineTheme()`. */ import type { TokenValue, ComponentStyleMap } from "./defineTheme.js"; import { deepMergeComponents } from "./mergeComponents.js"; /** * On-media theme overrides — same shape as the main theme but scoped * to a surface luminance context. */ export interface OnMediaOverrides { /** Token overrides for this surface context */ tokens?: Partial>; /** Component style overrides for this surface context */ components?: ComponentStyleMap; } /** * Resolved on-media overrides stored on DefinedTheme. * @internal */ export interface ResolvedOnMedia { /** Resolved token CSS values */ tokens: Record; /** Component style overrides (passthrough from input) */ components?: ComponentStyleMap; } /** * Default token overrides for content on a dark surface. * * Most tokens work automatically via `color-scheme: dark` which flips * `light-dark()` values. These overrides handle the tokens that need * different values on an inverted surface vs a dark page background. */ export const defaultOnDarkTokens: Record = { 'color-scheme': 'dark', // Text/icon primary — pure on-color, not dark-mode grey '--color-text-primary': 'var(--color-on-dark)', '--color-icon-primary': 'var(--color-on-dark)', // Accent collapses to on-color in inverted context '--color-accent': 'var(--color-on-dark)' }; /** * Default token overrides for content on a light surface. */ export const defaultOnLightTokens: Record = { 'color-scheme': 'light', // Text/icon primary — pure on-color, not light-mode dark '--color-text-primary': 'var(--color-on-light)', '--color-icon-primary': 'var(--color-on-light)', // Accent collapses to on-color '--color-accent': 'var(--color-on-light)' }; /** * Resolve a token value to a CSS string. */ function resolveValue(value: TokenValue): string { if (Array.isArray(value)) { return `light-dark(${value[0]}, ${value[1]})`; } return value; } /** * Resolve on-media overrides: merge user tokens with defaults, * pass through component overrides. * * `base` is the already-resolved surface of a theme being extended. It sits * between the defaults and this theme's own input, so a child theme inherits * the surface customizations of the theme it extends instead of silently * reverting them to the defaults. */ export function resolveOnMedia(surface: 'dark' | 'light', input?: OnMediaOverrides, base?: ResolvedOnMedia): ResolvedOnMedia { const defaults = surface === 'dark' ? defaultOnDarkTokens : defaultOnLightTokens; const tokens = { ...defaults, ...base?.tokens }; if (input?.tokens) { for (const [key, value] of Object.entries(input.tokens)) { if (value !== undefined) { tokens[key] = resolveValue(value); } } } return { tokens, components: deepMergeComponents(base?.components, input?.components) }; }