Skip to content

Commit 1f1102d

Browse files
authored
refactor: add color role table and buildScheme (#4916)
Adds src/theme/tokens/sys/color/roles.ts with a roleToTone table keyed by (mode, contrast) and a buildScheme() function. LightTheme and DarkTheme replace their hand-duplicated color literals with a single buildScheme() call each. Output shape is identical; tone values match the existing hand-written spec.
1 parent 2a0072e commit 1f1102d

7 files changed

Lines changed: 202 additions & 145 deletions

File tree

src/theme/fonts.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { TypescaleStyle, Typescale, TypescaleKey } from '../types';
21
import { typescale } from './tokens';
2+
import type { TypescaleStyle, Typescale, TypescaleKey } from './types';
33

44
type FontsConfig =
55
| {

src/theme/schemes/DarkTheme.tsx

Lines changed: 5 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,11 @@
1-
import color from 'color';
2-
3-
import { LightTheme } from './LightTheme';
4-
import type { Theme } from '../../types';
1+
import { baseTheme } from './base';
52
import { tokens } from '../tokens';
6-
7-
const { palette, stateOpacity } = tokens.md.ref;
3+
import { buildScheme } from '../tokens/sys/color/roles';
4+
import type { Theme } from '../types';
85

96
export const DarkTheme: Theme = {
10-
...LightTheme,
7+
...baseTheme,
118
dark: true,
129
mode: 'adaptive',
13-
colors: {
14-
primary: palette.primary80,
15-
primaryContainer: palette.primary30,
16-
secondary: palette.secondary80,
17-
secondaryContainer: palette.secondary30,
18-
tertiary: palette.tertiary80,
19-
tertiaryContainer: palette.tertiary30,
20-
surface: palette.neutral6,
21-
surfaceDim: palette.neutral6,
22-
surfaceBright: palette.neutral24,
23-
surfaceContainerLowest: palette.neutral4,
24-
surfaceContainerLow: palette.neutral10,
25-
surfaceContainer: palette.neutral12,
26-
surfaceContainerHigh: palette.neutral17,
27-
surfaceContainerHighest: palette.neutral22,
28-
surfaceVariant: palette.neutralVariant30,
29-
background: palette.neutral6,
30-
error: palette.error80,
31-
errorContainer: palette.error30,
32-
onPrimary: palette.primary20,
33-
onPrimaryContainer: palette.primary90,
34-
onSecondary: palette.secondary20,
35-
onSecondaryContainer: palette.secondary90,
36-
onTertiary: palette.tertiary20,
37-
onTertiaryContainer: palette.tertiary90,
38-
onSurface: palette.neutral90,
39-
onSurfaceVariant: palette.neutralVariant80,
40-
onError: palette.error20,
41-
onErrorContainer: palette.error80,
42-
onBackground: palette.neutral90,
43-
outline: palette.neutralVariant60,
44-
outlineVariant: palette.neutralVariant30,
45-
inverseSurface: palette.neutral90,
46-
inverseOnSurface: palette.neutral20,
47-
inversePrimary: palette.primary40,
48-
primaryFixed: palette.primary90,
49-
primaryFixedDim: palette.primary80,
50-
onPrimaryFixed: palette.primary10,
51-
onPrimaryFixedVariant: palette.primary30,
52-
secondaryFixed: palette.secondary90,
53-
secondaryFixedDim: palette.secondary80,
54-
onSecondaryFixed: palette.secondary10,
55-
onSecondaryFixedVariant: palette.secondary30,
56-
tertiaryFixed: palette.tertiary90,
57-
tertiaryFixedDim: palette.tertiary80,
58-
onTertiaryFixed: palette.tertiary10,
59-
onTertiaryFixedVariant: palette.tertiary30,
60-
shadow: palette.neutral0,
61-
scrim: palette.neutral0,
62-
stateLayerPressed: color(palette.neutral90)
63-
.alpha(stateOpacity.pressed)
64-
.rgb()
65-
.string(),
66-
elevation: {
67-
level0: 'transparent',
68-
level1: palette.neutral10,
69-
level2: palette.neutral12,
70-
level3: palette.neutral17,
71-
level4: palette.neutral17,
72-
level5: palette.neutral22,
73-
},
74-
},
10+
colors: buildScheme(tokens.md.ref.palette, tokens.md.ref, { mode: 'dark' }),
7511
};

src/theme/schemes/DynamicTheme.android.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Platform, PlatformColor } from 'react-native';
22

33
import { DarkTheme } from './DarkTheme';
44
import { LightTheme } from './LightTheme';
5-
import type { Theme } from '../../types';
5+
import type { Theme } from '../types';
66

77
const isApi34 = (Platform.Version as number) >= 34;
88
const isApi31 = (Platform.Version as number) >= 31;

src/theme/schemes/LightTheme.tsx

Lines changed: 5 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,10 @@
1-
import color from 'color';
2-
3-
import type { Theme } from '../../types';
4-
import configureFonts from '../fonts';
1+
import { baseTheme } from './base';
52
import { tokens } from '../tokens';
6-
7-
const { palette, stateOpacity } = tokens.md.ref;
3+
import { buildScheme } from '../tokens/sys/color/roles';
4+
import type { Theme } from '../types';
85

96
export const LightTheme: Theme = {
7+
...baseTheme,
108
dark: false,
11-
roundness: 4,
12-
colors: {
13-
primary: palette.primary40,
14-
primaryContainer: palette.primary90,
15-
secondary: palette.secondary40,
16-
secondaryContainer: palette.secondary90,
17-
tertiary: palette.tertiary40,
18-
tertiaryContainer: palette.tertiary90,
19-
surface: palette.neutral98,
20-
surfaceDim: palette.neutral87,
21-
surfaceBright: palette.neutral98,
22-
surfaceContainerLowest: palette.neutral100,
23-
surfaceContainerLow: palette.neutral96,
24-
surfaceContainer: palette.neutral94,
25-
surfaceContainerHigh: palette.neutral92,
26-
surfaceContainerHighest: palette.neutral90,
27-
surfaceVariant: palette.neutralVariant90,
28-
background: palette.neutral98,
29-
error: palette.error40,
30-
errorContainer: palette.error90,
31-
onPrimary: palette.primary100,
32-
onPrimaryContainer: palette.primary10,
33-
onSecondary: palette.secondary100,
34-
onSecondaryContainer: palette.secondary10,
35-
onTertiary: palette.tertiary100,
36-
onTertiaryContainer: palette.tertiary10,
37-
onSurface: palette.neutral10,
38-
onSurfaceVariant: palette.neutralVariant30,
39-
onError: palette.error100,
40-
onErrorContainer: palette.error10,
41-
onBackground: palette.neutral10,
42-
outline: palette.neutralVariant50,
43-
outlineVariant: palette.neutralVariant80,
44-
inverseSurface: palette.neutral20,
45-
inverseOnSurface: palette.neutral95,
46-
inversePrimary: palette.primary80,
47-
primaryFixed: palette.primary90,
48-
primaryFixedDim: palette.primary80,
49-
onPrimaryFixed: palette.primary10,
50-
onPrimaryFixedVariant: palette.primary30,
51-
secondaryFixed: palette.secondary90,
52-
secondaryFixedDim: palette.secondary80,
53-
onSecondaryFixed: palette.secondary10,
54-
onSecondaryFixedVariant: palette.secondary30,
55-
tertiaryFixed: palette.tertiary90,
56-
tertiaryFixedDim: palette.tertiary80,
57-
onTertiaryFixed: palette.tertiary10,
58-
onTertiaryFixedVariant: palette.tertiary30,
59-
shadow: palette.neutral0,
60-
scrim: palette.neutral0,
61-
stateLayerPressed: color(palette.neutral10)
62-
.alpha(stateOpacity.pressed)
63-
.rgb()
64-
.string(),
65-
elevation: {
66-
level0: 'transparent',
67-
level1: palette.neutral96,
68-
level2: palette.neutral94,
69-
level3: palette.neutral92,
70-
level4: palette.neutral92,
71-
level5: palette.neutral90,
72-
},
73-
},
74-
fonts: configureFonts(),
75-
animation: {
76-
scale: 1.0,
77-
},
9+
colors: buildScheme(tokens.md.ref.palette, tokens.md.ref, { mode: 'light' }),
7810
};

src/theme/schemes/base.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import configureFonts from '../fonts';
2+
3+
export const baseTheme = {
4+
roundness: 4,
5+
fonts: configureFonts(),
6+
animation: {
7+
scale: 1.0,
8+
},
9+
};

src/theme/tokens/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Platform } from 'react-native';
22

