Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {

export const Root = () => (
<ThemeContextProvider
fonts={{ primary: 'MyFont', secondary: 'MySecondaryFont' }}
fonts={{ heading: 'MyFont', base: 'MySecondaryFont' }}
initialTheme={ThemeVariant.Light}
>
<App />
Expand All @@ -45,7 +45,7 @@ const fonts = useFonts()
import { StyleSheet } from 'react-native-unistyles'

const styles = StyleSheet.create(({ fonts }) => ({
title: { fontFamily: fonts.primary },
title: { fontFamily: fonts.fontFamily.heading },
}))
```

Expand Down
21 changes: 10 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,26 +36,25 @@ npm i @cdek-it/react-native-ui-kit

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

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

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

```tsx
<ThemeContextProvider
fonts={{ primary: 'MyFont', secondary: 'MySecondaryFont' }}
/>
<ThemeContextProvider fonts={{ heading: 'MyFont', base: 'MySecondaryFont' }} />
```

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

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

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

Expand Down
7 changes: 3 additions & 4 deletions src/hooks/useFonts.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useUnistyles } from 'react-native-unistyles'

import type { FontsConfig } from '../theme'
import type { ThemeType } from '../theme/types'

export const useFonts = (): FontsConfig => {
export const useFonts = (): ThemeType['fonts'] => {
const { theme } = useUnistyles()
const { primary, secondary } = theme.fonts

return { primary, secondary }
return theme.fonts
}
66 changes: 12 additions & 54 deletions src/theme/ThemeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,19 @@ import { StyleSheet, UnistylesRuntime } from 'react-native-unistyles'

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

import { darkTheme } from './darkTheme'
import { lightTheme } from './lightTheme'
import {
componentTokens,
fontTokens,
semanticTokens,
type ComponentTokens,
type SemanticTokens,
} from './tokens'
import {
type FontsConfig,
type FontTokens,
type ThemeType,
ThemeVariant,
} from './types'

type InternalTheme = Omit<ThemeType, 'fonts' | 'semantic'> & {
components: ComponentTokens
fonts: FontsConfig & FontTokens
semantic: ThemeType['semantic'] & SemanticTokens
}

const createInternalTheme = (
theme: ThemeType,
components: ComponentTokens
): InternalTheme => ({
...theme,
components,
fonts: {
...fontTokens,
...theme.fonts,
fontFamily: { base: theme.fonts.secondary, heading: theme.fonts.primary },
},
semantic: { ...semanticTokens, ...theme.semantic },
})

const internalThemes = {
light: createInternalTheme(lightTheme, componentTokens.light),
dark: createInternalTheme(darkTheme, componentTokens.dark),
}
import { applyFontConfig, type FontsConfig } from './legacyTokens'
import { darkTheme, lightTheme } from './themes'
import { ThemeVariant } from './types'

StyleSheet.configure({
settings: { initialTheme: 'light' },
themes: internalThemes,
themes: { light: lightTheme, dark: darkTheme },
})

declare module 'react-native-unistyles' {
export interface UnistylesThemes {
light: (typeof internalThemes)['light']
dark: (typeof internalThemes)['dark']
light: typeof lightTheme
dark: typeof darkTheme
}
}

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

if (fonts) {
const updateFonts = (theme: InternalTheme): InternalTheme => ({
...theme,
fonts: {
...theme.fonts,
...fonts,
fontFamily: { base: fonts.secondary, heading: fonts.primary },
},
})

UnistylesRuntime.updateTheme('light', updateFonts)
UnistylesRuntime.updateTheme('dark', updateFonts)
UnistylesRuntime.updateTheme('light', (theme) =>
applyFontConfig(theme, fonts)
)
UnistylesRuntime.updateTheme('dark', (theme) =>
applyFontConfig(theme, fonts)
)
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- initial* props применяем только на mount
}, [])
Expand Down
114 changes: 71 additions & 43 deletions src/theme/__tests__/ThemeContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,10 @@ import { Text } from 'react-native'
import { UnistylesRuntime } from 'react-native-unistyles'

import { ThemeContextProvider } from '../ThemeContext'
import { darkTheme } from '../darkTheme'
import { lightTheme } from '../lightTheme'
import { darkTheme, lightTheme } from '../themes'
import { componentTokens, fontTokens, semanticTokens } from '../tokens'
import darkComponentTokens from '../tokens/components/dark.json'
import lightComponentTokens from '../tokens/components/light.json'
import darkSemanticColorSchemeTokens from '../tokens/semantic/colorScheme/dark.json'
import lightSemanticColorSchemeTokens from '../tokens/semantic/colorScheme/light.json'
import semanticDimensions from '../tokens/semantic/dimensions.json'
import semanticEffects from '../tokens/semantic/effects.json'
import { ThemeVariant } from '../types'

describe('ThemeContextProvider', () => {
Expand Down Expand Up @@ -40,7 +35,7 @@ describe('ThemeContextProvider', () => {
expect(UnistylesRuntime.setTheme).toHaveBeenCalledWith('light')
})

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

render(
Expand All @@ -59,14 +54,41 @@ describe('ThemeContextProvider', () => {
)

const [, updater] = jest.mocked(UnistylesRuntime.updateTheme).mock.calls[0]
const theme = UnistylesRuntime.getTheme('light')
const theme = lightTheme

expect(updater(theme)).toStrictEqual({
...theme,
fonts: {
...theme.fonts,
fontFamily: {
...theme.fonts.fontFamily,
heading: fonts.primary,
base: fonts.secondary,
},
...fonts,
fontFamily: { base: fonts.secondary, heading: fonts.primary },
},
})
})

test('при актуальном формате fonts обновляет обе темы', () => {
const fonts = { heading: 'Roboto', base: 'Inter' }

render(
<ThemeContextProvider fonts={fonts}>
<Text>child</Text>
</ThemeContextProvider>
)

const [, updater] = jest.mocked(UnistylesRuntime.updateTheme).mock.calls[0]
const theme = lightTheme

expect(updater(theme)).toStrictEqual({
...theme,
fonts: {
...theme.fonts,
fontFamily: { ...theme.fonts.fontFamily, ...fonts },
primary: fonts.heading,
secondary: fonts.base,
},
})
})
Expand All @@ -81,51 +103,57 @@ describe('ThemeContextProvider', () => {
expect(UnistylesRuntime.updateTheme).not.toHaveBeenCalled()
})

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

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

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

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

test('внутренняя тема Unistyles содержит сгенерированные токены', () => {
const internalLightTheme = UnistylesRuntime.getTheme('light')
const internalDarkTheme = UnistylesRuntime.getTheme('dark')

expect(internalLightTheme).toMatchObject({
components: lightComponentTokens,
fonts: fontTokens,
semantic: {
colorScheme: lightSemanticColorSchemeTokens,
dimension: semanticDimensions,
effects: semanticEffects,
},
})
test('публичные темы сохраняют legacy-алиасы шрифтов', () => {
expect(lightTheme.fonts).toStrictEqual(
expect.objectContaining({
primary: fontTokens.fontFamily.heading,
secondary: fontTokens.fontFamily.base,
})
)
})

expect(internalDarkTheme).toMatchObject({
components: darkComponentTokens,
fonts: fontTokens,
semantic: {
colorScheme: darkSemanticColorSchemeTokens,
dimension: semanticDimensions,
effects: semanticEffects,
},
})
expect(internalLightTheme.fonts).toStrictEqual(internalDarkTheme.fonts)
test('публичные темы сохраняют legacy-токены', () => {
const legacyKeys = [
'background',
'border',
'colors',
'custom',
'effects',
'global',
'shadow',
'sizing',
'spacing',
'theme',
'typography',
]

expect(Object.keys(lightTheme)).toStrictEqual(
expect.arrayContaining(legacyKeys)
)
expect(Object.keys(darkTheme)).toStrictEqual(
expect.arrayContaining(legacyKeys)
)
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toEasing } from '../tokenAdapters'
import { toEasing } from '../easing'

describe('toEasing', () => {
test('создаёт Reanimated easing из коэффициентов токена', () => {
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions src/theme/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { toEasing, type EasingToken } from './easing'
23 changes: 0 additions & 23 deletions src/theme/commonTheme.ts

This file was deleted.

15 changes: 1 addition & 14 deletions src/theme/darkTheme.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1 @@
import { InputSize } from './assets/InputSize'
import { ModalSize } from './assets/ModalSize'
import { customDark } from './assets/customDark'
import darkThemeAssets from './assets/themeDark.json'
import { commonTheme } from './commonTheme'
import darkSemanticColorSchemeTokens from './tokens/semantic/colorScheme/dark.json'
import type { ThemeType } from './types'

export const darkTheme: ThemeType = {
semantic: { colorScheme: darkSemanticColorSchemeTokens },
theme: { ...darkThemeAssets, InputSize, ModalSize, custom: customDark },
...commonTheme,
fonts: { primary: 'TT Fellows', secondary: 'Noto Sans' },
}
export { darkTheme } from './themes'
5 changes: 2 additions & 3 deletions src/theme/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export { darkTheme } from './darkTheme'
export { lightTheme } from './lightTheme'
export { toEasing, type EasingToken } from './tokenAdapters'
export { darkTheme, lightTheme } from './themes'
export { toEasing, type EasingToken } from './adapters'
export { semanticTokens, type SemanticTokens } from './tokens'
export {
ThemeContext,
Expand Down
13 changes: 13 additions & 0 deletions src/theme/legacyTokens/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Legacy-токены

Эти токены сохраняются только для обратной совместимости публичных `lightTheme`,
`darkTheme` и `ThemeType`. Компоненты библиотеки используют `components`,
`semantic` и сгенерированную структуру `fonts`.

Единственная точка подключения legacy-токенов — `index.ts`. Компоненты не должны
импортировать отдельные файлы из этой директории. Удалить этот слой можно в
следующей мажорной версии.

К этому же слою относятся deprecated-алиасы `fonts.primary` и `fonts.secondary`,
а также их преобразование в `fonts.fontFamily.heading` и
`fonts.fontFamily.base`.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading