From d6fdfec6651b4ef98c9a28e08472ee9ccfdfe48d Mon Sep 17 00:00:00 2001 From: Alesya Volosach Date: Tue, 4 Aug 2026 18:24:35 +0300 Subject: [PATCH] =?UTF-8?q?refactor(theme):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=81=D0=BB=D0=BE=D0=B9=20legacy-?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MIGRATION.md | 4 +- README.md | 21 ++- src/hooks/useFonts.ts | 7 +- src/theme/ThemeContext.tsx | 66 ++------- src/theme/__tests__/ThemeContext.test.tsx | 114 +++++++++------ .../__tests__/easing.test.ts} | 2 +- .../{tokenAdapters.ts => adapters/easing.ts} | 0 src/theme/adapters/index.ts | 1 + src/theme/commonTheme.ts | 23 ---- src/theme/darkTheme.ts | 15 +- src/theme/index.ts | 5 +- .../InputSize/base.json | 0 .../InputSize/index.ts | 0 .../InputSize/large.json | 0 .../InputSize/xlarge.json | 0 .../ModalSize/index.ts | 0 .../ModalSize/lg.json | 0 .../ModalSize/md.json | 0 .../ModalSize/sm.json | 0 .../ModalSize/xl.json | 0 src/theme/legacyTokens/README.md | 13 ++ .../{assets => legacyTokens}/background.json | 0 .../{assets => legacyTokens}/border.json | 0 .../{assets => legacyTokens}/customCommon.ts | 0 .../{assets => legacyTokens}/customDark.ts | 0 .../{assets => legacyTokens}/customLight.ts | 0 .../{assets => legacyTokens}/effects.json | 0 .../{assets => legacyTokens}/global.json | 0 src/theme/legacyTokens/index.ts | 130 ++++++++++++++++++ .../primaryColors.json | 0 src/theme/{assets => legacyTokens}/shadow.ts | 0 .../{assets => legacyTokens}/sizing.json | 0 .../{assets => legacyTokens}/spacing.json | 0 .../{assets => legacyTokens}/themeDark.json | 0 .../{assets => legacyTokens}/themeLight.json | 0 .../{assets => legacyTokens}/typography.json | 0 src/theme/lightTheme.ts | 15 +- src/theme/themes.ts | 29 ++++ src/theme/tokens/README.md | 12 +- src/theme/types.ts | 60 +++----- 40 files changed, 302 insertions(+), 215 deletions(-) rename src/theme/{__tests__/tokenAdapters.test.ts => adapters/__tests__/easing.test.ts} (87%) rename src/theme/{tokenAdapters.ts => adapters/easing.ts} (100%) create mode 100644 src/theme/adapters/index.ts delete mode 100644 src/theme/commonTheme.ts rename src/theme/{assets => legacyTokens}/InputSize/base.json (100%) rename src/theme/{assets => legacyTokens}/InputSize/index.ts (100%) rename src/theme/{assets => legacyTokens}/InputSize/large.json (100%) rename src/theme/{assets => legacyTokens}/InputSize/xlarge.json (100%) rename src/theme/{assets => legacyTokens}/ModalSize/index.ts (100%) rename src/theme/{assets => legacyTokens}/ModalSize/lg.json (100%) rename src/theme/{assets => legacyTokens}/ModalSize/md.json (100%) rename src/theme/{assets => legacyTokens}/ModalSize/sm.json (100%) rename src/theme/{assets => legacyTokens}/ModalSize/xl.json (100%) create mode 100644 src/theme/legacyTokens/README.md rename src/theme/{assets => legacyTokens}/background.json (100%) rename src/theme/{assets => legacyTokens}/border.json (100%) rename src/theme/{assets => legacyTokens}/customCommon.ts (100%) rename src/theme/{assets => legacyTokens}/customDark.ts (100%) rename src/theme/{assets => legacyTokens}/customLight.ts (100%) rename src/theme/{assets => legacyTokens}/effects.json (100%) rename src/theme/{assets => legacyTokens}/global.json (100%) create mode 100644 src/theme/legacyTokens/index.ts rename src/theme/{assets => legacyTokens}/primaryColors.json (100%) rename src/theme/{assets => legacyTokens}/shadow.ts (100%) rename src/theme/{assets => legacyTokens}/sizing.json (100%) rename src/theme/{assets => legacyTokens}/spacing.json (100%) rename src/theme/{assets => legacyTokens}/themeDark.json (100%) rename src/theme/{assets => legacyTokens}/themeLight.json (100%) rename src/theme/{assets => legacyTokens}/typography.json (100%) create mode 100644 src/theme/themes.ts diff --git a/MIGRATION.md b/MIGRATION.md index 074c690b..de792443 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -18,7 +18,7 @@ import { export const Root = () => ( @@ -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 }, })) ``` diff --git a/README.md b/README.md index b95d6112..bf4be666 100644 --- a/README.md +++ b/README.md @@ -36,26 +36,25 @@ npm i @cdek-it/react-native-ui-kit UI kit использует следующие виды шрифтов. -| Тип шрифта | Используемые начертания | Рекомендуемый шрифт | Рекомендуемый аналог | -| ----------- | ---------------------------------------------- | ------------------- | -------------------- | -| `primary` | – regular
– demibold
– demibold italic | TT Fellows | Noto Sans | -| `secondary` | – regular
– bold | Noto Sans | — | +| Тип шрифта | Используемые начертания | Рекомендуемый шрифт | Рекомендуемый аналог | +| ---------- | ---------------------------------------------- | ------------------- | -------------------- | +| `heading` | – regular
– demibold
– demibold italic | TT Fellows | Noto Sans | +| `base` | – regular
– bold | Noto Sans | — | Исходники шрифтов не поставляются вместе с пакетом, их требуется подключать -отдельно. После подключения шрифтов в проект, необходимо указать их в -`ThemeContextProvider`. +отдельно под именами, указанными в токенах. ```tsx - + ``` После этого шрифты доступны через `useUnistyles().theme.fonts` или прямо в `StyleSheet.create(({ fonts }) => ...)`. -`ThemeContextProvider` настраивает активную тему и шрифты для `unistyles`. -`ThemeContext` остается пустым и всегда имеет значение `null`. +`ThemeContextProvider` только настраивает темы и шрифты для `unistyles`. +`ThemeContext` остается пустым и всегда имеет значение `null`. Deprecated-формат +`fonts={{ primary, secondary }}` временно сохраняется для обратной +совместимости. ### Пример подключения шрифтов с помощью expo-fonts через плагин diff --git a/src/hooks/useFonts.ts b/src/hooks/useFonts.ts index e83308db..348ee283 100644 --- a/src/hooks/useFonts.ts +++ b/src/hooks/useFonts.ts @@ -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 } diff --git a/src/theme/ThemeContext.tsx b/src/theme/ThemeContext.tsx index f20afb5b..f528907e 100644 --- a/src/theme/ThemeContext.tsx +++ b/src/theme/ThemeContext.tsx @@ -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 & { - 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 } } @@ -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 }, []) diff --git a/src/theme/__tests__/ThemeContext.test.tsx b/src/theme/__tests__/ThemeContext.test.tsx index 2edec632..c65f0d92 100644 --- a/src/theme/__tests__/ThemeContext.test.tsx +++ b/src/theme/__tests__/ThemeContext.test.tsx @@ -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', () => { @@ -40,7 +35,7 @@ describe('ThemeContextProvider', () => { expect(UnistylesRuntime.setTheme).toHaveBeenCalledWith('light') }) - test('при передаче fonts обновляет обе темы', () => { + test('при legacy-формате fonts обновляет обе темы', () => { const fonts = { primary: 'Roboto', secondary: 'Inter' } render( @@ -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( + + child + + ) + + 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, }, }) }) @@ -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) + ) }) }) diff --git a/src/theme/__tests__/tokenAdapters.test.ts b/src/theme/adapters/__tests__/easing.test.ts similarity index 87% rename from src/theme/__tests__/tokenAdapters.test.ts rename to src/theme/adapters/__tests__/easing.test.ts index f11c67b4..c6218172 100644 --- a/src/theme/__tests__/tokenAdapters.test.ts +++ b/src/theme/adapters/__tests__/easing.test.ts @@ -1,4 +1,4 @@ -import { toEasing } from '../tokenAdapters' +import { toEasing } from '../easing' describe('toEasing', () => { test('создаёт Reanimated easing из коэффициентов токена', () => { diff --git a/src/theme/tokenAdapters.ts b/src/theme/adapters/easing.ts similarity index 100% rename from src/theme/tokenAdapters.ts rename to src/theme/adapters/easing.ts diff --git a/src/theme/adapters/index.ts b/src/theme/adapters/index.ts new file mode 100644 index 00000000..3c4cc4cc --- /dev/null +++ b/src/theme/adapters/index.ts @@ -0,0 +1 @@ +export { toEasing, type EasingToken } from './easing' diff --git a/src/theme/commonTheme.ts b/src/theme/commonTheme.ts deleted file mode 100644 index 8add2156..00000000 --- a/src/theme/commonTheme.ts +++ /dev/null @@ -1,23 +0,0 @@ -import background from './assets/background.json' -import border from './assets/border.json' -import { customCommon } from './assets/customCommon' -import effects from './assets/effects.json' -import global from './assets/global.json' -import primaryColors from './assets/primaryColors.json' -import { shadow } from './assets/shadow' -import sizing from './assets/sizing.json' -import spacing from './assets/spacing.json' -import typography from './assets/typography.json' - -export const commonTheme = { - background, - colors: { primary: primaryColors }, - border, - effects, - global, - sizing, - spacing, - typography, - custom: customCommon, - shadow, -} diff --git a/src/theme/darkTheme.ts b/src/theme/darkTheme.ts index bbc330fe..c3419e58 100644 --- a/src/theme/darkTheme.ts +++ b/src/theme/darkTheme.ts @@ -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' diff --git a/src/theme/index.ts b/src/theme/index.ts index 4266d324..76e8c11c 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -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, diff --git a/src/theme/assets/InputSize/base.json b/src/theme/legacyTokens/InputSize/base.json similarity index 100% rename from src/theme/assets/InputSize/base.json rename to src/theme/legacyTokens/InputSize/base.json diff --git a/src/theme/assets/InputSize/index.ts b/src/theme/legacyTokens/InputSize/index.ts similarity index 100% rename from src/theme/assets/InputSize/index.ts rename to src/theme/legacyTokens/InputSize/index.ts diff --git a/src/theme/assets/InputSize/large.json b/src/theme/legacyTokens/InputSize/large.json similarity index 100% rename from src/theme/assets/InputSize/large.json rename to src/theme/legacyTokens/InputSize/large.json diff --git a/src/theme/assets/InputSize/xlarge.json b/src/theme/legacyTokens/InputSize/xlarge.json similarity index 100% rename from src/theme/assets/InputSize/xlarge.json rename to src/theme/legacyTokens/InputSize/xlarge.json diff --git a/src/theme/assets/ModalSize/index.ts b/src/theme/legacyTokens/ModalSize/index.ts similarity index 100% rename from src/theme/assets/ModalSize/index.ts rename to src/theme/legacyTokens/ModalSize/index.ts diff --git a/src/theme/assets/ModalSize/lg.json b/src/theme/legacyTokens/ModalSize/lg.json similarity index 100% rename from src/theme/assets/ModalSize/lg.json rename to src/theme/legacyTokens/ModalSize/lg.json diff --git a/src/theme/assets/ModalSize/md.json b/src/theme/legacyTokens/ModalSize/md.json similarity index 100% rename from src/theme/assets/ModalSize/md.json rename to src/theme/legacyTokens/ModalSize/md.json diff --git a/src/theme/assets/ModalSize/sm.json b/src/theme/legacyTokens/ModalSize/sm.json similarity index 100% rename from src/theme/assets/ModalSize/sm.json rename to src/theme/legacyTokens/ModalSize/sm.json diff --git a/src/theme/assets/ModalSize/xl.json b/src/theme/legacyTokens/ModalSize/xl.json similarity index 100% rename from src/theme/assets/ModalSize/xl.json rename to src/theme/legacyTokens/ModalSize/xl.json diff --git a/src/theme/legacyTokens/README.md b/src/theme/legacyTokens/README.md new file mode 100644 index 00000000..f163ad42 --- /dev/null +++ b/src/theme/legacyTokens/README.md @@ -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`. diff --git a/src/theme/assets/background.json b/src/theme/legacyTokens/background.json similarity index 100% rename from src/theme/assets/background.json rename to src/theme/legacyTokens/background.json diff --git a/src/theme/assets/border.json b/src/theme/legacyTokens/border.json similarity index 100% rename from src/theme/assets/border.json rename to src/theme/legacyTokens/border.json diff --git a/src/theme/assets/customCommon.ts b/src/theme/legacyTokens/customCommon.ts similarity index 100% rename from src/theme/assets/customCommon.ts rename to src/theme/legacyTokens/customCommon.ts diff --git a/src/theme/assets/customDark.ts b/src/theme/legacyTokens/customDark.ts similarity index 100% rename from src/theme/assets/customDark.ts rename to src/theme/legacyTokens/customDark.ts diff --git a/src/theme/assets/customLight.ts b/src/theme/legacyTokens/customLight.ts similarity index 100% rename from src/theme/assets/customLight.ts rename to src/theme/legacyTokens/customLight.ts diff --git a/src/theme/assets/effects.json b/src/theme/legacyTokens/effects.json similarity index 100% rename from src/theme/assets/effects.json rename to src/theme/legacyTokens/effects.json diff --git a/src/theme/assets/global.json b/src/theme/legacyTokens/global.json similarity index 100% rename from src/theme/assets/global.json rename to src/theme/legacyTokens/global.json diff --git a/src/theme/legacyTokens/index.ts b/src/theme/legacyTokens/index.ts new file mode 100644 index 00000000..991912d0 --- /dev/null +++ b/src/theme/legacyTokens/index.ts @@ -0,0 +1,130 @@ +import { InputSize } from './InputSize' +import { ModalSize } from './ModalSize' +import background from './background.json' +import border from './border.json' +import { customCommon } from './customCommon' +import { customDark } from './customDark' +import { customLight } from './customLight' +import effects from './effects.json' +import global from './global.json' +import primaryColors from './primaryColors.json' +import { shadow } from './shadow' +import sizing from './sizing.json' +import spacing from './spacing.json' +import darkTheme from './themeDark.json' +import lightTheme from './themeLight.json' +import typography from './typography.json' + +interface FontFamilyTokens { + base: string + heading: string +} + +export interface LegacyFontAliases { + /** @deprecated Используйте `fonts.fontFamily.heading`. */ + primary: string + /** @deprecated Используйте `fonts.fontFamily.base`. */ + secondary: string +} + +export type FontsConfig = FontFamilyTokens | LegacyFontAliases + +export interface FontsConfigType { + fonts: FontsConfig +} + +export interface LegacyTheme { + /** @deprecated Используйте `semantic` или `components`. */ + background: typeof background + /** @deprecated Используйте `semantic` или `components`. */ + colors: { primary: typeof primaryColors } + /** @deprecated Используйте `semantic` или `components`. */ + border: typeof border + /** @deprecated Используйте `semantic.effects`. */ + effects: typeof effects + /** @deprecated Используйте `semantic`, `components` или `fonts`. */ + global: typeof global + /** @deprecated Используйте `semantic` или `components`. */ + sizing: typeof sizing + /** @deprecated Используйте `semantic` или `components`. */ + spacing: typeof spacing + /** @deprecated Используйте `semantic` или `components`. */ + theme: typeof lightTheme & { + custom: typeof customLight + InputSize: typeof InputSize + ModalSize: typeof ModalSize + } + /** @deprecated Используйте `fonts`. */ + typography: typeof typography + /** @deprecated Используйте `semantic` или `components`. */ + custom: typeof customCommon + /** @deprecated Используйте `semantic.effects`. */ + shadow: typeof shadow +} + +const commonLegacyTokens: Omit = { + background, + colors: { primary: primaryColors }, + border, + effects, + global, + sizing, + spacing, + typography, + custom: customCommon, + shadow, +} + +const legacyThemes = { + light: { + theme: { ...lightTheme, InputSize, ModalSize, custom: customLight }, + ...commonLegacyTokens, + }, + dark: { + theme: { ...darkTheme, InputSize, ModalSize, custom: customDark }, + ...commonLegacyTokens, + }, +} satisfies Record<'light' | 'dark', LegacyTheme> + +const toFontFamilyTokens = (fonts: FontsConfig): FontFamilyTokens => { + if (!('primary' in fonts)) { + return fonts + } + + const aliases = fonts as Record<'primary' | 'secondary', string> + + return { base: aliases.secondary, heading: aliases.primary } +} + +const toLegacyFontAliases = ({ + base, + heading, +}: FontFamilyTokens): LegacyFontAliases => ({ + primary: heading, + secondary: base, +}) + +export const applyFontConfig = < + T extends { fonts: { fontFamily: FontFamilyTokens } }, +>( + theme: T, + fonts: FontsConfig +) => { + const fontFamily = { ...theme.fonts.fontFamily, ...toFontFamilyTokens(fonts) } + + return { + ...theme, + fonts: { ...theme.fonts, fontFamily, ...toLegacyFontAliases(fontFamily) }, + } +} + +export const withLegacyTokens = < + T extends { fonts: { fontFamily: FontFamilyTokens } }, +>( + theme: T, + colorScheme: keyof typeof legacyThemes +) => ({ + ...theme, + ...legacyThemes[colorScheme], + fonts: { ...theme.fonts, ...toLegacyFontAliases(theme.fonts.fontFamily) }, +}) diff --git a/src/theme/assets/primaryColors.json b/src/theme/legacyTokens/primaryColors.json similarity index 100% rename from src/theme/assets/primaryColors.json rename to src/theme/legacyTokens/primaryColors.json diff --git a/src/theme/assets/shadow.ts b/src/theme/legacyTokens/shadow.ts similarity index 100% rename from src/theme/assets/shadow.ts rename to src/theme/legacyTokens/shadow.ts diff --git a/src/theme/assets/sizing.json b/src/theme/legacyTokens/sizing.json similarity index 100% rename from src/theme/assets/sizing.json rename to src/theme/legacyTokens/sizing.json diff --git a/src/theme/assets/spacing.json b/src/theme/legacyTokens/spacing.json similarity index 100% rename from src/theme/assets/spacing.json rename to src/theme/legacyTokens/spacing.json diff --git a/src/theme/assets/themeDark.json b/src/theme/legacyTokens/themeDark.json similarity index 100% rename from src/theme/assets/themeDark.json rename to src/theme/legacyTokens/themeDark.json diff --git a/src/theme/assets/themeLight.json b/src/theme/legacyTokens/themeLight.json similarity index 100% rename from src/theme/assets/themeLight.json rename to src/theme/legacyTokens/themeLight.json diff --git a/src/theme/assets/typography.json b/src/theme/legacyTokens/typography.json similarity index 100% rename from src/theme/assets/typography.json rename to src/theme/legacyTokens/typography.json diff --git a/src/theme/lightTheme.ts b/src/theme/lightTheme.ts index e3c80bfb..a39341e9 100644 --- a/src/theme/lightTheme.ts +++ b/src/theme/lightTheme.ts @@ -1,14 +1 @@ -import { InputSize } from './assets/InputSize' -import { ModalSize } from './assets/ModalSize' -import { customLight } from './assets/customLight' -import lightThemeAssets from './assets/themeLight.json' -import { commonTheme } from './commonTheme' -import lightSemanticColorSchemeTokens from './tokens/semantic/colorScheme/light.json' -import type { ThemeType } from './types' - -export const lightTheme: ThemeType = { - semantic: { colorScheme: lightSemanticColorSchemeTokens }, - theme: { ...lightThemeAssets, InputSize, ModalSize, custom: customLight }, - ...commonTheme, - fonts: { primary: 'TT Fellows', secondary: 'Noto Sans' }, -} +export { lightTheme } from './themes' diff --git a/src/theme/themes.ts b/src/theme/themes.ts new file mode 100644 index 00000000..9f55ed97 --- /dev/null +++ b/src/theme/themes.ts @@ -0,0 +1,29 @@ +import { withLegacyTokens } from './legacyTokens' +import { componentTokens, fontTokens, semanticTokens } from './tokens' +import darkSemanticColorSchemeTokens from './tokens/semantic/colorScheme/dark.json' +import lightSemanticColorSchemeTokens from './tokens/semantic/colorScheme/light.json' +import type { RuntimeTheme, ThemeType } from './types' + +const createRuntimeTheme = ( + colorScheme: RuntimeTheme['semantic']['colorScheme'], + components: RuntimeTheme['components'] +): RuntimeTheme => ({ + semantic: { colorScheme, ...semanticTokens }, + components, + fonts: fontTokens, +}) + +const runtimeThemes = { + light: createRuntimeTheme( + lightSemanticColorSchemeTokens, + componentTokens.light + ), + dark: createRuntimeTheme(darkSemanticColorSchemeTokens, componentTokens.dark), +} + +export const lightTheme: ThemeType = withLegacyTokens( + runtimeThemes.light, + 'light' +) + +export const darkTheme: ThemeType = withLegacyTokens(runtimeThemes.dark, 'dark') diff --git a/src/theme/tokens/README.md b/src/theme/tokens/README.md index c4c6c42c..f7dd2729 100644 --- a/src/theme/tokens/README.md +++ b/src/theme/tokens/README.md @@ -1,11 +1,11 @@ # Сгенерированные токены -В публичный `ThemeType.semantic` подключается только выбранный `colorScheme`. -Общие `dimension`, `effects` и `fonts` добавляются во внутреннюю тему Unistyles и -не зависят от цветовой схемы. В неё же добавляются соответствующие светлой или -тёмной теме `components`. В публичные темы и экспорты component- и font-токены -не входят. Всё генерируется из `design-tokens/input/tokens.json` и не -редактируется вручную. +В `ThemeType`, `lightTheme` и `darkTheme` входят выбранный +`semantic.colorScheme`, общие `semantic.dimension`, `semantic.effects`, `fonts` +и соответствующие теме `components`. Отдельно из пакета экспортируются только +общие `semanticTokens`; карты component- и font-токенов остаются внутренними. +Всё генерируется из `design-tokens/input/tokens.json` и не редактируется +вручную. Подробности: [генератор токенов](../../../scripts/token-generator/README.md). diff --git a/src/theme/types.ts b/src/theme/types.ts index c0b3ad20..0402a101 100644 --- a/src/theme/types.ts +++ b/src/theme/types.ts @@ -1,59 +1,39 @@ import type { TextStyle } from 'react-native' -import type { InputSize } from './assets/InputSize' -import type { ModalSize } from './assets/ModalSize' -import type background from './assets/background.json' -import type border from './assets/border.json' -import type { customCommon } from './assets/customCommon' -import type { customLight } from './assets/customLight' -import type effects from './assets/effects.json' -import type global from './assets/global.json' -import type primaryColors from './assets/primaryColors.json' -import type { shadow } from './assets/shadow' -import type sizing from './assets/sizing.json' -import type spacing from './assets/spacing.json' -import type lightTheme from './assets/themeLight.json' -import type typography from './assets/typography.json' +import type { LegacyFontAliases, LegacyTheme } from './legacyTokens' +import type lightComponentTokens from './tokens/components/light.json' import type fontTokens from './tokens/fonts.json' import type lightSemanticColorSchemeTokens from './tokens/semantic/colorScheme/light.json' +import type semanticDimensions from './tokens/semantic/dimensions.json' +import type semanticEffects from './tokens/semantic/effects.json' -export interface ThemeType { - background: typeof background - colors: { primary: typeof primaryColors } - border: typeof border - effects: typeof effects - global: typeof global - sizing: typeof sizing - spacing: typeof spacing - semantic: { colorScheme: typeof lightSemanticColorSchemeTokens } - theme: typeof lightTheme & { - custom: typeof customLight - InputSize: typeof InputSize - ModalSize: typeof ModalSize +export interface RuntimeTheme { + semantic: { + colorScheme: typeof lightSemanticColorSchemeTokens + dimension: typeof semanticDimensions + effects: typeof semanticEffects } - typography: typeof typography - custom: typeof customCommon - shadow: typeof shadow - fonts: FontsConfig + components: typeof lightComponentTokens + fonts: FontTokens } +export type ThemeType = RuntimeTheme & + LegacyTheme & { fonts: FontTokens & LegacyFontAliases } + export enum ThemeVariant { Light = 'Light', Dark = 'Dark', } -export interface FontsConfig { - primary: string - secondary: string -} - -export interface FontsConfigType { - fonts: FontsConfig -} - export type FontTokens = Omit & { fontWeight: Record< keyof typeof fontTokens.fontWeight, TextStyle['fontWeight'] > } + +export type { + FontsConfig, + FontsConfigType, + LegacyFontAliases, +} from './legacyTokens'