3-
import type { Font } from '../../types';
3+
import type { Font } from '../types';
44

55
const ref = {
66
palette: {
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import color from 'color';
2+
3+
import type { ElevationColors, ThemeColors } from '../../../types';
4+
import { tokens } from '../../index';
5+
6+
type Palette = typeof tokens.md.ref.palette;
7+
type PaletteKey = keyof Palette;
8+
type Ref = typeof tokens.md.ref;
9+
10+
/** Roles that map 1:1 to a palette key. Excludes the computed fields. */
11+
type MappedRoles = Omit<ThemeColors, 'stateLayerPressed' | 'elevation'>;
12+
13+
type Contrast = 'standard'; // extend with 'medium' | 'high' when those ship
14+
15+
const roleToTone: Record<
16+
'light' | 'dark',
17+
Record<Contrast, Record<keyof MappedRoles, PaletteKey>>
18+
> = {
19+
light: {
20+
standard: {
21+
primary: 'primary40',
22+
onPrimary: 'primary100',
23+
primaryContainer: 'primary90',
24+
onPrimaryContainer: 'primary10',
25+
secondary: 'secondary40',
26+
onSecondary: 'secondary100',
27+
secondaryContainer: 'secondary90',
28+
onSecondaryContainer: 'secondary10',
29+
tertiary: 'tertiary40',
30+
onTertiary: 'tertiary100',
31+
tertiaryContainer: 'tertiary90',
32+
onTertiaryContainer: 'tertiary10',
33+
error: 'error40',
34+
onError: 'error100',
35+
errorContainer: 'error90',
36+
onErrorContainer: 'error10',
37+
surface: 'neutral98',
38+
surfaceDim: 'neutral87',
39+
surfaceBright: 'neutral98',
40+
surfaceContainerLowest: 'neutral100',
41+
surfaceContainerLow: 'neutral96',
42+
surfaceContainer: 'neutral94',
43+
surfaceContainerHigh: 'neutral92',
44+
surfaceContainerHighest: 'neutral90',
45+
surfaceVariant: 'neutralVariant90',
46+
background: 'neutral98',
47+
onSurface: 'neutral10',
48+
onSurfaceVariant: 'neutralVariant30',
49+
onBackground: 'neutral10',
50+
outline: 'neutralVariant50',
51+
outlineVariant: 'neutralVariant80',
52+
inverseSurface: 'neutral20',
53+
inverseOnSurface: 'neutral95',
54+
inversePrimary: 'primary80',
55+
primaryFixed: 'primary90',
56+
primaryFixedDim: 'primary80',
57+
onPrimaryFixed: 'primary10',
58+
onPrimaryFixedVariant: 'primary30',
59+
secondaryFixed: 'secondary90',
60+
secondaryFixedDim: 'secondary80',
61+
onSecondaryFixed: 'secondary10',
62+
onSecondaryFixedVariant: 'secondary30',
63+
tertiaryFixed: 'tertiary90',
64+
tertiaryFixedDim: 'tertiary80',
65+
onTertiaryFixed: 'tertiary10',
66+
onTertiaryFixedVariant: 'tertiary30',
67+
shadow: 'neutral0',
68+
scrim: 'neutral0',
69+
},
70+
},
71+
dark: {
72+
standard: {
73+
primary: 'primary80',
74+
onPrimary: 'primary20',
75+
primaryContainer: 'primary30',
76+
onPrimaryContainer: 'primary90',
77+
secondary: 'secondary80',
78+
onSecondary: 'secondary20',
79+
secondaryContainer: 'secondary30',
80+
onSecondaryContainer: 'secondary90',
81+
tertiary: 'tertiary80',
82+
onTertiary: 'tertiary20',
83+
tertiaryContainer: 'tertiary30',
84+
onTertiaryContainer: 'tertiary90',
85+
error: 'error80',
86+
onError: 'error20',
87+
errorContainer: 'error30',
88+
onErrorContainer: 'error80',
89+
surface: 'neutral6',
90+
surfaceDim: 'neutral6',
91+
surfaceBright: 'neutral24',
92+
surfaceContainerLowest: 'neutral4',
93+
surfaceContainerLow: 'neutral10',
94+
surfaceContainer: 'neutral12',
95+
surfaceContainerHigh: 'neutral17',
96+
surfaceContainerHighest: 'neutral22',
97+
surfaceVariant: 'neutralVariant30',
98+
background: 'neutral6',
99+
onSurface: 'neutral90',
100+
onSurfaceVariant: 'neutralVariant80',
101+
onBackground: 'neutral90',
102+
outline: 'neutralVariant60',
103+
outlineVariant: 'neutralVariant30',
104+
inverseSurface: 'neutral90',
105+
inverseOnSurface: 'neutral20',
106+
inversePrimary: 'primary40',
107+
primaryFixed: 'primary90',
108+
primaryFixedDim: 'primary80',
109+
onPrimaryFixed: 'primary10',
110+
onPrimaryFixedVariant: 'primary30',
111+
secondaryFixed: 'secondary90',
112+
secondaryFixedDim: 'secondary80',
113+
onSecondaryFixed: 'secondary10',
114+
onSecondaryFixedVariant: 'secondary30',
115+
tertiaryFixed: 'tertiary90',
116+
tertiaryFixedDim: 'tertiary80',
117+
onTertiaryFixed: 'tertiary10',
118+
onTertiaryFixedVariant: 'tertiary30',
119+
shadow: 'neutral0',
120+
scrim: 'neutral0',
121+
},
122+
},
123+
};
124+
125+
const elevationToTone: Record<
126+
'light' | 'dark',
127+
Record<Contrast, Record<Exclude<keyof ElevationColors, 'level0'>, PaletteKey>>
128+
> = {
129+
light: {
130+
standard: {
131+
level1: 'neutral96',
132+
level2: 'neutral94',
133+
level3: 'neutral92',
134+
level4: 'neutral92',
135+
level5: 'neutral90',
136+
},
137+
},
138+
dark: {
139+
standard: {
140+
level1: 'neutral10',
141+
level2: 'neutral12',
142+
level3: 'neutral17',
143+
level4: 'neutral17',
144+
level5: 'neutral22',
145+
},
146+
},
147+
};
148+
149+
export function buildScheme(
150+
palette: Palette,
151+
ref: Ref,
152+
opts: { mode: 'light' | 'dark'; contrast?: Contrast }
153+
): ThemeColors {
154+
const contrast = opts.contrast ?? 'standard';
155+
const tones = roleToTone[opts.mode][contrast];
156+
const elevTones = elevationToTone[opts.mode][contrast];
157+
158+
const mapped = Object.fromEntries(
159+
(Object.keys(tones) as Array<keyof typeof tones>).map((role) => [
160+
role,
161+
palette[tones[role]],
162+
])
163+
) as MappedRoles;
164+
165+
return {
166+
...mapped,
167+
stateLayerPressed: color(palette[tones.onSurface])
168+
.alpha(ref.stateOpacity.pressed)
169+
.rgb()
170+
.string(),
171+
elevation: {
172+
level0: 'transparent',
173+
level1: palette[elevTones.level1],
174+
level2: palette[elevTones.level2],
175+
level3: palette[elevTones.level3],
176+
level4: palette[elevTones.level4],
177+
level5: palette[elevTones.level5],
178+
},
179+
};
180+
}

0 commit comments

Comments
 (0)