From 96603d4a3acbbbae35822891351217d13358668f Mon Sep 17 00:00:00 2001 From: Adrian Cotfas Date: Thu, 7 May 2026 16:09:53 +0300 Subject: [PATCH] feat: add dynamicColor prop and DynamicTheme refactor --- .../__fixtures__/rewrite-imports/output.js | 2 +- src/core/PaperProvider.tsx | 31 +- src/core/__tests__/PaperProvider.test.tsx | 12 +- src/deprecated.ts | 11 + src/index.tsx | 3 +- src/theme/provider.tsx | 10 +- src/theme/schemes/DynamicTheme.android.tsx | 929 +++++++++--------- src/theme/schemes/DynamicTheme.tsx | 6 + src/theme/schemes/index.ts | 6 +- src/theme/types/theme.ts | 20 +- 10 files changed, 532 insertions(+), 498 deletions(-) diff --git a/src/babel/__fixtures__/rewrite-imports/output.js b/src/babel/__fixtures__/rewrite-imports/output.js index 7105f4216e..f9b98ad938 100644 --- a/src/babel/__fixtures__/rewrite-imports/output.js +++ b/src/babel/__fixtures__/rewrite-imports/output.js @@ -8,4 +8,4 @@ import { MD3Colors } from "react-native-paper/lib/module/deprecated"; import { NonExistent, NonExistentSecond as Stuff } from "react-native-paper/lib/module/index.js"; import { ThemeProvider } from "react-native-paper/lib/module/core/theming"; import { withTheme } from "react-native-paper/lib/module/core/theming"; -import { DefaultTheme } from "react-native-paper/lib/module/core/theming"; +import { DefaultTheme } from "react-native-paper/lib/module/deprecated"; diff --git a/src/core/PaperProvider.tsx b/src/core/PaperProvider.tsx index 4faa16f109..d261892977 100644 --- a/src/core/PaperProvider.tsx +++ b/src/core/PaperProvider.tsx @@ -7,6 +7,11 @@ import { defaultThemes, ThemeProvider } from './theming'; import MaterialCommunityIcon from '../components/MaterialCommunityIcon'; import PortalHost from '../components/Portal/PortalHost'; import { useAccessibleTheme } from '../theme/accessibility'; +import { + isDynamicColorSupported, + lightDynamicColors, + darkDynamicColors, +} from '../theme/schemes/DynamicTheme'; import type { Theme, ThemeProp } from '../types'; export type Props = { @@ -19,10 +24,17 @@ export type Props = { * accessibility in your own code. */ accessibilityAdapters?: boolean; + /** + * Whether to use Android Material You (dynamic) colors from the system wallpaper seed. + * When `true`, dynamic colors override `theme.colors` on Android API 31+. + * Falls back silently on unsupported platforms and API levels. + * Set to `false` (default) when providing a fully custom color theme. + */ + dynamicColor?: boolean; }; const PaperProvider = (props: Props) => { - const { accessibilityAdapters = true } = props; + const { accessibilityAdapters = true, dynamicColor = false } = props; const colorSchemeName = (!props.theme && Appearance?.getColorScheme()) || 'light'; @@ -59,17 +71,28 @@ const PaperProvider = (props: Props) => { }, [props.theme]); const rawTheme = React.useMemo(() => { - const scheme = colorScheme === 'dark' ? 'dark' : 'light'; + const effectiveDark = props.theme?.dark ?? colorScheme === 'dark'; + const scheme = effectiveDark ? 'dark' : 'light'; const base = defaultThemes[scheme]; + const isDynamic = dynamicColor && isDynamicColorSupported; + const dynamicColors = isDynamic + ? scheme === 'dark' + ? darkDynamicColors + : lightDynamicColors + : null; return { ...base, ...props.theme, + ...(dynamicColors + ? { colors: { ...base.colors, ...dynamicColors } } + : {}), animation: { ...props.theme?.animation, scale: props.theme?.animation?.scale ?? 1, }, - } as Theme; - }, [colorScheme, props.theme]); + dynamic: isDynamic, + } as unknown as Theme; + }, [colorScheme, props.theme, dynamicColor]); const theme = useAccessibleTheme(rawTheme, accessibilityAdapters !== false); diff --git a/src/core/__tests__/PaperProvider.test.tsx b/src/core/__tests__/PaperProvider.test.tsx index 77e92bb042..410cd7104b 100644 --- a/src/core/__tests__/PaperProvider.test.tsx +++ b/src/core/__tests__/PaperProvider.test.tsx @@ -103,8 +103,8 @@ const createProvider = (theme?: ThemeProp) => { ); }; -const ExtendedLightTheme = { ...LightTheme } as ThemeProp; -const ExtendedDarkTheme = { ...DarkTheme } as ThemeProp; +const ExtendedLightTheme = { ...LightTheme, dynamic: false } as ThemeProp; +const ExtendedDarkTheme = { ...DarkTheme, dynamic: false } as ThemeProp; describe('PaperProvider', () => { beforeEach(() => { @@ -134,6 +134,7 @@ describe('PaperProvider', () => { expect(getByTestId('provider-child-view').props.theme).toStrictEqual({ ...LightTheme, animation: { scale: 1, defaultAnimationDuration: 250 }, + dynamic: false, }); }); @@ -229,8 +230,9 @@ describe('PaperProvider', () => { }, } as ThemeProp; const { getByTestId } = render(createProvider(customTheme)); - expect(getByTestId('provider-child-view').props.theme).toStrictEqual( - customTheme - ); + expect(getByTestId('provider-child-view').props.theme).toStrictEqual({ + ...customTheme, + dynamic: false, + }); }); }); diff --git a/src/deprecated.ts b/src/deprecated.ts index e294f6a44d..c6f361fd90 100644 --- a/src/deprecated.ts +++ b/src/deprecated.ts @@ -8,6 +8,7 @@ * Do not add anything here that isn't a deprecated alias. */ +import { useTheme } from './core/theming'; import { DarkTheme } from './theme/schemes/DarkTheme'; import { LightTheme } from './theme/schemes/LightTheme'; import { Palette } from './theme/tokens'; @@ -45,3 +46,13 @@ export type MD3Theme = Theme; * @deprecated Use `Elevation` instead. Will be removed in a future version. */ export type MD3Elevation = Elevation; + +/** + * @deprecated Use `LightTheme` instead. Will be removed in a future version. + */ +export const DefaultTheme = LightTheme; + +/** + * @deprecated Use `useTheme` instead. Will be removed in a future version. + */ +export { useTheme as useAppTheme }; diff --git a/src/index.tsx b/src/index.tsx index 8fc041b225..e652950d21 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,6 @@ export { useTheme, withTheme, ThemeProvider, - DefaultTheme, adaptNavigationTheme, } from './core/theming'; @@ -168,4 +167,6 @@ export { MD3TypescaleKey, type MD3Theme, type MD3Elevation, + DefaultTheme, + useAppTheme, } from './deprecated'; diff --git a/src/theme/provider.tsx b/src/theme/provider.tsx index 22056d9354..801130e5ad 100644 --- a/src/theme/provider.tsx +++ b/src/theme/provider.tsx @@ -3,7 +3,7 @@ import type { ComponentType } from 'react'; import { $DeepPartial, createTheming } from '@callstack/react-theme-provider'; import { DarkTheme, LightTheme } from './schemes'; -import type { InternalTheme, Theme, NavigationTheme } from '../types'; +import type { Theme, NavigationTheme } from '../types'; export const DefaultTheme = LightTheme; @@ -18,11 +18,11 @@ export function useTheme(overrides?: $DeepPartial) { } export const useInternalTheme = ( - themeOverrides: $DeepPartial | undefined -) => useAppTheme(themeOverrides); + themeOverrides: $DeepPartial | undefined +) => useAppTheme(themeOverrides); -export const withInternalTheme = ( - WrappedComponent: ComponentType & C +export const withInternalTheme = ( + WrappedComponent: ComponentType & C ) => withTheme(WrappedComponent); export const defaultThemes = { diff --git a/src/theme/schemes/DynamicTheme.android.tsx b/src/theme/schemes/DynamicTheme.android.tsx index 4840210024..608dd78c43 100644 --- a/src/theme/schemes/DynamicTheme.android.tsx +++ b/src/theme/schemes/DynamicTheme.android.tsx @@ -2,11 +2,11 @@ import { Platform, PlatformColor } from 'react-native'; import { DarkTheme } from './DarkTheme'; import { LightTheme } from './LightTheme'; +import { Palette } from '../tokens'; import { defaultState } from '../tokens/sys/state'; -import type { Theme } from '../types'; +import type { Theme, ThemeColors } from '../types'; -const isApi34 = (Platform.Version as number) >= 34; -const isApi31 = (Platform.Version as number) >= 31; +const apiLevel = Platform.Version as number; const ac = (name: string) => PlatformColor(`@android:color/${name}`) as unknown as string; @@ -14,492 +14,477 @@ const ac = (name: string) => /** * Picks the correct color value for the current Android API level. * - API 34+: uses the named role resource (system_*_light/dark) - * - API 31-33: uses the tonal accent resource (system_accent*_NNN), or ref - * - API < 31: uses the reference palette string from the base theme + * - API 31-33: uses the tonal accent resource (system_accent*_NNN), or ref when null + * - API < 31: uses the reference palette value + * + * Pass `null` as api31 for roles that have no @android:color/ resource on API31-33 + * (MCL @color/m3_ref_palette_* resources and error roles). Those fall through to ref. * @see https://github.com/material-components/material-components-android/blob/master/docs/theming/Color.md */ -const pick = (api34: string, api31: string, ref: string): string => - isApi34 ? ac(api34) : isApi31 && api31 != null ? ac(api31) : ref; +const pick = (api34: string, api31: string | null, ref: string): string => + apiLevel >= 34 + ? ac(api34) + : apiLevel >= 31 && api31 !== null + ? ac(api31) + : ref; // Known limitation: surface/container roles on API 31-33 use // @color/m3_ref_palette_dynamic_neutral_variant* (MCL resources that require a // native DynamicColors setup). No @android:color/ equivalent exists for those // slots. Reference palette values are used as fallback on API 31-33. -const lightColors = { - primary: pick( - 'system_primary_light', - 'system_accent1_600', - LightTheme.colors.primary - ), - onPrimary: pick( - 'system_on_primary_light', - 'system_accent1_0', - LightTheme.colors.onPrimary - ), - primaryContainer: pick( - 'system_primary_container_light', - 'system_accent1_100', - LightTheme.colors.primaryContainer - ), - onPrimaryContainer: pick( - 'system_on_primary_container_light', - 'system_accent1_900', - LightTheme.colors.onPrimaryContainer - ), - inversePrimary: pick( - 'system_primary_dark', - 'system_accent1_200', - LightTheme.colors.inversePrimary - ), - secondary: pick( - 'system_secondary_light', - 'system_accent2_600', - LightTheme.colors.secondary - ), - onSecondary: pick( - 'system_on_secondary_light', - 'system_accent2_0', - LightTheme.colors.onSecondary - ), - secondaryContainer: pick( - 'system_secondary_container_light', - 'system_accent2_100', - LightTheme.colors.secondaryContainer - ), - onSecondaryContainer: pick( - 'system_on_secondary_container_light', - 'system_accent2_900', - LightTheme.colors.onSecondaryContainer - ), - tertiary: pick( - 'system_tertiary_light', - 'system_accent3_600', - LightTheme.colors.tertiary - ), - onTertiary: pick( - 'system_on_tertiary_light', - 'system_accent3_0', - LightTheme.colors.onTertiary - ), - tertiaryContainer: pick( - 'system_tertiary_container_light', - 'system_accent3_100', - LightTheme.colors.tertiaryContainer - ), - onTertiaryContainer: pick( - 'system_on_tertiary_container_light', - 'system_accent3_900', - LightTheme.colors.onTertiaryContainer - ), - error: pick( - 'system_error_light', - LightTheme.colors.error, - LightTheme.colors.error - ), - onError: pick( - 'system_on_error_light', - LightTheme.colors.onError, - LightTheme.colors.onError - ), - errorContainer: pick( - 'system_error_container_light', - LightTheme.colors.errorContainer, - LightTheme.colors.errorContainer - ), - onErrorContainer: pick( - 'system_on_error_container_light', - LightTheme.colors.onErrorContainer, - LightTheme.colors.onErrorContainer - ), - onSurface: pick( - 'system_on_surface_light', - 'system_neutral1_900', - LightTheme.colors.onSurface - ), - onBackground: pick( - 'system_on_background_light', - 'system_neutral1_900', - LightTheme.colors.onBackground - ), - onSurfaceVariant: pick( - 'system_on_surface_variant_light', - 'system_neutral2_700', - LightTheme.colors.onSurfaceVariant - ), - outline: pick( - 'system_outline_light', - 'system_neutral2_500', - LightTheme.colors.outline - ), - outlineVariant: pick( - 'system_outline_variant_light', - 'system_neutral2_200', - LightTheme.colors.outlineVariant - ), - inverseSurface: pick( - 'system_surface_dark', - 'system_neutral1_800', - LightTheme.colors.inverseSurface - ), - inverseOnSurface: pick( - 'system_on_surface_dark', - 'system_neutral1_50', - LightTheme.colors.inverseOnSurface - ), - surfaceContainerLowest: pick( - 'system_surface_container_lowest_light', - 'system_neutral2_0', - LightTheme.colors.surfaceContainerLowest - ), - surfaceContainerLow: pick( - 'system_surface_container_low_light', - LightTheme.colors.surfaceContainerLow, - LightTheme.colors.surfaceContainerLow - ), - surfaceContainerHighest: pick( - 'system_surface_container_highest_light', - 'system_neutral2_100', - LightTheme.colors.surfaceContainerHighest - ), - surface: pick( - 'system_surface_light', - LightTheme.colors.surface, - LightTheme.colors.surface - ), - surfaceDim: pick( - 'system_surface_dim_light', - LightTheme.colors.surfaceDim, - LightTheme.colors.surfaceDim - ), - surfaceBright: pick( - 'system_surface_bright_light', - LightTheme.colors.surfaceBright, - LightTheme.colors.surfaceBright - ), - surfaceContainer: pick( - 'system_surface_container_light', - LightTheme.colors.surfaceContainer, - LightTheme.colors.surfaceContainer - ), - surfaceContainerHigh: pick( - 'system_surface_container_high_light', - LightTheme.colors.surfaceContainerHigh, - LightTheme.colors.surfaceContainerHigh - ), - background: pick( - 'system_background_light', - LightTheme.colors.background, - LightTheme.colors.background - ), - surfaceVariant: pick( - 'system_surface_variant_light', - LightTheme.colors.surfaceVariant, - LightTheme.colors.surfaceVariant - ), - primaryFixed: pick( - 'system_primary_fixed', - 'system_accent1_100', - LightTheme.colors.primaryFixed - ), - primaryFixedDim: pick( - 'system_primary_fixed_dim', - 'system_accent1_200', - LightTheme.colors.primaryFixedDim - ), - onPrimaryFixed: pick( - 'system_on_primary_fixed', - 'system_accent1_900', - LightTheme.colors.onPrimaryFixed - ), - onPrimaryFixedVariant: pick( - 'system_on_primary_fixed_variant', - 'system_accent1_700', - LightTheme.colors.onPrimaryFixedVariant - ), - secondaryFixed: pick( - 'system_secondary_fixed', - 'system_accent2_100', - LightTheme.colors.secondaryFixed - ), - secondaryFixedDim: pick( - 'system_secondary_fixed_dim', - 'system_accent2_200', - LightTheme.colors.secondaryFixedDim - ), - onSecondaryFixed: pick( - 'system_on_secondary_fixed', - 'system_accent2_900', - LightTheme.colors.onSecondaryFixed - ), - onSecondaryFixedVariant: pick( - 'system_on_secondary_fixed_variant', - 'system_accent2_700', - LightTheme.colors.onSecondaryFixedVariant - ), - tertiaryFixed: pick( - 'system_tertiary_fixed', - 'system_accent3_100', - LightTheme.colors.tertiaryFixed - ), - tertiaryFixedDim: pick( - 'system_tertiary_fixed_dim', - 'system_accent3_200', - LightTheme.colors.tertiaryFixedDim - ), - onTertiaryFixed: pick( - 'system_on_tertiary_fixed', - 'system_accent3_900', - LightTheme.colors.onTertiaryFixed - ), - onTertiaryFixedVariant: pick( - 'system_on_tertiary_fixed_variant', - 'system_accent3_700', - LightTheme.colors.onTertiaryFixedVariant - ), +type RoleEntry = { + role: keyof ThemeColors; + light: [string, string | null, string]; + dark: [string, string | null, string]; }; -const darkColors = { - primary: pick( - 'system_primary_dark', - 'system_accent1_200', - DarkTheme.colors.primary - ), - onPrimary: pick( - 'system_on_primary_dark', - 'system_accent1_800', - DarkTheme.colors.onPrimary - ), - primaryContainer: pick( - 'system_primary_container_dark', - 'system_accent1_700', - DarkTheme.colors.primaryContainer - ), - onPrimaryContainer: pick( - 'system_on_primary_container_dark', - 'system_accent1_100', - DarkTheme.colors.onPrimaryContainer - ), - inversePrimary: pick( - 'system_primary_light', - 'system_accent1_600', - DarkTheme.colors.inversePrimary - ), - secondary: pick( - 'system_secondary_dark', - 'system_accent2_200', - DarkTheme.colors.secondary - ), - onSecondary: pick( - 'system_on_secondary_dark', - 'system_accent2_800', - DarkTheme.colors.onSecondary - ), - secondaryContainer: pick( - 'system_secondary_container_dark', - 'system_accent2_700', - DarkTheme.colors.secondaryContainer - ), - onSecondaryContainer: pick( - 'system_on_secondary_container_dark', - 'system_accent2_100', - DarkTheme.colors.onSecondaryContainer - ), - tertiary: pick( - 'system_tertiary_dark', - 'system_accent3_200', - DarkTheme.colors.tertiary - ), - onTertiary: pick( - 'system_on_tertiary_dark', - 'system_accent3_800', - DarkTheme.colors.onTertiary - ), - tertiaryContainer: pick( - 'system_tertiary_container_dark', - 'system_accent3_700', - DarkTheme.colors.tertiaryContainer - ), - onTertiaryContainer: pick( - 'system_on_tertiary_container_dark', - 'system_accent3_100', - DarkTheme.colors.onTertiaryContainer - ), - error: pick( - 'system_error_dark', - DarkTheme.colors.error, - DarkTheme.colors.error - ), - onError: pick( - 'system_on_error_dark', - DarkTheme.colors.onError, - DarkTheme.colors.onError - ), - errorContainer: pick( - 'system_error_container_dark', - DarkTheme.colors.errorContainer, - DarkTheme.colors.errorContainer - ), - onErrorContainer: pick( - 'system_on_error_container_dark', - DarkTheme.colors.onErrorContainer, - DarkTheme.colors.onErrorContainer - ), - onSurface: pick( - 'system_on_surface_dark', - 'system_neutral1_100', - DarkTheme.colors.onSurface - ), - onBackground: pick( - 'system_on_background_dark', - 'system_neutral1_100', - DarkTheme.colors.onBackground - ), - onSurfaceVariant: pick( - 'system_on_surface_variant_dark', - 'system_neutral2_200', - DarkTheme.colors.onSurfaceVariant - ), - outline: pick( - 'system_outline_dark', - 'system_neutral2_400', - DarkTheme.colors.outline - ), - outlineVariant: pick( - 'system_outline_variant_dark', - 'system_neutral2_700', - DarkTheme.colors.outlineVariant - ), - inverseSurface: pick( - 'system_surface_light', - 'system_neutral1_100', - DarkTheme.colors.inverseSurface - ), - inverseOnSurface: pick( - 'system_on_surface_light', - 'system_neutral1_800', - DarkTheme.colors.inverseOnSurface - ), - surfaceContainerLowest: pick( - 'system_surface_container_lowest_dark', - DarkTheme.colors.surfaceContainerLowest, - DarkTheme.colors.surfaceContainerLowest - ), - surfaceContainerLow: pick( - 'system_surface_container_low_dark', - 'system_neutral2_900', - DarkTheme.colors.surfaceContainerLow - ), - surfaceContainerHighest: pick( - 'system_surface_container_highest_dark', - DarkTheme.colors.surfaceContainerHighest, - DarkTheme.colors.surfaceContainerHighest - ), - surface: pick( - 'system_surface_dark', - DarkTheme.colors.surface, - DarkTheme.colors.surface - ), - surfaceDim: pick( - 'system_surface_dim_dark', - DarkTheme.colors.surfaceDim, - DarkTheme.colors.surfaceDim - ), - surfaceBright: pick( - 'system_surface_bright_dark', - DarkTheme.colors.surfaceBright, - DarkTheme.colors.surfaceBright - ), - surfaceContainer: pick( - 'system_surface_container_dark', - DarkTheme.colors.surfaceContainer, - DarkTheme.colors.surfaceContainer - ), - surfaceContainerHigh: pick( - 'system_surface_container_high_dark', - DarkTheme.colors.surfaceContainerHigh, - DarkTheme.colors.surfaceContainerHigh - ), - background: pick( - 'system_background_dark', - DarkTheme.colors.background, - DarkTheme.colors.background - ), - surfaceVariant: pick( - 'system_surface_variant_dark', - DarkTheme.colors.surfaceVariant, - DarkTheme.colors.surfaceVariant - ), - primaryFixed: pick( - 'system_primary_fixed', - 'system_accent1_100', - DarkTheme.colors.primaryFixed - ), - primaryFixedDim: pick( - 'system_primary_fixed_dim', - 'system_accent1_200', - DarkTheme.colors.primaryFixedDim - ), - onPrimaryFixed: pick( - 'system_on_primary_fixed', - 'system_accent1_900', - DarkTheme.colors.onPrimaryFixed - ), - onPrimaryFixedVariant: pick( - 'system_on_primary_fixed_variant', - 'system_accent1_700', - DarkTheme.colors.onPrimaryFixedVariant - ), - secondaryFixed: pick( - 'system_secondary_fixed', - 'system_accent2_100', - DarkTheme.colors.secondaryFixed - ), - secondaryFixedDim: pick( - 'system_secondary_fixed_dim', - 'system_accent2_200', - DarkTheme.colors.secondaryFixedDim - ), - onSecondaryFixed: pick( - 'system_on_secondary_fixed', - 'system_accent2_900', - DarkTheme.colors.onSecondaryFixed - ), - onSecondaryFixedVariant: pick( - 'system_on_secondary_fixed_variant', - 'system_accent2_700', - DarkTheme.colors.onSecondaryFixedVariant - ), - tertiaryFixed: pick( - 'system_tertiary_fixed', - 'system_accent3_100', - DarkTheme.colors.tertiaryFixed - ), - tertiaryFixedDim: pick( - 'system_tertiary_fixed_dim', - 'system_accent3_200', - DarkTheme.colors.tertiaryFixedDim - ), - onTertiaryFixed: pick( - 'system_on_tertiary_fixed', - 'system_accent3_900', - DarkTheme.colors.onTertiaryFixed - ), - onTertiaryFixedVariant: pick( - 'system_on_tertiary_fixed_variant', - 'system_accent3_700', - DarkTheme.colors.onTertiaryFixedVariant - ), -}; +const colorRoleMap: RoleEntry[] = [ + // Primary family + { + role: 'primary', + light: ['system_primary_light', 'system_accent1_600', Palette.primary40], + dark: ['system_primary_dark', 'system_accent1_200', Palette.primary80], + }, + { + role: 'onPrimary', + light: ['system_on_primary_light', 'system_accent1_0', Palette.primary100], + dark: ['system_on_primary_dark', 'system_accent1_800', Palette.primary20], + }, + { + role: 'primaryContainer', + light: [ + 'system_primary_container_light', + 'system_accent1_100', + Palette.primary90, + ], + dark: [ + 'system_primary_container_dark', + 'system_accent1_700', + Palette.primary30, + ], + }, + { + role: 'onPrimaryContainer', + light: [ + 'system_on_primary_container_light', + 'system_accent1_900', + Palette.primary10, + ], + dark: [ + 'system_on_primary_container_dark', + 'system_accent1_100', + Palette.primary90, + ], + }, + { + role: 'inversePrimary', + light: ['system_primary_dark', 'system_accent1_200', Palette.primary80], + dark: ['system_primary_light', 'system_accent1_600', Palette.primary40], + }, + // Secondary family + { + role: 'secondary', + light: [ + 'system_secondary_light', + 'system_accent2_600', + Palette.secondary40, + ], + dark: ['system_secondary_dark', 'system_accent2_200', Palette.secondary80], + }, + { + role: 'onSecondary', + light: [ + 'system_on_secondary_light', + 'system_accent2_0', + Palette.secondary100, + ], + dark: [ + 'system_on_secondary_dark', + 'system_accent2_800', + Palette.secondary20, + ], + }, + { + role: 'secondaryContainer', + light: [ + 'system_secondary_container_light', + 'system_accent2_100', + Palette.secondary90, + ], + dark: [ + 'system_secondary_container_dark', + 'system_accent2_700', + Palette.secondary30, + ], + }, + { + role: 'onSecondaryContainer', + light: [ + 'system_on_secondary_container_light', + 'system_accent2_900', + Palette.secondary10, + ], + dark: [ + 'system_on_secondary_container_dark', + 'system_accent2_100', + Palette.secondary90, + ], + }, + // Tertiary family + { + role: 'tertiary', + light: ['system_tertiary_light', 'system_accent3_600', Palette.tertiary40], + dark: ['system_tertiary_dark', 'system_accent3_200', Palette.tertiary80], + }, + { + role: 'onTertiary', + light: [ + 'system_on_tertiary_light', + 'system_accent3_0', + Palette.tertiary100, + ], + dark: ['system_on_tertiary_dark', 'system_accent3_800', Palette.tertiary20], + }, + { + role: 'tertiaryContainer', + light: [ + 'system_tertiary_container_light', + 'system_accent3_100', + Palette.tertiary90, + ], + dark: [ + 'system_tertiary_container_dark', + 'system_accent3_700', + Palette.tertiary30, + ], + }, + { + role: 'onTertiaryContainer', + light: [ + 'system_on_tertiary_container_light', + 'system_accent3_900', + Palette.tertiary10, + ], + dark: [ + 'system_on_tertiary_container_dark', + 'system_accent3_100', + Palette.tertiary90, + ], + }, + // Error family -- no @android:color/ resource on API31-33; null falls through to ref + { + role: 'error', + light: ['system_error_light', null, Palette.error40], + dark: ['system_error_dark', null, Palette.error80], + }, + { + role: 'onError', + light: ['system_on_error_light', null, Palette.error100], + dark: ['system_on_error_dark', null, Palette.error20], + }, + { + role: 'errorContainer', + light: ['system_error_container_light', null, Palette.error90], + dark: ['system_error_container_dark', null, Palette.error30], + }, + { + role: 'onErrorContainer', + light: ['system_on_error_container_light', null, Palette.error10], + dark: ['system_on_error_container_dark', null, Palette.error90], + }, + // Neutral roles + { + role: 'onSurface', + light: [ + 'system_on_surface_light', + 'system_neutral1_900', + Palette.neutral10, + ], + dark: ['system_on_surface_dark', 'system_neutral1_100', Palette.neutral90], + }, + { + role: 'onBackground', + light: [ + 'system_on_background_light', + 'system_neutral1_900', + Palette.neutral10, + ], + dark: [ + 'system_on_background_dark', + 'system_neutral1_100', + Palette.neutral90, + ], + }, + { + role: 'onSurfaceVariant', + light: [ + 'system_on_surface_variant_light', + 'system_neutral2_700', + Palette.neutralVariant30, + ], + dark: [ + 'system_on_surface_variant_dark', + 'system_neutral2_200', + Palette.neutralVariant80, + ], + }, + { + role: 'outline', + light: [ + 'system_outline_light', + 'system_neutral2_500', + Palette.neutralVariant50, + ], + dark: [ + 'system_outline_dark', + 'system_neutral2_400', + Palette.neutralVariant60, + ], + }, + { + role: 'outlineVariant', + light: [ + 'system_outline_variant_light', + 'system_neutral2_200', + Palette.neutralVariant80, + ], + dark: [ + 'system_outline_variant_dark', + 'system_neutral2_700', + Palette.neutralVariant30, + ], + }, + { + role: 'inverseSurface', + light: ['system_surface_dark', 'system_neutral1_800', Palette.neutral20], + dark: ['system_surface_light', 'system_neutral1_100', Palette.neutral90], + }, + { + role: 'inverseOnSurface', + light: ['system_on_surface_dark', 'system_neutral1_50', Palette.neutral95], + dark: ['system_on_surface_light', 'system_neutral1_800', Palette.neutral20], + }, + { + role: 'surfaceVariant', + light: [ + 'system_surface_variant_light', + 'system_neutral2_100', + Palette.neutralVariant90, + ], + dark: [ + 'system_surface_variant_dark', + 'system_neutral2_700', + Palette.neutralVariant30, + ], + }, + // Surface/background family -- API31-33 uses MCL @color/m3_ref_palette_* (not @android:color/); null falls through to ref + { + role: 'background', + light: ['system_background_light', null, Palette.neutral98], + dark: ['system_background_dark', null, Palette.neutral6], + }, + { + role: 'surface', + light: ['system_surface_light', null, Palette.neutral98], + dark: ['system_surface_dark', null, Palette.neutral6], + }, + { + role: 'surfaceBright', + light: ['system_surface_bright_light', null, Palette.neutral98], + dark: ['system_surface_bright_dark', null, Palette.neutral24], + }, + { + role: 'surfaceDim', + light: ['system_surface_dim_light', null, Palette.neutral87], + dark: ['system_surface_dim_dark', null, Palette.neutral6], + }, + { + role: 'surfaceContainer', + light: ['system_surface_container_light', null, Palette.neutral94], + dark: ['system_surface_container_dark', null, Palette.neutral12], + }, + { + role: 'surfaceContainerLow', + light: ['system_surface_container_low_light', null, Palette.neutral96], + dark: [ + 'system_surface_container_low_dark', + 'system_neutral2_900', + Palette.neutral10, + ], + }, + { + role: 'surfaceContainerLowest', + light: [ + 'system_surface_container_lowest_light', + 'system_neutral2_0', + Palette.neutral100, + ], + dark: ['system_surface_container_lowest_dark', null, Palette.neutral4], + }, + { + role: 'surfaceContainerHigh', + light: ['system_surface_container_high_light', null, Palette.neutral92], + dark: ['system_surface_container_high_dark', null, Palette.neutral17], + }, + { + role: 'surfaceContainerHighest', + light: [ + 'system_surface_container_highest_light', + 'system_neutral2_100', + Palette.neutral90, + ], + dark: ['system_surface_container_highest_dark', null, Palette.neutral22], + }, + // Fixed roles: same api34/api31 for both light and dark schemes + { + role: 'primaryFixed', + light: ['system_primary_fixed', 'system_accent1_100', Palette.primary90], + dark: ['system_primary_fixed', 'system_accent1_100', Palette.primary90], + }, + { + role: 'primaryFixedDim', + light: [ + 'system_primary_fixed_dim', + 'system_accent1_200', + Palette.primary80, + ], + dark: ['system_primary_fixed_dim', 'system_accent1_200', Palette.primary80], + }, + { + role: 'onPrimaryFixed', + light: ['system_on_primary_fixed', 'system_accent1_900', Palette.primary10], + dark: ['system_on_primary_fixed', 'system_accent1_900', Palette.primary10], + }, + { + role: 'onPrimaryFixedVariant', + light: [ + 'system_on_primary_fixed_variant', + 'system_accent1_700', + Palette.primary30, + ], + dark: [ + 'system_on_primary_fixed_variant', + 'system_accent1_700', + Palette.primary30, + ], + }, + { + role: 'secondaryFixed', + light: [ + 'system_secondary_fixed', + 'system_accent2_100', + Palette.secondary90, + ], + dark: ['system_secondary_fixed', 'system_accent2_100', Palette.secondary90], + }, + { + role: 'secondaryFixedDim', + light: [ + 'system_secondary_fixed_dim', + 'system_accent2_200', + Palette.secondary80, + ], + dark: [ + 'system_secondary_fixed_dim', + 'system_accent2_200', + Palette.secondary80, + ], + }, + { + role: 'onSecondaryFixed', + light: [ + 'system_on_secondary_fixed', + 'system_accent2_900', + Palette.secondary10, + ], + dark: [ + 'system_on_secondary_fixed', + 'system_accent2_900', + Palette.secondary10, + ], + }, + { + role: 'onSecondaryFixedVariant', + light: [ + 'system_on_secondary_fixed_variant', + 'system_accent2_700', + Palette.secondary30, + ], + dark: [ + 'system_on_secondary_fixed_variant', + 'system_accent2_700', + Palette.secondary30, + ], + }, + { + role: 'tertiaryFixed', + light: ['system_tertiary_fixed', 'system_accent3_100', Palette.tertiary90], + dark: ['system_tertiary_fixed', 'system_accent3_100', Palette.tertiary90], + }, + { + role: 'tertiaryFixedDim', + light: [ + 'system_tertiary_fixed_dim', + 'system_accent3_200', + Palette.tertiary80, + ], + dark: [ + 'system_tertiary_fixed_dim', + 'system_accent3_200', + Palette.tertiary80, + ], + }, + { + role: 'onTertiaryFixed', + light: [ + 'system_on_tertiary_fixed', + 'system_accent3_900', + Palette.tertiary10, + ], + dark: [ + 'system_on_tertiary_fixed', + 'system_accent3_900', + Palette.tertiary10, + ], + }, + { + role: 'onTertiaryFixedVariant', + light: [ + 'system_on_tertiary_fixed_variant', + 'system_accent3_700', + Palette.tertiary30, + ], + dark: [ + 'system_on_tertiary_fixed_variant', + 'system_accent3_700', + Palette.tertiary30, + ], + }, +]; + +function buildDynamicColors(scheme: 'light' | 'dark'): Partial { + return Object.fromEntries( + colorRoleMap.map(({ role, [scheme]: [api34, api31, ref] }) => [ + role, + pick(api34, api31, ref), + ]) + ) as Partial; +} + +export const isDynamicColorSupported = apiLevel >= 31; + +export const lightDynamicColors = buildDynamicColors('light'); +export const darkDynamicColors = buildDynamicColors('dark'); export const DynamicLightTheme: Theme = { ...LightTheme, - colors: { ...LightTheme.colors, ...lightColors }, + colors: { ...LightTheme.colors, ...lightDynamicColors }, state: defaultState, }; export const DynamicDarkTheme: Theme = { ...DarkTheme, - colors: { ...DarkTheme.colors, ...darkColors }, + colors: { ...DarkTheme.colors, ...darkDynamicColors }, state: defaultState, }; diff --git a/src/theme/schemes/DynamicTheme.tsx b/src/theme/schemes/DynamicTheme.tsx index d932f5a624..bdeb2649fd 100644 --- a/src/theme/schemes/DynamicTheme.tsx +++ b/src/theme/schemes/DynamicTheme.tsx @@ -1,2 +1,8 @@ +import type { ThemeColors } from '../types/color'; + export { DarkTheme as DynamicDarkTheme } from './DarkTheme'; export { LightTheme as DynamicLightTheme } from './LightTheme'; + +export const isDynamicColorSupported = false; +export const lightDynamicColors: Partial = {}; +export const darkDynamicColors: Partial = {}; diff --git a/src/theme/schemes/index.ts b/src/theme/schemes/index.ts index 4063fcba08..37407657e2 100644 --- a/src/theme/schemes/index.ts +++ b/src/theme/schemes/index.ts @@ -1,3 +1,7 @@ export { LightTheme } from './LightTheme'; export { DarkTheme } from './DarkTheme'; -export { DynamicLightTheme, DynamicDarkTheme } from './DynamicTheme'; +export { + DynamicLightTheme, + DynamicDarkTheme, + isDynamicColorSupported, +} from './DynamicTheme'; diff --git a/src/theme/types/theme.ts b/src/theme/types/theme.ts index 574e361cea..7b534de065 100644 --- a/src/theme/types/theme.ts +++ b/src/theme/types/theme.ts @@ -7,13 +7,10 @@ import type { ThemeShapes } from './shape'; import type { ThemeState } from './state'; import type { Typescale } from './typography'; -/** @deprecated Will be removed in a future version. MD3 uses tonal surface colors via `theme.colors.elevation.*`. */ -type Mode = 'adaptive' | 'exact'; - -export type ThemeBase = { +export type Theme = { dark: boolean; /** @deprecated Will be removed in a future version. MD3 uses tonal surface colors via `theme.colors.elevation.*`. */ - mode?: Mode; + mode?: 'adaptive' | 'exact'; /** @deprecated Use `theme.shapes.*` instead. Will be removed in a future version. */ roundness: number; /** @deprecated Use `theme.motion.*` instead. Will be removed in a future version. */ @@ -23,17 +20,22 @@ export type ThemeBase = { /** @deprecated No-op. Use `theme.motion.duration.*` instead. Will be removed in a future version. */ defaultAnimationDuration?: number; }; -}; - -export type Theme = ThemeBase & { colors: ThemeColors; fonts: Typescale; state: ThemeState; shapes: ThemeShapes; motion: MotionConfig; elevation: ThemeElevation; + /** True when Android Material You (dynamic) colors are active via `PaperProvider`'s `dynamicColor` prop. */ + dynamic?: boolean; }; +/** @deprecated Use `Theme` instead. Will be removed in a future version. */ +export type ThemeBase = Pick< + Theme, + 'dark' | 'mode' | 'roundness' | 'animation' +>; + export type InternalTheme = Theme; -export type ThemeProp = $DeepPartial; +export type ThemeProp = $DeepPartial;