Skip to content

Commit d55fbde

Browse files
refactor(theme): добавлен слой legacy-совместимости
1 parent b5e0ca1 commit d55fbde

40 files changed

Lines changed: 302 additions & 215 deletions

MIGRATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818

1919
export const Root = () => (
2020
<ThemeContextProvider
21-
fonts={{ primary: 'MyFont', secondary: 'MySecondaryFont' }}
21+
fonts={{ heading: 'MyFont', base: 'MySecondaryFont' }}
2222
initialTheme={ThemeVariant.Light}
2323
>
2424
<App />
@@ -45,7 +45,7 @@ const fonts = useFonts()
4545
import { StyleSheet } from 'react-native-unistyles'
4646

4747
const styles = StyleSheet.create(({ fonts }) => ({
48-
title: { fontFamily: fonts.primary },
48+
title: { fontFamily: fonts.fontFamily.heading },
4949
}))
5050
```
5151

README.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,26 +36,25 @@ npm i @cdek-it/react-native-ui-kit
3636

3737
UI kit использует следующие виды шрифтов.
3838

39-
| Тип шрифта | Используемые начертания | Рекомендуемый шрифт | Рекомендуемый аналог |
40-
| ----------- | ---------------------------------------------- | ------------------- | -------------------- |
41-
| `primary` | – regular<br/>– demibold<br/>– demibold italic | TT Fellows | Noto Sans |
42-
| `secondary` | – regular<br/>– bold | Noto Sans ||
39+
| Тип шрифта | Используемые начертания | Рекомендуемый шрифт | Рекомендуемый аналог |
40+
| ---------- | ---------------------------------------------- | ------------------- | -------------------- |
41+
| `heading` | – regular<br/>– demibold<br/>– demibold italic | TT Fellows | Noto Sans |
42+
| `base` | – regular<br/>– bold | Noto Sans ||
4343

4444
Исходники шрифтов не поставляются вместе с пакетом, их требуется подключать
45-
отдельно. После подключения шрифтов в проект, необходимо указать их в
46-
`ThemeContextProvider`.
45+
отдельно под именами, указанными в токенах.
4746

4847
```tsx
49-
<ThemeContextProvider
50-
fonts={{ primary: 'MyFont', secondary: 'MySecondaryFont' }}
51-
/>
48+
<ThemeContextProvider fonts={{ heading: 'MyFont', base: 'MySecondaryFont' }} />
5249
```
5350

5451
После этого шрифты доступны через `useUnistyles().theme.fonts` или прямо в
5552
`StyleSheet.create(({ fonts }) => ...)`.
5653

57-
`ThemeContextProvider` настраивает активную тему и шрифты для `unistyles`.
58-
`ThemeContext` остается пустым и всегда имеет значение `null`.
54+
`ThemeContextProvider` только настраивает темы и шрифты для `unistyles`.
55+
`ThemeContext` остается пустым и всегда имеет значение `null`. Deprecated-формат
56+
`fonts={{ primary, secondary }}` временно сохраняется для обратной
57+
совместимости.
5958

6059
### Пример подключения шрифтов с помощью expo-fonts через плагин
6160

src/hooks/useFonts.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { useUnistyles } from 'react-native-unistyles'
22

3-
import type { FontsConfig } from '../theme'
3+
import type { ThemeType } from '../theme/types'
44

5-
export const useFonts = (): FontsConfig => {
5+
export const useFonts = (): ThemeType['fonts'] => {
66
const { theme } = useUnistyles()
7-
const { primary, secondary } = theme.fonts
87

9-
return { primary, secondary }
8+
return theme.fonts
109
}

src/theme/ThemeContext.tsx

Lines changed: 12 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,56 +4,19 @@ import { StyleSheet, UnistylesRuntime } from 'react-native-unistyles'
44

55
import { SkeletonContextProvider } from '../utils/SkeletonContext'
66

7-
import { darkTheme } from './darkTheme'
8-
import { lightTheme } from './lightTheme'
9-
import {
10-
componentTokens,
11-
fontTokens,
12-
semanticTokens,
13-
type ComponentTokens,
14-
type SemanticTokens,
15-
} from './tokens'
16-
import {
17-
type FontsConfig,
18-
type FontTokens,
19-
type ThemeType,
20-
ThemeVariant,
21-
} from './types'
22-
23-
type InternalTheme = Omit<ThemeType, 'fonts' | 'semantic'> & {
24-
components: ComponentTokens
25-
fonts: FontsConfig & FontTokens
26-
semantic: ThemeType['semantic'] & SemanticTokens
27-
}
28-
29-
const createInternalTheme = (
30-
theme: ThemeType,
31-
components: ComponentTokens
32-
): InternalTheme => ({
33-
...theme,
34-
components,
35-
fonts: {
36-
...fontTokens,
37-
...theme.fonts,
38-
fontFamily: { base: theme.fonts.secondary, heading: theme.fonts.primary },
39-
},
40-
semantic: { ...semanticTokens, ...theme.semantic },
41-
})
42-
43-
const internalThemes = {
44-
light: createInternalTheme(lightTheme, componentTokens.light),
45-
dark: createInternalTheme(darkTheme, componentTokens.dark),
46-
}
7+
import { applyFontConfig, type FontsConfig } from './legacyTokens'
8+
import { darkTheme, lightTheme } from './themes'
9+
import { ThemeVariant } from './types'
4710

4811
StyleSheet.configure({
4912
settings: { initialTheme: 'light' },
50-
themes: internalThemes,
13+
themes: { light: lightTheme, dark: darkTheme },
5114
})
5215

5316
declare module 'react-native-unistyles' {
5417
export interface UnistylesThemes {
55-
light: (typeof internalThemes)['light']
56-
dark: (typeof internalThemes)['dark']
18+
light: typeof lightTheme
19+
dark: typeof darkTheme
5720
}
5821
}
5922

@@ -79,17 +42,12 @@ export const ThemeContextProvider = ({
7942
UnistylesRuntime.setTheme(THEME_NAME_MAP[initialTheme])
8043

8144
if (fonts) {
82-
const updateFonts = (theme: InternalTheme): InternalTheme => ({
83-
...theme,
84-
fonts: {
85-
...theme.fonts,
86-
...fonts,
87-
fontFamily: { base: fonts.secondary, heading: fonts.primary },
88-
},
89-
})
90-
91-
UnistylesRuntime.updateTheme('light', updateFonts)
92-
UnistylesRuntime.updateTheme('dark', updateFonts)
45+
UnistylesRuntime.updateTheme('light', (theme) =>
46+
applyFontConfig(theme, fonts)
47+
)
48+
UnistylesRuntime.updateTheme('dark', (theme) =>
49+
applyFontConfig(theme, fonts)
50+
)
9351
}
9452
// eslint-disable-next-line react-hooks/exhaustive-deps -- initial* props применяем только на mount
9553
}, [])

src/theme/__tests__/ThemeContext.test.tsx

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,10 @@ import { Text } from 'react-native'
44
import { UnistylesRuntime } from 'react-native-unistyles'
55

66
import { ThemeContextProvider } from '../ThemeContext'
7-
import { darkTheme } from '../darkTheme'
8-
import { lightTheme } from '../lightTheme'
7+
import { darkTheme, lightTheme } from '../themes'
98
import { componentTokens, fontTokens, semanticTokens } from '../tokens'
10-
import darkComponentTokens from '../tokens/components/dark.json'
11-
import lightComponentTokens from '../tokens/components/light.json'
129
import darkSemanticColorSchemeTokens from '../tokens/semantic/colorScheme/dark.json'
1310
import lightSemanticColorSchemeTokens from '../tokens/semantic/colorScheme/light.json'
14-
import semanticDimensions from '../tokens/semantic/dimensions.json'
15-
import semanticEffects from '../tokens/semantic/effects.json'
1611
import { ThemeVariant } from '../types'
1712

1813
describe('ThemeContextProvider', () => {
@@ -40,7 +35,7 @@ describe('ThemeContextProvider', () => {
4035
expect(UnistylesRuntime.setTheme).toHaveBeenCalledWith('light')
4136
})
4237

43-
test('при передаче fonts обновляет обе темы', () => {
38+
test('при legacy-формате fonts обновляет обе темы', () => {
4439
const fonts = { primary: 'Roboto', secondary: 'Inter' }
4540

4641
render(
@@ -59,14 +54,41 @@ describe('ThemeContextProvider', () => {
5954
)
6055

6156
const [, updater] = jest.mocked(UnistylesRuntime.updateTheme).mock.calls[0]
62-
const theme = UnistylesRuntime.getTheme('light')
57+
const theme = lightTheme
6358

6459
expect(updater(theme)).toStrictEqual({
6560
...theme,
6661
fonts: {
6762
...theme.fonts,
63+
fontFamily: {
64+
...theme.fonts.fontFamily,
65+
heading: fonts.primary,
66+
base: fonts.secondary,
67+
},
6868
...fonts,
69-
fontFamily: { base: fonts.secondary, heading: fonts.primary },
69+
},
70+
})
71+
})
72+
73+
test('при актуальном формате fonts обновляет обе темы', () => {
74+
const fonts = { heading: 'Roboto', base: 'Inter' }
75+
76+
render(
77+
<ThemeContextProvider fonts={fonts}>
78+
<Text>child</Text>
79+
</ThemeContextProvider>
80+
)
81+
82+
const [, updater] = jest.mocked(UnistylesRuntime.updateTheme).mock.calls[0]
83+
const theme = lightTheme
84+
85+
expect(updater(theme)).toStrictEqual({
86+
...theme,
87+
fonts: {
88+
...theme.fonts,
89+
fontFamily: { ...theme.fonts.fontFamily, ...fonts },
90+
primary: fonts.heading,
91+
secondary: fonts.base,
7092
},
7193
})
7294
})
@@ -81,51 +103,57 @@ describe('ThemeContextProvider', () => {
81103
expect(UnistylesRuntime.updateTheme).not.toHaveBeenCalled()
82104
})
83105

84-
test('каждая тема содержит только соответствующую цветовую схему', () => {
106+
test('каждая тема содержит соответствующие semantic-токены', () => {
85107
expect(lightTheme.semantic.colorScheme).toBe(lightSemanticColorSchemeTokens)
86108
expect(darkTheme.semantic.colorScheme).toBe(darkSemanticColorSchemeTokens)
87-
expect(lightTheme.semantic).not.toHaveProperty('dimension')
88-
expect(lightTheme.semantic).not.toHaveProperty('effects')
109+
expect(lightTheme.semantic.dimension).toBe(semanticTokens.dimension)
110+
expect(lightTheme.semantic.effects).toBe(semanticTokens.effects)
89111
})
90112

91113
test('общие semantic-токены не зависят от темы', () => {
92-
expect(semanticTokens.dimension).toBe(semanticDimensions)
93-
expect(semanticTokens.effects).toBe(semanticEffects)
114+
expect(lightTheme.semantic.dimension).toBe(darkTheme.semantic.dimension)
115+
expect(lightTheme.semantic.effects).toBe(darkTheme.semantic.effects)
94116
})
95117

96-
test('component-токены не входят в публичные темы', () => {
97-
expect(lightTheme).not.toHaveProperty('components')
98-
expect(darkTheme).not.toHaveProperty('components')
118+
test('каждая тема содержит соответствующие component-токены', () => {
119+
expect(lightTheme.components).toBe(componentTokens.light)
120+
expect(darkTheme.components).toBe(componentTokens.dark)
99121
})
100122

101-
test('внутренняя карта component-токенов содержит обе темы', () => {
102-
expect(componentTokens.light).toBe(lightComponentTokens)
103-
expect(componentTokens.dark).toBe(darkComponentTokens)
123+
test('каждая тема содержит общие шрифтовые токены', () => {
124+
expect(lightTheme.fonts.fontSize).toBe(fontTokens.fontSize)
125+
expect(darkTheme.fonts.fontSize).toBe(fontTokens.fontSize)
104126
})
105127

106-
test('внутренняя тема Unistyles содержит сгенерированные токены', () => {
107-
const internalLightTheme = UnistylesRuntime.getTheme('light')
108-
const internalDarkTheme = UnistylesRuntime.getTheme('dark')
109-
110-
expect(internalLightTheme).toMatchObject({
111-
components: lightComponentTokens,
112-
fonts: fontTokens,
113-
semantic: {
114-
colorScheme: lightSemanticColorSchemeTokens,
115-
dimension: semanticDimensions,
116-
effects: semanticEffects,
117-
},
118-
})
128+
test('публичные темы сохраняют legacy-алиасы шрифтов', () => {
129+
expect(lightTheme.fonts).toStrictEqual(
130+
expect.objectContaining({
131+
primary: fontTokens.fontFamily.heading,
132+
secondary: fontTokens.fontFamily.base,
133+
})
134+
)
135+
})
119136

120-
expect(internalDarkTheme).toMatchObject({
121-
components: darkComponentTokens,
122-
fonts: fontTokens,
123-
semantic: {
124-
colorScheme: darkSemanticColorSchemeTokens,
125-
dimension: semanticDimensions,
126-
effects: semanticEffects,
127-
},
128-
})
129-
expect(internalLightTheme.fonts).toStrictEqual(internalDarkTheme.fonts)
137+
test('публичные темы сохраняют legacy-токены', () => {
138+
const legacyKeys = [
139+
'background',
140+
'border',
141+
'colors',
142+
'custom',
143+
'effects',
144+
'global',
145+
'shadow',
146+
'sizing',
147+
'spacing',
148+
'theme',
149+
'typography',
150+
]
151+
152+
expect(Object.keys(lightTheme)).toStrictEqual(
153+
expect.arrayContaining(legacyKeys)
154+
)
155+
expect(Object.keys(darkTheme)).toStrictEqual(
156+
expect.arrayContaining(legacyKeys)
157+
)
130158
})
131159
})

src/theme/__tests__/tokenAdapters.test.ts renamed to src/theme/adapters/__tests__/easing.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { toEasing } from '../tokenAdapters'
1+
import { toEasing } from '../easing'
22

33
describe('toEasing', () => {
44
test('создаёт Reanimated easing из коэффициентов токена', () => {

src/theme/adapters/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { toEasing, type EasingToken } from './easing'

src/theme/commonTheme.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/theme/darkTheme.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1 @@
1-
import { InputSize } from './assets/InputSize'
2-
import { ModalSize } from './assets/ModalSize'
3-
import { customDark } from './assets/customDark'
4-
import darkThemeAssets from './assets/themeDark.json'
5-
import { commonTheme } from './commonTheme'
6-
import darkSemanticColorSchemeTokens from './tokens/semantic/colorScheme/dark.json'
7-
import type { ThemeType } from './types'
8-
9-
export const darkTheme: ThemeType = {
10-
semantic: { colorScheme: darkSemanticColorSchemeTokens },
11-
theme: { ...darkThemeAssets, InputSize, ModalSize, custom: customDark },
12-
...commonTheme,
13-
fonts: { primary: 'TT Fellows', secondary: 'Noto Sans' },
14-
}
1+
export { darkTheme } from './themes'

0 commit comments

Comments
 (0)