diff --git a/src/theme/fonts.tsx b/src/theme/fonts.tsx index 3a67d00d67..7808205474 100644 --- a/src/theme/fonts.tsx +++ b/src/theme/fonts.tsx @@ -1,5 +1,5 @@ -import type { TypescaleStyle, Typescale, TypescaleKey } from '../types'; import { typescale } from './tokens'; +import type { TypescaleStyle, Typescale, TypescaleKey } from './types'; type FontsConfig = | { diff --git a/src/theme/schemes/DarkTheme.tsx b/src/theme/schemes/DarkTheme.tsx index 830bfec4b1..85ca245e57 100644 --- a/src/theme/schemes/DarkTheme.tsx +++ b/src/theme/schemes/DarkTheme.tsx @@ -1,75 +1,11 @@ -import color from 'color'; - -import { LightTheme } from './LightTheme'; -import type { Theme } from '../../types'; +import { baseTheme } from './base'; import { tokens } from '../tokens'; - -const { palette, stateOpacity } = tokens.md.ref; +import { buildScheme } from '../tokens/sys/color/roles'; +import type { Theme } from '../types'; export const DarkTheme: Theme = { - ...LightTheme, + ...baseTheme, dark: true, mode: 'adaptive', - colors: { - primary: palette.primary80, - primaryContainer: palette.primary30, - secondary: palette.secondary80, - secondaryContainer: palette.secondary30, - tertiary: palette.tertiary80, - tertiaryContainer: palette.tertiary30, - surface: palette.neutral6, - surfaceDim: palette.neutral6, - surfaceBright: palette.neutral24, - surfaceContainerLowest: palette.neutral4, - surfaceContainerLow: palette.neutral10, - surfaceContainer: palette.neutral12, - surfaceContainerHigh: palette.neutral17, - surfaceContainerHighest: palette.neutral22, - surfaceVariant: palette.neutralVariant30, - background: palette.neutral6, - error: palette.error80, - errorContainer: palette.error30, - onPrimary: palette.primary20, - onPrimaryContainer: palette.primary90, - onSecondary: palette.secondary20, - onSecondaryContainer: palette.secondary90, - onTertiary: palette.tertiary20, - onTertiaryContainer: palette.tertiary90, - onSurface: palette.neutral90, - onSurfaceVariant: palette.neutralVariant80, - onError: palette.error20, - onErrorContainer: palette.error80, - onBackground: palette.neutral90, - outline: palette.neutralVariant60, - outlineVariant: palette.neutralVariant30, - inverseSurface: palette.neutral90, - inverseOnSurface: palette.neutral20, - inversePrimary: palette.primary40, - primaryFixed: palette.primary90, - primaryFixedDim: palette.primary80, - onPrimaryFixed: palette.primary10, - onPrimaryFixedVariant: palette.primary30, - secondaryFixed: palette.secondary90, - secondaryFixedDim: palette.secondary80, - onSecondaryFixed: palette.secondary10, - onSecondaryFixedVariant: palette.secondary30, - tertiaryFixed: palette.tertiary90, - tertiaryFixedDim: palette.tertiary80, - onTertiaryFixed: palette.tertiary10, - onTertiaryFixedVariant: palette.tertiary30, - shadow: palette.neutral0, - scrim: palette.neutral0, - stateLayerPressed: color(palette.neutral90) - .alpha(stateOpacity.pressed) - .rgb() - .string(), - elevation: { - level0: 'transparent', - level1: palette.neutral10, - level2: palette.neutral12, - level3: palette.neutral17, - level4: palette.neutral17, - level5: palette.neutral22, - }, - }, + colors: buildScheme(tokens.md.ref.palette, tokens.md.ref, { mode: 'dark' }), }; diff --git a/src/theme/schemes/DynamicTheme.android.tsx b/src/theme/schemes/DynamicTheme.android.tsx index f2791670e8..84667f5b79 100644 --- a/src/theme/schemes/DynamicTheme.android.tsx +++ b/src/theme/schemes/DynamicTheme.android.tsx @@ -2,7 +2,7 @@ import { Platform, PlatformColor } from 'react-native'; import { DarkTheme } from './DarkTheme'; import { LightTheme } from './LightTheme'; -import type { Theme } from '../../types'; +import type { Theme } from '../types'; const isApi34 = (Platform.Version as number) >= 34; const isApi31 = (Platform.Version as number) >= 31; diff --git a/src/theme/schemes/LightTheme.tsx b/src/theme/schemes/LightTheme.tsx index 8d7f3854d9..d60f1a7709 100644 --- a/src/theme/schemes/LightTheme.tsx +++ b/src/theme/schemes/LightTheme.tsx @@ -1,78 +1,10 @@ -import color from 'color'; - -import type { Theme } from '../../types'; -import configureFonts from '../fonts'; +import { baseTheme } from './base'; import { tokens } from '../tokens'; - -const { palette, stateOpacity } = tokens.md.ref; +import { buildScheme } from '../tokens/sys/color/roles'; +import type { Theme } from '../types'; export const LightTheme: Theme = { + ...baseTheme, dark: false, - roundness: 4, - colors: { - primary: palette.primary40, - primaryContainer: palette.primary90, - secondary: palette.secondary40, - secondaryContainer: palette.secondary90, - tertiary: palette.tertiary40, - tertiaryContainer: palette.tertiary90, - surface: palette.neutral98, - surfaceDim: palette.neutral87, - surfaceBright: palette.neutral98, - surfaceContainerLowest: palette.neutral100, - surfaceContainerLow: palette.neutral96, - surfaceContainer: palette.neutral94, - surfaceContainerHigh: palette.neutral92, - surfaceContainerHighest: palette.neutral90, - surfaceVariant: palette.neutralVariant90, - background: palette.neutral98, - error: palette.error40, - errorContainer: palette.error90, - onPrimary: palette.primary100, - onPrimaryContainer: palette.primary10, - onSecondary: palette.secondary100, - onSecondaryContainer: palette.secondary10, - onTertiary: palette.tertiary100, - onTertiaryContainer: palette.tertiary10, - onSurface: palette.neutral10, - onSurfaceVariant: palette.neutralVariant30, - onError: palette.error100, - onErrorContainer: palette.error10, - onBackground: palette.neutral10, - outline: palette.neutralVariant50, - outlineVariant: palette.neutralVariant80, - inverseSurface: palette.neutral20, - inverseOnSurface: palette.neutral95, - inversePrimary: palette.primary80, - primaryFixed: palette.primary90, - primaryFixedDim: palette.primary80, - onPrimaryFixed: palette.primary10, - onPrimaryFixedVariant: palette.primary30, - secondaryFixed: palette.secondary90, - secondaryFixedDim: palette.secondary80, - onSecondaryFixed: palette.secondary10, - onSecondaryFixedVariant: palette.secondary30, - tertiaryFixed: palette.tertiary90, - tertiaryFixedDim: palette.tertiary80, - onTertiaryFixed: palette.tertiary10, - onTertiaryFixedVariant: palette.tertiary30, - shadow: palette.neutral0, - scrim: palette.neutral0, - stateLayerPressed: color(palette.neutral10) - .alpha(stateOpacity.pressed) - .rgb() - .string(), - elevation: { - level0: 'transparent', - level1: palette.neutral96, - level2: palette.neutral94, - level3: palette.neutral92, - level4: palette.neutral92, - level5: palette.neutral90, - }, - }, - fonts: configureFonts(), - animation: { - scale: 1.0, - }, + colors: buildScheme(tokens.md.ref.palette, tokens.md.ref, { mode: 'light' }), }; diff --git a/src/theme/schemes/base.ts b/src/theme/schemes/base.ts new file mode 100644 index 0000000000..68cf999cee --- /dev/null +++ b/src/theme/schemes/base.ts @@ -0,0 +1,9 @@ +import configureFonts from '../fonts'; + +export const baseTheme = { + roundness: 4, + fonts: configureFonts(), + animation: { + scale: 1.0, + }, +}; diff --git a/src/theme/tokens/index.ts b/src/theme/tokens/index.ts index 987bca6ace..ecc35ffa24 100644 --- a/src/theme/tokens/index.ts +++ b/src/theme/tokens/index.ts @@ -1,6 +1,6 @@ import { Platform } from 'react-native'; -import type { Font } from '../../types'; +import type { Font } from '../types'; const ref = { palette: { diff --git a/src/theme/tokens/sys/color/roles.ts b/src/theme/tokens/sys/color/roles.ts new file mode 100644 index 0000000000..a3b3a49695 --- /dev/null +++ b/src/theme/tokens/sys/color/roles.ts @@ -0,0 +1,180 @@ +import color from 'color'; + +import type { ElevationColors, ThemeColors } from '../../../types'; +import { tokens } from '../../index'; + +type Palette = typeof tokens.md.ref.palette; +type PaletteKey = keyof Palette; +type Ref = typeof tokens.md.ref; + +/** Roles that map 1:1 to a palette key. Excludes the computed fields. */ +type MappedRoles = Omit; + +type Contrast = 'standard'; // extend with 'medium' | 'high' when those ship + +const roleToTone: Record< + 'light' | 'dark', + Record> +> = { + light: { + standard: { + primary: 'primary40', + onPrimary: 'primary100', + primaryContainer: 'primary90', + onPrimaryContainer: 'primary10', + secondary: 'secondary40', + onSecondary: 'secondary100', + secondaryContainer: 'secondary90', + onSecondaryContainer: 'secondary10', + tertiary: 'tertiary40', + onTertiary: 'tertiary100', + tertiaryContainer: 'tertiary90', + onTertiaryContainer: 'tertiary10', + error: 'error40', + onError: 'error100', + errorContainer: 'error90', + onErrorContainer: 'error10', + surface: 'neutral98', + surfaceDim: 'neutral87', + surfaceBright: 'neutral98', + surfaceContainerLowest: 'neutral100', + surfaceContainerLow: 'neutral96', + surfaceContainer: 'neutral94', + surfaceContainerHigh: 'neutral92', + surfaceContainerHighest: 'neutral90', + surfaceVariant: 'neutralVariant90', + background: 'neutral98', + onSurface: 'neutral10', + onSurfaceVariant: 'neutralVariant30', + onBackground: 'neutral10', + outline: 'neutralVariant50', + outlineVariant: 'neutralVariant80', + inverseSurface: 'neutral20', + inverseOnSurface: 'neutral95', + inversePrimary: 'primary80', + primaryFixed: 'primary90', + primaryFixedDim: 'primary80', + onPrimaryFixed: 'primary10', + onPrimaryFixedVariant: 'primary30', + secondaryFixed: 'secondary90', + secondaryFixedDim: 'secondary80', + onSecondaryFixed: 'secondary10', + onSecondaryFixedVariant: 'secondary30', + tertiaryFixed: 'tertiary90', + tertiaryFixedDim: 'tertiary80', + onTertiaryFixed: 'tertiary10', + onTertiaryFixedVariant: 'tertiary30', + shadow: 'neutral0', + scrim: 'neutral0', + }, + }, + dark: { + standard: { + primary: 'primary80', + onPrimary: 'primary20', + primaryContainer: 'primary30', + onPrimaryContainer: 'primary90', + secondary: 'secondary80', + onSecondary: 'secondary20', + secondaryContainer: 'secondary30', + onSecondaryContainer: 'secondary90', + tertiary: 'tertiary80', + onTertiary: 'tertiary20', + tertiaryContainer: 'tertiary30', + onTertiaryContainer: 'tertiary90', + error: 'error80', + onError: 'error20', + errorContainer: 'error30', + onErrorContainer: 'error80', + surface: 'neutral6', + surfaceDim: 'neutral6', + surfaceBright: 'neutral24', + surfaceContainerLowest: 'neutral4', + surfaceContainerLow: 'neutral10', + surfaceContainer: 'neutral12', + surfaceContainerHigh: 'neutral17', + surfaceContainerHighest: 'neutral22', + surfaceVariant: 'neutralVariant30', + background: 'neutral6', + onSurface: 'neutral90', + onSurfaceVariant: 'neutralVariant80', + onBackground: 'neutral90', + outline: 'neutralVariant60', + outlineVariant: 'neutralVariant30', + inverseSurface: 'neutral90', + inverseOnSurface: 'neutral20', + inversePrimary: 'primary40', + primaryFixed: 'primary90', + primaryFixedDim: 'primary80', + onPrimaryFixed: 'primary10', + onPrimaryFixedVariant: 'primary30', + secondaryFixed: 'secondary90', + secondaryFixedDim: 'secondary80', + onSecondaryFixed: 'secondary10', + onSecondaryFixedVariant: 'secondary30', + tertiaryFixed: 'tertiary90', + tertiaryFixedDim: 'tertiary80', + onTertiaryFixed: 'tertiary10', + onTertiaryFixedVariant: 'tertiary30', + shadow: 'neutral0', + scrim: 'neutral0', + }, + }, +}; + +const elevationToTone: Record< + 'light' | 'dark', + Record, PaletteKey>> +> = { + light: { + standard: { + level1: 'neutral96', + level2: 'neutral94', + level3: 'neutral92', + level4: 'neutral92', + level5: 'neutral90', + }, + }, + dark: { + standard: { + level1: 'neutral10', + level2: 'neutral12', + level3: 'neutral17', + level4: 'neutral17', + level5: 'neutral22', + }, + }, +}; + +export function buildScheme( + palette: Palette, + ref: Ref, + opts: { mode: 'light' | 'dark'; contrast?: Contrast } +): ThemeColors { + const contrast = opts.contrast ?? 'standard'; + const tones = roleToTone[opts.mode][contrast]; + const elevTones = elevationToTone[opts.mode][contrast]; + + const mapped = Object.fromEntries( + (Object.keys(tones) as Array).map((role) => [ + role, + palette[tones[role]], + ]) + ) as MappedRoles; + + return { + ...mapped, + stateLayerPressed: color(palette[tones.onSurface]) + .alpha(ref.stateOpacity.pressed) + .rgb() + .string(), + elevation: { + level0: 'transparent', + level1: palette[elevTones.level1], + level2: palette[elevTones.level2], + level3: palette[elevTones.level3], + level4: palette[elevTones.level4], + level5: palette[elevTones.level5], + }, + }; +}