diff --git a/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap b/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap index 80b6cc6c20..7149535a56 100644 --- a/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/ListSection.test.tsx.snap @@ -188,6 +188,16 @@ exports[`renders list section with custom title style 1`] = ` }, }, "roundness": 4, + "state": { + "opacity": { + "disabled": 0.38, + "dragged": 0.16, + "enabled": 1, + "focused": 0.1, + "hovered": 0.08, + "pressed": 0.1, + }, + }, } } > @@ -730,6 +740,16 @@ exports[`renders list section with subheader 1`] = ` }, }, "roundness": 4, + "state": { + "opacity": { + "disabled": 0.38, + "dragged": 0.16, + "enabled": 1, + "focused": 0.1, + "hovered": 0.08, + "pressed": 0.1, + }, + }, } } > @@ -1270,6 +1290,16 @@ exports[`renders list section without subheader 1`] = ` }, }, "roundness": 4, + "state": { + "opacity": { + "disabled": 0.38, + "dragged": 0.16, + "enabled": 1, + "focused": 0.1, + "hovered": 0.08, + "pressed": 0.1, + }, + }, } } > diff --git a/src/theme/schemes/DarkTheme.tsx b/src/theme/schemes/DarkTheme.tsx index 85ca245e57..2bb6eacf5d 100644 --- a/src/theme/schemes/DarkTheme.tsx +++ b/src/theme/schemes/DarkTheme.tsx @@ -1,6 +1,7 @@ import { baseTheme } from './base'; import { tokens } from '../tokens'; import { buildScheme } from '../tokens/sys/color/roles'; +import { defaultState } from '../tokens/sys/state'; import type { Theme } from '../types'; export const DarkTheme: Theme = { @@ -8,4 +9,5 @@ export const DarkTheme: Theme = { dark: true, mode: 'adaptive', colors: buildScheme(tokens.md.ref.palette, tokens.md.ref, { mode: 'dark' }), + state: defaultState, }; diff --git a/src/theme/schemes/DynamicTheme.android.tsx b/src/theme/schemes/DynamicTheme.android.tsx index 84667f5b79..4840210024 100644 --- a/src/theme/schemes/DynamicTheme.android.tsx +++ b/src/theme/schemes/DynamicTheme.android.tsx @@ -2,6 +2,7 @@ import { Platform, PlatformColor } from 'react-native'; import { DarkTheme } from './DarkTheme'; import { LightTheme } from './LightTheme'; +import { defaultState } from '../tokens/sys/state'; import type { Theme } from '../types'; const isApi34 = (Platform.Version as number) >= 34; @@ -494,9 +495,11 @@ const darkColors = { export const DynamicLightTheme: Theme = { ...LightTheme, colors: { ...LightTheme.colors, ...lightColors }, + state: defaultState, }; export const DynamicDarkTheme: Theme = { ...DarkTheme, colors: { ...DarkTheme.colors, ...darkColors }, + state: defaultState, }; diff --git a/src/theme/schemes/LightTheme.tsx b/src/theme/schemes/LightTheme.tsx index d60f1a7709..c63324fa63 100644 --- a/src/theme/schemes/LightTheme.tsx +++ b/src/theme/schemes/LightTheme.tsx @@ -1,10 +1,12 @@ import { baseTheme } from './base'; import { tokens } from '../tokens'; import { buildScheme } from '../tokens/sys/color/roles'; +import { defaultState } from '../tokens/sys/state'; import type { Theme } from '../types'; export const LightTheme: Theme = { ...baseTheme, dark: false, colors: buildScheme(tokens.md.ref.palette, tokens.md.ref, { mode: 'light' }), + state: defaultState, }; diff --git a/src/theme/tokens/index.ts b/src/theme/tokens/index.ts index ecc35ffa24..e8f041163e 100644 --- a/src/theme/tokens/index.ts +++ b/src/theme/tokens/index.ts @@ -122,8 +122,8 @@ const ref = { stateOpacity: { dragged: 0.16, pressed: 0.1, - focus: 0.1, - hover: 0.08, + focused: 0.1, + hovered: 0.08, disabled: 0.38, enabled: 1.0, }, diff --git a/src/theme/tokens/sys/state.ts b/src/theme/tokens/sys/state.ts new file mode 100644 index 0000000000..88dc40d041 --- /dev/null +++ b/src/theme/tokens/sys/state.ts @@ -0,0 +1,15 @@ +import type { ThemeState } from '../../types'; +import { tokens } from '../index'; + +const { stateOpacity } = tokens.md.ref; + +export const defaultState: ThemeState = { + opacity: { + hovered: stateOpacity.hovered, + focused: stateOpacity.focused, + pressed: stateOpacity.pressed, + dragged: stateOpacity.dragged, + disabled: stateOpacity.disabled, + enabled: stateOpacity.enabled, + }, +}; diff --git a/src/theme/types.ts b/src/theme/types.ts deleted file mode 100644 index b424142b86..0000000000 --- a/src/theme/types.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type * as React from 'react'; - -import type { $DeepPartial } from '@callstack/react-theme-provider'; - -export type Font = { - fontFamily: string; - fontWeight?: - | 'normal' - | 'bold' - | '100' - | '200' - | '300' - | '400' - | '500' - | '600' - | '700' - | '800' - | '900'; - fontStyle?: 'normal' | 'italic' | undefined; -}; - -export type Fonts = { - regular: Font; - medium: Font; - light: Font; - thin: Font; -}; - -type Mode = 'adaptive' | 'exact'; - -export type ThemeColors = { - primary: string; - primaryContainer: string; - secondary: string; - secondaryContainer: string; - tertiary: string; - tertiaryContainer: string; - surface: string; - surfaceDim: string; - surfaceBright: string; - surfaceContainerLowest: string; - surfaceContainerLow: string; - surfaceContainer: string; - surfaceContainerHigh: string; - surfaceContainerHighest: string; - surfaceVariant: string; - background: string; - error: string; - errorContainer: string; - onPrimary: string; - onPrimaryContainer: string; - onSecondary: string; - onSecondaryContainer: string; - onTertiary: string; - onTertiaryContainer: string; - onSurface: string; - onSurfaceVariant: string; - onError: string; - onErrorContainer: string; - onBackground: string; - outline: string; - outlineVariant: string; - inverseSurface: string; - inverseOnSurface: string; - inversePrimary: string; - primaryFixed: string; - primaryFixedDim: string; - onPrimaryFixed: string; - onPrimaryFixedVariant: string; - secondaryFixed: string; - secondaryFixedDim: string; - onSecondaryFixed: string; - onSecondaryFixedVariant: string; - tertiaryFixed: string; - tertiaryFixedDim: string; - onTertiaryFixed: string; - onTertiaryFixedVariant: string; - shadow: string; - scrim: string; - /** Pre-computed state layer color at press opacity (0.10). - * Used for ripple effects. Avoids runtime alpha manipulation - * which is incompatible with PlatformColor on Android. - * TODO: revisit after https://github.com/facebook/react-native/pull/56395 - * @see https://m3.material.io/foundations/interaction/states/state-layers */ - stateLayerPressed: string; - elevation: ElevationColors; -}; - -export type ThemeProp = $DeepPartial; - -export type ThemeBase = { - dark: boolean; - mode?: Mode; - roundness: number; - animation: { - scale: number; - defaultAnimationDuration?: number; - }; -}; - -export type Theme = ThemeBase & { - colors: ThemeColors; - fonts: Typescale; -}; - -export type InternalTheme = Theme; - -export enum TypescaleKey { - displayLarge = 'displayLarge', - displayMedium = 'displayMedium', - displaySmall = 'displaySmall', - - headlineLarge = 'headlineLarge', - headlineMedium = 'headlineMedium', - headlineSmall = 'headlineSmall', - - titleLarge = 'titleLarge', - titleMedium = 'titleMedium', - titleSmall = 'titleSmall', - - labelLarge = 'labelLarge', - labelMedium = 'labelMedium', - labelSmall = 'labelSmall', - - bodyLarge = 'bodyLarge', - bodyMedium = 'bodyMedium', - bodySmall = 'bodySmall', -} - -export type TypescaleStyle = { - fontFamily: string; - letterSpacing: number; - fontWeight: Font['fontWeight']; - lineHeight: number; - fontSize: number; - fontStyle?: Font['fontStyle']; -}; - -export type Typescale = - | { - [key in TypescaleKey]: TypescaleStyle; - } & { - ['default']: Omit; - }; - -export type Elevation = 0 | 1 | 2 | 3 | 4 | 5; - -export enum ElevationLevels { - 'level0', - 'level1', - 'level2', - 'level3', - 'level4', - 'level5', -} - -export type ElevationColors = { - [key in keyof typeof ElevationLevels]: string; -}; - -export type $Omit = Pick>; -export type $RemoveChildren> = $Omit< - React.ComponentPropsWithoutRef, - 'children' ->; - -export type EllipsizeProp = 'head' | 'middle' | 'tail' | 'clip'; - -export type NavigationTheme = { - dark: boolean; - colors: { - primary: string; - background: string; - card: string; - text: string; - border: string; - notification: string; - }; -}; diff --git a/src/theme/types/color.ts b/src/theme/types/color.ts new file mode 100644 index 0000000000..cc7ed49b86 --- /dev/null +++ b/src/theme/types/color.ts @@ -0,0 +1,59 @@ +import type { ElevationColors } from './elevation'; + +export type ThemeColors = { + primary: string; + primaryContainer: string; + secondary: string; + secondaryContainer: string; + tertiary: string; + tertiaryContainer: string; + surface: string; + surfaceDim: string; + surfaceBright: string; + surfaceContainerLowest: string; + surfaceContainerLow: string; + surfaceContainer: string; + surfaceContainerHigh: string; + surfaceContainerHighest: string; + surfaceVariant: string; + background: string; + error: string; + errorContainer: string; + onPrimary: string; + onPrimaryContainer: string; + onSecondary: string; + onSecondaryContainer: string; + onTertiary: string; + onTertiaryContainer: string; + onSurface: string; + onSurfaceVariant: string; + onError: string; + onErrorContainer: string; + onBackground: string; + outline: string; + outlineVariant: string; + inverseSurface: string; + inverseOnSurface: string; + inversePrimary: string; + primaryFixed: string; + primaryFixedDim: string; + onPrimaryFixed: string; + onPrimaryFixedVariant: string; + secondaryFixed: string; + secondaryFixedDim: string; + onSecondaryFixed: string; + onSecondaryFixedVariant: string; + tertiaryFixed: string; + tertiaryFixedDim: string; + onTertiaryFixed: string; + onTertiaryFixedVariant: string; + shadow: string; + scrim: string; + /** Pre-computed state layer color at press opacity (0.10). + * Used for ripple effects. Avoids runtime alpha manipulation + * which is incompatible with PlatformColor on Android. + * TODO: revisit after https://github.com/facebook/react-native/pull/56395 + * @see https://m3.material.io/foundations/interaction/states/state-layers */ + stateLayerPressed: string; + elevation: ElevationColors; +}; diff --git a/src/theme/types/elevation.ts b/src/theme/types/elevation.ts new file mode 100644 index 0000000000..9267a6c81b --- /dev/null +++ b/src/theme/types/elevation.ts @@ -0,0 +1,14 @@ +export type Elevation = 0 | 1 | 2 | 3 | 4 | 5; + +export enum ElevationLevels { + 'level0', + 'level1', + 'level2', + 'level3', + 'level4', + 'level5', +} + +export type ElevationColors = { + [key in keyof typeof ElevationLevels]: string; +}; diff --git a/src/theme/types/index.ts b/src/theme/types/index.ts new file mode 100644 index 0000000000..562a7ba943 --- /dev/null +++ b/src/theme/types/index.ts @@ -0,0 +1,7 @@ +export * from './color'; +export * from './elevation'; +export * from './navigation'; +export * from './state'; +export * from './theme'; +export * from './typography'; +export * from './utils'; diff --git a/src/theme/types/navigation.ts b/src/theme/types/navigation.ts new file mode 100644 index 0000000000..51d4a6f763 --- /dev/null +++ b/src/theme/types/navigation.ts @@ -0,0 +1,11 @@ +export type NavigationTheme = { + dark: boolean; + colors: { + primary: string; + background: string; + card: string; + text: string; + border: string; + notification: string; + }; +}; diff --git a/src/theme/types/state.ts b/src/theme/types/state.ts new file mode 100644 index 0000000000..6b7ba307cb --- /dev/null +++ b/src/theme/types/state.ts @@ -0,0 +1,11 @@ +export type InteractionState = + | 'hovered' + | 'focused' + | 'pressed' + | 'dragged' + | 'disabled' + | 'enabled'; + +export type ThemeState = { + opacity: Record; +}; diff --git a/src/theme/types/theme.ts b/src/theme/types/theme.ts new file mode 100644 index 0000000000..742b3db5c4 --- /dev/null +++ b/src/theme/types/theme.ts @@ -0,0 +1,27 @@ +import type { $DeepPartial } from '@callstack/react-theme-provider'; + +import type { ThemeColors } from './color'; +import type { ThemeState } from './state'; +import type { Typescale } from './typography'; + +type Mode = 'adaptive' | 'exact'; + +export type ThemeBase = { + dark: boolean; + mode?: Mode; + roundness: number; + animation: { + scale: number; + defaultAnimationDuration?: number; + }; +}; + +export type Theme = ThemeBase & { + colors: ThemeColors; + fonts: Typescale; + state: ThemeState; +}; + +export type InternalTheme = Theme; + +export type ThemeProp = $DeepPartial; diff --git a/src/theme/types/typography.ts b/src/theme/types/typography.ts new file mode 100644 index 0000000000..96b34b0815 --- /dev/null +++ b/src/theme/types/typography.ts @@ -0,0 +1,61 @@ +export type Font = { + fontFamily: string; + fontWeight?: + | 'normal' + | 'bold' + | '100' + | '200' + | '300' + | '400' + | '500' + | '600' + | '700' + | '800' + | '900'; + fontStyle?: 'normal' | 'italic' | undefined; +}; + +export type Fonts = { + regular: Font; + medium: Font; + light: Font; + thin: Font; +}; + +export enum TypescaleKey { + displayLarge = 'displayLarge', + displayMedium = 'displayMedium', + displaySmall = 'displaySmall', + + headlineLarge = 'headlineLarge', + headlineMedium = 'headlineMedium', + headlineSmall = 'headlineSmall', + + titleLarge = 'titleLarge', + titleMedium = 'titleMedium', + titleSmall = 'titleSmall', + + labelLarge = 'labelLarge', + labelMedium = 'labelMedium', + labelSmall = 'labelSmall', + + bodyLarge = 'bodyLarge', + bodyMedium = 'bodyMedium', + bodySmall = 'bodySmall', +} + +export type TypescaleStyle = { + fontFamily: string; + letterSpacing: number; + fontWeight: Font['fontWeight']; + lineHeight: number; + fontSize: number; + fontStyle?: Font['fontStyle']; +}; + +export type Typescale = + | { + [key in TypescaleKey]: TypescaleStyle; + } & { + ['default']: Omit; + }; diff --git a/src/theme/types/utils.ts b/src/theme/types/utils.ts new file mode 100644 index 0000000000..0ea6b262b9 --- /dev/null +++ b/src/theme/types/utils.ts @@ -0,0 +1,9 @@ +import type * as React from 'react'; + +export type $Omit = Pick>; +export type $RemoveChildren> = $Omit< + React.ComponentPropsWithoutRef, + 'children' +>; + +export type EllipsizeProp = 'head' | 'middle' | 'tail' | 'clip';