diff --git a/.prettierignore b/.prettierignore
index 72079094..b109670a 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,4 +1,8 @@
ios/Pods
+ios
+.github/workflows
.yarn
android/app/.cxx
-CHANGELOG.md
\ No newline at end of file
+android/app/build
+android/build
+CHANGELOG.md
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index d4317acb..146026b8 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,10 +1,5 @@
import type { Preview } from '@storybook/react'
-import {
- makeStyles,
- ThemeContextProvider,
- ThemeVariant,
- useChangeTheme,
-} from '../src'
+import { makeStyles, ThemeVariant, useChangeTheme } from '../src'
import { View } from 'react-native'
import React, { type FunctionComponent, type ReactNode, useEffect } from 'react'
@@ -12,13 +7,11 @@ const preview: Preview = {
decorators: [
(Story, { args }) => {
return (
-
-
-
-
-
-
-
+
+
+
+
+
)
},
],
diff --git a/.storybook/storybook.requires.ts b/.storybook/storybook.requires.ts
index 21bd048f..00985b4f 100644
--- a/.storybook/storybook.requires.ts
+++ b/.storybook/storybook.requires.ts
@@ -1,47 +1,52 @@
/* do not change this file, it is auto generated by storybook. */
+import { start, updateView, View } from '@storybook/react-native';
-import { start, updateView } from '@storybook/react-native'
-
-import '@storybook/addon-ondevice-notes/register'
-import '@storybook/addon-ondevice-controls/register'
-import '@storybook/addon-ondevice-actions/register'
+import "@storybook/addon-ondevice-notes/register";
+import "@storybook/addon-ondevice-controls/register";
+import "@storybook/addon-ondevice-actions/register";
const normalizedStories = [
{
- titlePrefix: '',
- directory: './src',
- files: '**/*.stories.?(ts|tsx|js|jsx)',
- importPathMatcher:
- /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/,
+ titlePrefix: "",
+ directory: "./src",
+ files: "**/*.stories.?(ts|tsx|js|jsx)",
+ importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/,
// @ts-ignore
req: require.context(
'../src',
true,
/^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/
),
- },
-]
+ }
+];
+
declare global {
- var view: ReturnType
- var STORIES: typeof normalizedStories
+ var view: View;
+ var STORIES: typeof normalizedStories;
}
+
const annotations = [
require('./preview'),
- require('@storybook/react-native/dist/preview'),
- require('@storybook/addon-actions/preview'),
-]
+ require("@storybook/react-native/preview")
+];
-global.STORIES = normalizedStories
+global.STORIES = normalizedStories;
// @ts-ignore
-module?.hot?.accept?.()
+module?.hot?.accept?.();
+
+
if (!global.view) {
- global.view = start({ annotations, storyEntries: normalizedStories })
+ global.view = start({
+ annotations,
+ storyEntries: normalizedStories,
+
+ });
} else {
- updateView(global.view, annotations, normalizedStories)
+ updateView(global.view, annotations, normalizedStories);
}
-export const view: ReturnType = global.view
+export const view: View = global.view;
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 00000000..be96cdbb
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,150 @@
+# Миграция на Unistyles V3
+
+Стили переведены на `react-native-unistyles`. Удален собственный `React Context`
+и провайдер.
+
+## Изменения
+
+### `ThemeContextProvider` — удалён
+
+Темы регистрируются автоматически. Обёртка больше не нужна.
+
+### `useFonts` — deprecated
+
+Используйте `useUnistyles`:
+
+```tsx
+import { useUnistyles } from '@cdek-it/react-native-ui-kit'
+
+const { theme } = useUnistyles()
+theme.fonts
+```
+
+Или прямо в стилях через `StyleSheet.create(...)`.
+
+### `makeStyles` — deprecated
+
+Используйте `StyleSheet.create(...)`:
+
+```tsx
+import { StyleSheet } from '@cdek-it/react-native-ui-kit'
+
+const styles = StyleSheet.create((theme) => ({
+ container: { backgroundColor: theme.Button.Brand.buttonBg },
+}))
+```
+
+`makeStyles` использует `useUnistyles()`, что вызывает React-ререндеры при смене
+темы. `StyleSheet.create(...)` — нативный путь, обновляет стили **без**
+ререндеров.
+
+SDK реэкспортирует `StyleSheet`, `useUnistyles`, `UnistylesRuntime` и
+`withUnistyles`, поэтому потребителям не нужно импортировать
+`react-native-unistyles` напрямую.
+
+### `useTheme()` — deprecated
+
+```tsx
+import { UnistylesRuntime, useUnistyles } from '@cdek-it/react-native-ui-kit'
+
+const themeName = UnistylesRuntime.themeName // 'light' | 'dark'
+```
+
+Для реактивного поведения используйте `useUnistyles()`:
+
+```tsx
+const { rt } = useUnistyles()
+rt.themeName
+```
+
+### `useChangeTheme()` — deprecated
+
+```tsx
+import { UnistylesRuntime } from '@cdek-it/react-native-ui-kit'
+
+UnistylesRuntime.setTheme('dark')
+```
+
+## ESLint Правила для Unistyles
+
+Три обязательных ESLint правила защищают от потери скрытого `unistyles` payload:
+
+### ⛔ `unistyles/no-spread-unistyles` (error)
+
+**Проблема**: Spread оператор теряет скрытый payload unistyles, что приводит к
+потере темы и реактивности при её смене.
+
+```typescript
+// ❌ Неправильно — payload теряется
+const myStyle = { ...styles.button }
+const btn = { ...styles.button, marginTop: 10 }
+Object.assign({}, styles.button)
+const { button, text } = styles
+
+// ✅ Правильно — payload сохранится
+const myStyle = styles.button
+style={[styles.button, { marginTop: 10 }]}
+style={[styles.button, isActive && styles.buttonActive]}
+```
+
+### ⛔ `unistyles/no-unistyles-in-worklet` (error)
+
+**Проблема**: Worklet функции (`useAnimatedStyle`, `runOnJS`, `withSpring`)
+передаются в native код и не могут захватить весь unistyles объект. Нужно
+вытащить примитивы.
+
+```typescript
+// ❌ Неправильно — styles целиком в worklet
+const animStyle = useAnimatedStyle(() => ({ color: styles.text.color }))
+
+// ✅ Правильно — примитив вытащен перед worklet
+const color = styles.text.color
+const animStyle = useAnimatedStyle(() => ({
+ color, // Теперь это просто строка
+}))
+```
+
+### ⚠️ `unistyles/no-spread-icon-styles` (warn)
+
+Рекомендуется передавать явные props для Icon компонентов вместо spread.
+
+```typescript
+// ❌ Не рекомендуется
+
+
+// ✅ Рекомендуется
+const color = styles.icon.color
+const width = styles.icon.width
+
+```
+
+### Почему это важно
+
+`react-native-unistyles` добавляет скрытый payload в каждый объект из
+`StyleSheet.create()`. Этот payload содержит информацию о:
+
+- **Активной теме** (light/dark)
+- **Responsive breakpoint** (размер экрана)
+- **Unistyles runtime configuration**
+
+Если потерять payload, нативная часть больше не сможет:
+
+- Применить правильную тему
+- Обновить стиль при смене темы/breakpoint
+- Корректно интерпретировать значения
+
+Подробнее:
+[ESLint Rules for Unistyles](./configs/eslint/rules/unistyles/README.md)
+
+## Babel конфигурация
+
+Для получения нативного обновления стилей без React-ререндеров:
+
+1. Используйте `StyleSheet.create(...)`.
+2. Добавьте `autoProcessPaths` в Babel-конфиг вашего приложения.
+
+Документация:
+
+- [useUnistyles](https://www.unistyl.es/v3/references/use-unistyles/)
+- [StyleSheet](https://www.unistyl.es/v3/references/stylesheet/)
+- [Babel plugin](https://www.unistyl.es/v3/other/babel-plugin/)
diff --git a/babel.config.js b/babel.config.js
index 46fba684..ae8e3b2f 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -3,6 +3,20 @@ module.exports = (api) => {
return {
presets: ['babel-preset-expo'],
- plugins: ['@babel/plugin-transform-class-static-block'],
+ plugins: [
+ [
+ 'react-native-unistyles/plugin',
+ {
+ root: 'src',
+ autoProcessImports: [
+ '../utils',
+ '../../utils',
+ '../../../utils',
+ '../../../../utils',
+ ],
+ },
+ ],
+ '@babel/plugin-transform-class-static-block',
+ ],
}
}
diff --git a/configs/eslint/index.ts b/configs/eslint/index.ts
index 52274ad4..b909dcf8 100644
--- a/configs/eslint/index.ts
+++ b/configs/eslint/index.ts
@@ -9,6 +9,7 @@ import {
reactConfig,
reactNativeConfig,
prettierConfig,
+ unistylesConfig,
} from './rules'
export const MobileConfig = defineConfig([
@@ -20,6 +21,7 @@ export const MobileConfig = defineConfig([
...importConfig,
...reactConfig,
...reactNativeConfig,
+ ...unistylesConfig,
globalIgnores([
'dist/',
'.yarn/',
diff --git a/configs/eslint/rules/index.ts b/configs/eslint/rules/index.ts
index ab304c6b..75a60b25 100644
--- a/configs/eslint/rules/index.ts
+++ b/configs/eslint/rules/index.ts
@@ -6,3 +6,4 @@ export { reactConfig } from './react'
export { reactNativeConfig } from './reactNative'
export { stylisticConfig } from './stylistic'
export { prettierConfig } from './prettier'
+export { unistylesConfig } from './unistyles'
diff --git a/configs/eslint/rules/unistyles.ts b/configs/eslint/rules/unistyles.ts
new file mode 100644
index 00000000..93af5e9e
--- /dev/null
+++ b/configs/eslint/rules/unistyles.ts
@@ -0,0 +1,8 @@
+/**
+ * ESLint правила для unistyles
+ *
+ * Этот файл переиспортирует конфиг из папки unistyles/.
+ * Полная документация и реализация в configs/eslint/rules/unistyles/
+ */
+
+export { unistylesConfig } from './unistyles/index'
diff --git a/configs/eslint/rules/unistyles/README.md b/configs/eslint/rules/unistyles/README.md
new file mode 100644
index 00000000..42474787
--- /dev/null
+++ b/configs/eslint/rules/unistyles/README.md
@@ -0,0 +1,262 @@
+# ESLint Rules for Unistyles
+
+Custom ESLint rules для защиты от потери скрытого `unistyles_*` payload при
+работе со стилями из `StyleSheet.create()`.
+
+## Проблема
+
+`react-native-unistyles` добавляет скрытый payload в каждый объект из
+`StyleSheet.create()`. Этот payload содержит информацию о:
+
+- **Теме** (light/dark и т.д.)
+- **Responsive breakpoint** (текущий размер экрана)
+- **Unistyles runtime configuration**
+
+Когда ты распаковываешь объект через spread или деструктуризируешь его, этот
+payload **теряется**. Нативная часть больше не может:
+
+- Применить правильную тему
+- Обновить стиль при смене темы/breakpoint
+- Корректно интерпретировать значения
+
+## Правила
+
+### 1. `unistyles/no-spread-unistyles` (error)
+
+**Запрещает** распаковывать объекты из `StyleSheet.create()` через spread
+оператор.
+
+#### ❌ Неправильно
+
+```typescript
+const styles = StyleSheet.create({ button: { padding: 12 } })
+
+// Spread теряет скрытый payload
+const myStyle = { ...styles.button }
+const btn = { ...styles.button, marginTop: 10 }
+
+// Object.assign тоже теряет payload
+Object.assign({}, styles.button)
+```
+
+#### ✅ Правильно
+
+```typescript
+const styles = StyleSheet.create({ button: { padding: 12 } })
+
+// Передавай массив стилей (самый безопасный способ)
+style={[styles.button, extraStyle]}
+
+// Или используй напрямую
+style={styles.button}
+
+// Для динамических стилей — массив
+style={[
+ styles.button,
+ isActive && styles.buttonActive,
+]}
+```
+
+---
+
+### 2. `unistyles/no-unistyles-in-worklet` (error)
+
+**Запрещает** захватывать переменную `styles` в worklet closures
+(`useAnimatedStyle`, `runOnJS` и т.д.).
+
+Причина: worklet функции передаются в native код, и весь unistyles объект
+потеряет скрытый payload при этой передаче.
+
+#### ❌ Неправильно
+
+```typescript
+const styles = useStyles()
+
+// ❌ styles захвачена целиком в worklet
+const animStyle = useAnimatedStyle(() => ({ color: styles.text.color }))
+
+// ❌ styles передана в worklet
+withSpring(styles.animConfig)
+
+// ❌ styles в runOnJS
+runOnJS(() => console.log(styles.debug))
+```
+
+#### ✅ Правильно
+
+```typescript
+const styles = useStyles()
+
+// ✅ Вытащи примитив ДО worklet
+const color = styles.text.color as string
+const animConfig = styles.animConfig
+
+const animStyle = useAnimatedStyle(() => ({
+ color, // Теперь это просто строка
+}))
+
+withSpring(animConfig) // Примитив передан
+
+// Если нужны разные типы — распакуй явно
+const { width, height } = styles.icon
+runOnJS(() => {
+ console.log(width, height) // Примитивы
+})
+```
+
+---
+
+### 3. `unistyles/no-spread-icon-styles` (warn)
+
+**Предупреждает** о spread unistyles объектов при передаче в Icon компоненты.
+
+Лучше передавать явные props для понимаемости и безопасности.
+
+#### ❌ Не рекомендуется
+
+```typescript
+const styles = StyleSheet.create({
+ icon: { width: 24, height: 24, color: 'red' }
+})
+
+// Spread скрывает, какие props передаются
+
+
+```
+
+#### ✅ Рекомендуется
+
+```typescript
+const styles = StyleSheet.create({
+ icon: { width: 24, height: 24, color: 'red' }
+})
+
+// Явные props — лучше видна структура
+
+
+// Или если нужны переменные
+const color = styles.icon.color
+const width = styles.icon.width
+
+```
+
+---
+
+## Пример потери payload
+
+```typescript
+const styles = StyleSheet.create({
+ text: useColorScheme() === 'dark'
+ ? { color: '#fff' }
+ : { color: '#000' }
+})
+
+// ❌ Потеря payload при spread
+const myStyle = { ...styles.text } // payload потерян, цвет не обновится при смене темы
+
+// ✅ Payload сохранён
+const myStyle = styles.text // нет payload потерь
+style={styles.text} // payload сохранён
+```
+
+---
+
+## Как исправить существующий код
+
+### 1. Spread в объектах → используй массив
+
+```typescript
+// ❌ Было
+{ ...styles.button, marginTop: 10 }
+
+// ✅ Стало
+[styles.button, { marginTop: 10 }]
+```
+
+### 2. Worklets → вытащи примитив перед worklet
+
+```typescript
+// ❌ Было
+useAnimatedStyle(() => ({ color: styles.text.color }))
+
+// ✅ Стало
+const color = styles.text.color
+useAnimatedStyle(() => ({ color }))
+```
+
+### 3. Icon spreads → явные props
+
+```typescript
+// ❌ Было
+
+
+// ✅ Стало
+
+```
+
+---
+
+## Использование
+
+Правила автоматически включены в конфиг ESLint для всех файлов в
+`src/**/*.{ts,tsx}`.
+
+### Конфигурация
+
+```typescript
+rules: {
+ 'unistyles/no-spread-unistyles': 'error', // ⛔ Критичная ошибка
+ 'unistyles/no-unistyles-in-worklet': 'error', // ⛔ Критичная ошибка
+ 'unistyles/no-spread-icon-styles': 'warn', // ⚠️ Рекомендация
+}
+```
+
+### Проверить нарушения
+
+```bash
+npm run lint:check
+```
+
+### Автоматическое исправление
+
+```bash
+npm run lint:fix
+```
+
+---
+
+## Как это работает
+
+Правила используют **ESLint AST (Abstract Syntax Tree)** для отслеживания:
+
+1. **SpreadElement** — ловит `{ ...styles.foo }`
+2. **CallExpression** — ловит `Object.assign({}, styles.foo)`
+3. **Identifier / MemberExpression** — проверяет захват `styles` в worklet
+ closures
+4. **JSXSpreadAttribute** — ловит `{...styles}` в JSX
+
+Это гарантирует, что скрытый `unistyles_*` payload не будет случайно потерян при
+refactoring или во время разработки.
+
+---
+
+## Структура
+
+```
+configs/eslint/rules/unistyles/
+├── index.ts - Все правила и конфиг
+├── types.ts - Типы для AST узлов
+└── README.md - Эта документация
+```
+
+---
+
+## Ссылки
+
+- [Unistyles Documentation](https://www.unistyl.es)
+- [ESLint Custom Rules Guide](https://eslint.org/docs/developer-guide/working-with-rules)
+- [Unistyles with Reanimated](https://www.unistyl.es/v3/guides/reanimated/)
diff --git a/configs/eslint/rules/unistyles/index.ts b/configs/eslint/rules/unistyles/index.ts
new file mode 100644
index 00000000..a13523f7
--- /dev/null
+++ b/configs/eslint/rules/unistyles/index.ts
@@ -0,0 +1,218 @@
+import { defineConfig } from 'eslint/config'
+
+import type { ASTNode, RuleContext } from './types'
+
+/**
+ * Правило: не распаковывать стили из unistyles
+ * ❌ { ...styles.button }
+ * ❌ const btn = { ...styles.button, marginTop: 10 }
+ * ✅ [styles.button, customStyle]
+ */
+const noSpreadUnistyles = {
+ meta: {
+ type: 'problem' as const,
+ docs: {
+ description:
+ 'Запретить spread операции на стилях из StyleSheet.create() — теряется unistyles metadata',
+ category: 'Best Practices',
+ recommended: 'error' as const,
+ },
+ messages: {
+ noSpread:
+ 'Не распаковывай стили через spread ({...styles}). Это теряет unistyles metadata. Используй массив: [styles.button, customStyle]',
+ },
+ },
+ create(context: RuleContext) {
+ return {
+ SpreadElement(node: ASTNode) {
+ // Проверяем: { ...styles.foo }
+ const arg = node.argument
+
+ if (
+ arg?.type === 'MemberExpression' &&
+ arg.object?.type === 'Identifier' &&
+ arg.object.name?.includes('styles')
+ ) {
+ context.report({ node, messageId: 'noSpread' })
+ }
+ },
+
+ CallExpression(node: ASTNode) {
+ // Проверяем Object.assign({}, styles.foo)
+ if (
+ node.callee?.object?.name === 'Object' &&
+ node.callee?.property?.name === 'assign'
+ ) {
+ for (const arg of node.arguments || []) {
+ if (
+ arg?.type === 'MemberExpression' &&
+ arg.object?.name?.includes('styles')
+ ) {
+ context.report({ node, messageId: 'noSpread' })
+
+ return
+ }
+ }
+ }
+ },
+ }
+ },
+}
+
+/**
+ * Правило: не захватывать styles в worklet closures
+ * ❌ useAnimatedStyle(() => ({ color: styles.text.color }))
+ */
+const noUnistylesInWorklet = {
+ meta: {
+ type: 'problem' as const,
+ docs: {
+ description: 'Запретить захват styles переменных в worklet closures',
+ category: 'Best Practices',
+ recommended: 'error' as const,
+ },
+ messages: {
+ noCapture:
+ 'Не захватывай styles переменную в worklet. Вытащи примитив перед: const color = styles.text.color',
+ },
+ },
+ create(context: RuleContext) {
+ const workletNames = new Set([
+ 'useAnimatedStyle',
+ 'useAnimatedReaction',
+ 'runOnJS',
+ 'runOnUIThread',
+ 'withTiming',
+ 'withSpring',
+ 'withDecay',
+ 'withDelay',
+ 'withSequence',
+ 'withRepeat',
+ ])
+
+ const skipKeys = new Set(['parent', 'loc', 'range', 'start', 'end'])
+
+ const hasStylesReference = (node: ASTNode): boolean => {
+ if (!node) return false
+
+ if (node.type === 'Identifier' && node.name && node.name === 'styles') {
+ return true
+ }
+
+ if (
+ node.type === 'MemberExpression' &&
+ node.object?.type === 'Identifier' &&
+ node.object.name === 'styles'
+ ) {
+ return true
+ }
+
+ for (const key of Object.keys(node)) {
+ if (skipKeys.has(key)) {
+ // eslint-disable-next-line no-continue
+ continue
+ }
+
+ const child = node[key]
+
+ if (Array.isArray(child)) {
+ if (child.some(hasStylesReference)) return true
+ } else if (child && typeof child === 'object') {
+ if (hasStylesReference(child)) return true
+ }
+ }
+
+ return false
+ }
+
+ return {
+ CallExpression(node: ASTNode) {
+ const funcName = node.callee?.name
+
+ if (!funcName || !workletNames.has(funcName)) return
+
+ const fn = node.arguments?.[0]
+
+ if (
+ !fn ||
+ (fn.type !== 'ArrowFunctionExpression' &&
+ fn.type !== 'FunctionExpression')
+ ) {
+ return
+ }
+
+ if (hasStylesReference(fn.body)) {
+ context.report({ node: fn, messageId: 'noCapture' })
+ }
+ },
+ }
+ },
+}
+
+/**
+ * Правило: не спредить styles в Icon компонентах
+ * ❌
+ * ✅
+ */
+const noSpreadIconStyles = {
+ meta: {
+ type: 'suggestion' as const,
+ docs: {
+ description: 'Передавай явные props для Icon вместо spread',
+ category: 'Best Practices',
+ recommended: 'warn' as const,
+ },
+ messages: {
+ noSpread:
+ 'Не спредь styles в Icon. Передавай явные props: width, height, color',
+ },
+ },
+ create(context: RuleContext) {
+ return {
+ JSXSpreadAttribute(node: ASTNode) {
+ // Проверяем, что это styles.something
+ const arg = node.argument
+
+ if (
+ arg?.type === 'MemberExpression' &&
+ arg.object?.name &&
+ arg.object.name.includes('styles')
+ ) {
+ // Проверяем, что мы в Icon компоненте
+ const parent = node.parent
+
+ if (parent?.type === 'JSXOpeningElement') {
+ const tagName = parent.name?.name || ''
+
+ if (tagName.includes('Icon')) {
+ context.report({ node, messageId: 'noSpread' })
+ }
+ }
+ }
+ },
+ }
+ },
+}
+
+/**
+ * Интегрируем все правила в конфиг
+ */
+export const unistylesConfig = defineConfig([
+ {
+ files: ['src/**/*.{ts,tsx}'],
+ plugins: {
+ unistyles: {
+ rules: {
+ 'no-spread-unistyles': noSpreadUnistyles,
+ 'no-unistyles-in-worklet': noUnistylesInWorklet,
+ 'no-spread-icon-styles': noSpreadIconStyles,
+ },
+ },
+ },
+ rules: {
+ 'unistyles/no-spread-unistyles': 'error',
+ 'unistyles/no-unistyles-in-worklet': 'error',
+ 'unistyles/no-spread-icon-styles': 'warn',
+ },
+ },
+])
diff --git a/configs/eslint/rules/unistyles/types.ts b/configs/eslint/rules/unistyles/types.ts
new file mode 100644
index 00000000..31f4a7a6
--- /dev/null
+++ b/configs/eslint/rules/unistyles/types.ts
@@ -0,0 +1,10 @@
+import type { Rule } from 'eslint'
+
+export type RuleContext = Rule.RuleContext
+
+/**
+ * ESLint AST узлы имеют сложную типизацию с дискриминированными типами.
+ * Используем more practical подход с type guards и indexed access.
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type ASTNode = any
diff --git a/eslint.config.ts b/eslint.config.ts
index 509fd717..063ecab3 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -5,9 +5,28 @@ import { MobileConfig } from './configs/eslint'
export default defineConfig([
...MobileConfig,
{ files: ['configs/eslint/**/*'], rules: { 'max-lines': 'off' } },
+ // Временное решение до миграции компонентов
+ {
+ files: ['src/components/**/*.{ts,tsx}'],
+ rules: { '@typescript-eslint/no-deprecated': 'off' },
+ },
{
ignores: [
+ 'node_modules/**/*',
+ '.expo/**/*',
+ '.git/**/*',
+ '.idea/**/*',
'dist/**/*',
+ 'build/**/*',
+ 'coverage/**/*',
+ '**/*.min.js',
+ '.gradle/**/*',
+ 'android/**/*',
+ 'ios/**/*',
+ '.yarn/**/*',
+ '.vscode/**/*',
+ '.jest/**/*',
+ '.gemini/**/*',
'.storybook/**/*',
'configs/cz-conventional-mobile/**/*',
],
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 4f3054a0..69c230d6 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -64,6 +64,29 @@ PODS:
- hermes-engine (0.81.5):
- hermes-engine/Pre-built (= 0.81.5)
- hermes-engine/Pre-built (0.81.5)
+ - NitroModules (0.35.2):
+ - hermes-engine
+ - RCTRequired
+ - RCTTypeSafety
+ - React-callinvoker
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
- RCTDeprecation (0.81.5)
- RCTRequired (0.81.5)
- RCTTypeSafety (0.81.5):
@@ -1896,7 +1919,7 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- - RNReanimated (4.1.1):
+ - RNReanimated (4.2.1):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -1918,10 +1941,10 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- - RNReanimated/reanimated (= 4.1.1)
+ - RNReanimated/reanimated (= 4.2.1)
- RNWorklets
- Yoga
- - RNReanimated/reanimated (4.1.1):
+ - RNReanimated/reanimated (4.2.1):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -1943,10 +1966,10 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- - RNReanimated/reanimated/apple (= 4.1.1)
+ - RNReanimated/reanimated/apple (= 4.2.1)
- RNWorklets
- Yoga
- - RNReanimated/reanimated/apple (4.1.1):
+ - RNReanimated/reanimated/apple (4.2.1):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -2015,7 +2038,7 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- - RNWorklets (0.5.1):
+ - RNWorklets (0.7.1):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -2037,9 +2060,9 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- - RNWorklets/worklets (= 0.5.1)
+ - RNWorklets/worklets (= 0.7.1)
- Yoga
- - RNWorklets/worklets (0.5.1):
+ - RNWorklets/worklets (0.7.1):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -2061,9 +2084,9 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- - RNWorklets/worklets/apple (= 0.5.1)
+ - RNWorklets/worklets/apple (= 0.7.1)
- Yoga
- - RNWorklets/worklets/apple (0.5.1):
+ - RNWorklets/worklets/apple (0.7.1):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@@ -2086,6 +2109,29 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
+ - Unistyles (3.2.3):
+ - hermes-engine
+ - NitroModules
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-Core-prebuilt
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactNativeDependencies
+ - Yoga
- Yoga (0.0.0)
DEPENDENCIES:
@@ -2099,6 +2145,7 @@ DEPENDENCIES:
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
+ - NitroModules (from `../node_modules/react-native-nitro-modules`)
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
@@ -2173,6 +2220,7 @@ DEPENDENCIES:
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNWorklets (from `../node_modules/react-native-worklets`)
+ - Unistyles (from `../node_modules/react-native-unistyles`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
@@ -2201,6 +2249,8 @@ EXTERNAL SOURCES:
hermes-engine:
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
:tag: hermes-2025-07-07-RNv0.81.0-e0fc67142ec0763c6b6153ca2bf96df815539782
+ NitroModules:
+ :path: "../node_modules/react-native-nitro-modules"
RCTDeprecation:
:path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
RCTRequired:
@@ -2347,6 +2397,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-svg"
RNWorklets:
:path: "../node_modules/react-native-worklets"
+ Unistyles:
+ :path: "../node_modules/react-native-unistyles"
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
@@ -2362,6 +2414,7 @@ SPEC CHECKSUMS:
FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12
ForkInputMask: 55e3fbab504b22da98483e9f9a6514b98fdd2f3c
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172
+ NitroModules: 76063cb7bc1a21cf46d11b25abfcf1759bf0be47
RCTDeprecation: 943572d4be82d480a48f4884f670135ae30bf990
RCTRequired: 8f3cfc90cc25cf6e420ddb3e7caaaabc57df6043
RCTTypeSafety: 16a4144ca3f959583ab019b57d5633df10b5e97c
@@ -2432,9 +2485,10 @@ SPEC CHECKSUMS:
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
RNDateTimePicker: 19ffa303c4524ec0a2dfdee2658198451c16b7f1
RNGestureHandler: 723f29dac55e25f109d263ed65cecc4b9c4bd46a
- RNReanimated: 6e0147e13f8906f63703143f40237f84347e6ca1
+ RNReanimated: 8a7182314bb7afc01041a529e409a9112c007a50
RNSVG: cf9ae78f2edf2988242c71a6392d15ff7dd62522
- RNWorklets: 76fce72926e28e304afb44f0da23b2d24f2c1fa0
+ RNWorklets: 9eb6d567fa43984e96b6924a6df504b8a15980cd
+ Unistyles: 6bb7e273c90d75b4f6bcb68fd1b94fb7b246145b
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
PODFILE CHECKSUM: 64cb709f656081a8373e391252a6bd658b57e3c1
diff --git a/jest.config.ts b/jest.config.ts
index b5a706d8..4a42d615 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -14,9 +14,12 @@ const config: Config.InitialOptions = {
coverageReporters: ['text', 'text-summary'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
testRunner: 'jest-circus',
- maxWorkers: 4,
+ maxWorkers: '100%',
rootDir: '.',
- moduleNameMapper: { '\\.svg': '/__mocks__/svgMock.js' },
+ moduleNameMapper: {
+ '\\.svg': '/__mocks__/svgMock.js',
+ '^react-native-worklets$': 'react-native-worklets/lib/module/mock',
+ },
setupFiles: ['/jest.setup.ts'],
setupFilesAfterEnv: ['jest-extended/all'],
}
diff --git a/jest.d.ts b/jest.d.ts
index a86a4041..d41f79d0 100644
--- a/jest.d.ts
+++ b/jest.d.ts
@@ -3,3 +3,9 @@ type PropertyCombinations = { [K in keyof T]: Array }
declare let generatePropsCombinations: (
properties: PropertyCombinations
) => T[]
+
+declare module '@react-native/normalize-colors' {
+ const normalizeColors: (color: string | number) => number | null
+
+ export default normalizeColors
+}
diff --git a/jest.setup.ts b/jest.setup.ts
index 5fbb78c5..fd4df2ef 100644
--- a/jest.setup.ts
+++ b/jest.setup.ts
@@ -1,9 +1,143 @@
import 'jest-extended'
import 'react-native-gesture-handler/jestSetup'
import { setUpTests } from 'react-native-reanimated'
+import 'react-native-unistyles/mocks'
setUpTests()
+type ThemeName = 'light' | 'dark'
+
+interface MockedUnistylesModule {
+ StyleSheet: { create: (...args: unknown[]) => unknown }
+ UnistylesRuntime: {
+ miniRuntime: unknown
+ setTheme: jest.Mock
+ themeName: ThemeName | undefined
+ }
+ useUnistyles: jest.Mock
+ withUnistyles: jest.Mock
+}
+
+const { darkTheme, lightTheme } = require('./src/theme')
+
+const unistyles = jest.requireMock(
+ 'react-native-unistyles'
+) as MockedUnistylesModule
+
+const getTheme = (themeName: ThemeName | undefined) =>
+ themeName === 'dark' ? darkTheme : lightTheme
+
+const runtime = unistyles.UnistylesRuntime
+
+runtime.themeName = 'light'
+runtime.setTheme = jest.fn((themeName: ThemeName) => {
+ runtime.themeName = themeName
+})
+
+unistyles.useUnistyles = jest.fn(() => ({
+ theme: getTheme(runtime.themeName),
+ rt: runtime,
+}))
+
+const normalizeVariantValue = (value: unknown) => {
+ if (typeof value === 'boolean') {
+ return value ? 'true' : 'false'
+ }
+
+ return value
+}
+
+const resolveStyle = (
+ style: Record,
+ activeVariants: Record
+) => {
+ const { variants, compoundVariants, ...baseStyle } = style as {
+ variants?: Record>>
+ compoundVariants?: Array>
+ }
+
+ const resolvedStyle: Record = { ...baseStyle }
+
+ if (variants) {
+ for (const [variantName, variantMap] of Object.entries(variants)) {
+ const activeValue = normalizeVariantValue(activeVariants[variantName])
+
+ if (
+ activeValue !== undefined &&
+ variantMap[activeValue as keyof typeof variantMap]
+ ) {
+ Object.assign(
+ resolvedStyle,
+ variantMap[activeValue as keyof typeof variantMap]
+ )
+ }
+ }
+ }
+
+ if (compoundVariants) {
+ for (const compoundVariant of compoundVariants) {
+ const { styles, ...conditions } = compoundVariant
+ const matches = Object.entries(conditions).every(
+ ([variantName, expectedValue]) =>
+ normalizeVariantValue(activeVariants[variantName]) ===
+ normalizeVariantValue(expectedValue)
+ )
+
+ if (matches && styles && typeof styles === 'object') {
+ Object.assign(resolvedStyle, styles)
+ }
+ }
+ }
+
+ return resolvedStyle
+}
+
+unistyles.StyleSheet.create = jest.fn(
+ (stylesheet: ((theme: ReturnType) => unknown) | unknown) => {
+ const styleDefinitions =
+ typeof stylesheet === 'function'
+ ? stylesheet(getTheme(runtime.themeName))
+ : stylesheet
+
+ const activeVariants: Record = {}
+ const resolvedStyles: Record = {
+ useVariants: (variants: Record) => {
+ Object.keys(activeVariants).forEach((key) => {
+ activeVariants[key] = undefined
+ })
+
+ Object.assign(activeVariants, variants)
+ },
+ }
+
+ for (const [styleName, styleValue] of Object.entries(
+ styleDefinitions as Record
+ )) {
+ Object.defineProperty(resolvedStyles, styleName, {
+ enumerable: true,
+ get() {
+ if (
+ styleValue &&
+ typeof styleValue === 'object' &&
+ !Array.isArray(styleValue)
+ ) {
+ return resolveStyle(
+ styleValue as Record,
+ activeVariants
+ )
+ }
+
+ return styleValue
+ },
+ })
+ }
+
+ return resolvedStyles
+ }
+)
+
+unistyles.withUnistyles = jest.fn((Component: T) => Component)
+
generatePropsCombinations = (properties: PropertyCombinations): T[] => {
const keys = Object.keys(properties) as Array
diff --git a/package.json b/package.json
index 9f0494ed..3115e47f 100644
--- a/package.json
+++ b/package.json
@@ -27,14 +27,14 @@
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.build.json",
"test": "jest",
- "start": "expo start --dev-client",
+ "start": "expo start --dev-client --clear",
"android": "expo run:android",
"ios": "expo run:ios --no-install",
"storybook-generate": "sb-rn-get-stories --config-path .storybook && sed -i -e 's/export const view = global.view/export const view: ReturnType = global.view/' .storybook/storybook.requires.ts && prettier .storybook --write",
"doctor": "expo-doctor",
"check": "expo install --check",
- "lint:check": "eslint .",
- "lint:fix": "eslint --fix .",
+ "lint:check": "eslint --cache .",
+ "lint:fix": "eslint --fix --cache .",
"prettier:check": "prettier . --check",
"prettier:fix": "prettier . --write",
"prettier:watch": "onchange . -- prettier --write --ignore-unknown \"{{changed}}\"",
@@ -42,7 +42,7 @@
"pod-install": "bundle exec pod install --project-directory=ios"
},
"dependencies": {
- "@tabler/icons-react-native": "^3.36.0"
+ "@tabler/icons-react-native": "3.36.1"
},
"devDependencies": {
"@babel/core": "7.28.5",
@@ -106,10 +106,12 @@
"react-native": "0.81.5",
"react-native-advanced-input-mask": "1.4.6",
"react-native-gesture-handler": "2.29.1",
- "react-native-reanimated": "4.1.1",
+ "react-native-nitro-modules": "0.35.2",
+ "react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "5.6.2",
"react-native-svg": "15.15.1",
- "react-native-worklets": "0.5.1",
+ "react-native-unistyles": "3.2.3",
+ "react-native-worklets": "0.7.1",
"release-it": "19.1.0",
"standard-version": "9.5.0",
"storybook": "10.1.10",
@@ -123,8 +125,10 @@
"expo": ">=54.x.x",
"react": ">=19.1",
"react-native": ">=0.81.5",
- "react-native-reanimated": ">=4.1.1",
- "react-native-svg": ">=15.15.1"
+ "react-native-reanimated": ">=4.2.1",
+ "react-native-svg": ">=15.15.1",
+ "react-native-unistyles": ">=3.2.3",
+ "react-native-worklets": ">=0.7.0"
},
"peerDependenciesMeta": {
"expo": {
diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx
index 9af5bde7..f596b0d5 100644
--- a/src/components/Accordion/Accordion.tsx
+++ b/src/components/Accordion/Accordion.tsx
@@ -10,8 +10,8 @@ import Animated, {
import type { ViewProps } from 'react-native-svg/lib/typescript/fabric/utils'
+import { StyleSheet } from '../../utils'
import { type SvgSource, SvgUniversal } from '../../utils/SvgUniversal'
-import { makeStyles } from '../../utils/makeStyles'
export interface AccordionProps extends ViewProps {
/** Иконка слева от заголовка */
@@ -50,8 +50,6 @@ export const Accordion: React.FC = ({
children,
...rest
}) => {
- const styles = useStyles()
-
const contentHeight = useSharedValue(0)
const contentOpenFraction = useSharedValue(initiallyExpanded ? 1 : 0)
const [isExpanded, setIsExpanded] = useState(initiallyExpanded)
@@ -107,13 +105,20 @@ export const Accordion: React.FC = ({
style={arrowAnimatedStyle}
testID={AccordionTestIds.arrow}
>
-
+
{Icon ? (
) : null}
{title}
@@ -151,7 +156,7 @@ export const AccordionTestIds = {
separator: 'Separator',
}
-const useStyles = makeStyles(({ theme, fonts }) => ({
+const styles = StyleSheet.create(({ theme, fonts }) => ({
component: { width: '100%' },
header: {
paddingVertical: theme.Panel.Accordion.accordionHeaderPaddingTopBottom,
diff --git a/src/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap b/src/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap
index 163abe4c..88883a29 100644
--- a/src/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap
+++ b/src/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap
@@ -121,6 +121,7 @@ exports[`Accordion Header elements maximal 1`] = `
},
]
}
+ testID="SvgUniversalComponent"
vbHeight={24}
vbWidth={24}
width={17.5}
@@ -168,6 +169,7 @@ exports[`Accordion Header elements maximal 1`] = `
strokeLinecap={1}
strokeLinejoin={1}
strokeWidth={2}
+ testID="SvgUniversalComponent"
/>
@@ -344,7 +346,7 @@ exports[`Accordion Header elements maximal 1`] = `
strokeWidth={2}
>
diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx
index bfae1fba..755c3a28 100644
--- a/src/components/Avatar/Avatar.tsx
+++ b/src/components/Avatar/Avatar.tsx
@@ -21,8 +21,8 @@ import {
type ViewStyle,
} from 'react-native'
+import { StyleSheet } from '../../utils'
import { type SvgSource, SvgUniversal } from '../../utils/SvgUniversal'
-import { makeStyles } from '../../utils/makeStyles'
export type AvatarSize = 'xlarge' | 'large' | 'normal'
@@ -137,7 +137,6 @@ export const Avatar = memo(
onError,
iconColor,
}) => {
- const styles = useStyles()
const window = useWindowDimensions()
const [badgeLayout, setBadgeLayout] = useState()
@@ -192,7 +191,7 @@ export const Avatar = memo(
width={iconSize}
/>
)
- }, [Icon, calculatedSize, iconColor, size, styles, type])
+ }, [Icon, calculatedSize, iconColor, size, type])
useEffect(() => {
if (badge) {
@@ -255,7 +254,7 @@ export const Avatar = memo(
}
)
-const useStyles = makeStyles(({ theme, border, typography, fonts }) => ({
+const styles = StyleSheet.create(({ theme, border, typography, fonts }) => ({
container: {
justifyContent: 'center',
alignItems: 'center',
diff --git a/src/components/Avatar/__tests__/__snapshots__/Avatar.test.tsx.snap b/src/components/Avatar/__tests__/__snapshots__/Avatar.test.tsx.snap
index 734dacdd..87f18c6d 100644
--- a/src/components/Avatar/__tests__/__snapshots__/Avatar.test.tsx.snap
+++ b/src/components/Avatar/__tests__/__snapshots__/Avatar.test.tsx.snap
@@ -1701,17 +1701,13 @@ exports[`Avatar component tests With badge, showBadge: true 1`] = `
>
(
- ({ children, dot, severity = 'basic', style, testID, ...rest }) => {
- const styles = useStyles()
+export const Badge = memo(
+ ({
+ children,
+ dot,
+ severity = 'basic',
+ style,
+ testID,
+ ...rest
+ }: BadgeProps) => {
+ badgeStyles.useVariants({ severity })
const [textLayout, setTextLayout] = useState()
const onTextLayout = useCallback((e: LayoutChangeEvent) => {
@@ -60,18 +67,15 @@ export const Badge = memo(
}, [])
return (
-
+
{dot ? (
-
+
) : (
<>
-
+
{children}
@@ -81,12 +85,12 @@ export const Badge = memo(
{children}
@@ -100,41 +104,70 @@ export const Badge = memo(
}
)
-const useStyles = makeStyles(({ theme, border, typography, fonts }) => ({
- container: { alignItems: 'flex-start' },
- dot: {
- width: theme.Misc.Badge.badgeDotSize,
- height: theme.Misc.Badge.badgeDotSize,
- borderRadius: border.Radius['rounded-full'],
- },
- textBadgeContainer: {
- height: theme.Misc.Badge.badgeHeight,
- paddingHorizontal: theme.Misc.Tag.tagPadding,
- justifyContent: 'center',
- borderRadius: border.Radius['rounded-full'],
- },
- textBadge: {
- color: theme.Misc.Badge.badgeTextColor,
- fontSize: typography.Size['text-xs'],
- includeFontPadding: false,
- verticalAlign: 'middle',
- fontFamily: fonts.primary,
- },
- basic: { backgroundColor: theme.Misc.Badge.badgeBg },
- info: { backgroundColor: theme.Button.Severity.Info.Basic.infoButtonBg },
- success: {
- backgroundColor: theme.Button.Severity.Success.Basic.successButtonBg,
- },
- warning: {
- backgroundColor: theme.Button.Severity.Warning.Basic.warningButtonBg,
- },
- danger: {
- backgroundColor: theme.Button.Severity.Danger.Basic.dangerButtonBg,
- },
- hiddenContainer: {
- width: Dimensions.get('window').width,
- height: 0,
- flexDirection: 'row',
- position: 'absolute',
- },
-}))
+const badgeStyles = StyleSheet.create(
+ ({ theme, border, typography, fonts }) => ({
+ container: { alignItems: 'flex-start' },
+ dot: {
+ width: theme.Misc.Badge.badgeDotSize,
+ height: theme.Misc.Badge.badgeDotSize,
+ borderRadius: border.Radius['rounded-full'],
+ variants: {
+ severity: {
+ basic: { backgroundColor: theme.Misc.Badge.badgeBg },
+ info: {
+ backgroundColor: theme.Button.Severity.Info.Basic.infoButtonBg,
+ },
+ success: {
+ backgroundColor:
+ theme.Button.Severity.Success.Basic.successButtonBg,
+ },
+ warning: {
+ backgroundColor:
+ theme.Button.Severity.Warning.Basic.warningButtonBg,
+ },
+ danger: {
+ backgroundColor: theme.Button.Severity.Danger.Basic.dangerButtonBg,
+ },
+ },
+ },
+ },
+ textBadgeContainer: {
+ height: theme.Misc.Badge.badgeHeight,
+ paddingHorizontal: theme.Misc.Tag.tagPadding,
+ justifyContent: 'center',
+ borderRadius: border.Radius['rounded-full'],
+ variants: {
+ severity: {
+ basic: { backgroundColor: theme.Misc.Badge.badgeBg },
+ info: {
+ backgroundColor: theme.Button.Severity.Info.Basic.infoButtonBg,
+ },
+ success: {
+ backgroundColor:
+ theme.Button.Severity.Success.Basic.successButtonBg,
+ },
+ warning: {
+ backgroundColor:
+ theme.Button.Severity.Warning.Basic.warningButtonBg,
+ },
+ danger: {
+ backgroundColor: theme.Button.Severity.Danger.Basic.dangerButtonBg,
+ },
+ },
+ },
+ },
+ textBadge: {
+ color: theme.Misc.Badge.badgeTextColor,
+ fontSize: typography.Size['text-xs'],
+ includeFontPadding: false,
+ verticalAlign: 'middle',
+ fontFamily: fonts.primary,
+ },
+ hiddenContainer: {
+ width: Dimensions.get('window').width,
+ height: 0,
+ flexDirection: 'row',
+ position: 'absolute',
+ },
+ })
+)
diff --git a/src/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap b/src/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap
index e5b425a8..0cac9d85 100644
--- a/src/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap
+++ b/src/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap
@@ -13,16 +13,12 @@ exports[`Badge component tests dot, severity: basic 1`] = `
>
@@ -41,16 +37,12 @@ exports[`Badge component tests dot, severity: danger 1`] = `
>
@@ -69,16 +61,12 @@ exports[`Badge component tests dot, severity: info 1`] = `
>
@@ -97,16 +85,12 @@ exports[`Badge component tests dot, severity: success 1`] = `
>
@@ -125,16 +109,12 @@ exports[`Badge component tests dot, severity: warning 1`] = `
>
@@ -153,17 +133,13 @@ exports[`Badge component tests severity: basic 1`] = `
>
diff --git a/src/components/Button/BaseButton.tsx b/src/components/Button/BaseButton.tsx
index 4b604e3b..5e752b27 100644
--- a/src/components/Button/BaseButton.tsx
+++ b/src/components/Button/BaseButton.tsx
@@ -1,21 +1,21 @@
import { useCallback, useState } from 'react'
-
import type { GestureResponderEvent } from 'react-native'
import { genericMemo } from '../../utils/genericMemo'
-import type { ButtonProps, ButtonVariant, VariantStyles } from './types'
+import type { ButtonProps, ButtonVariant } from './types'
import {
ButtonLeftArea,
ButtonRightArea,
ButtonLabel,
ButtonContainer,
} from './utils'
+import { ButtonPressedContext } from './utils/ButtonPressedContext'
export type BaseButtonComponentProps = Omit<
ButtonProps,
'variant'
-> & { readonly variant: Variant } & VariantStyles
+> & { readonly variant: Variant }
const BaseButtonComponent = ({
size = 'base',
@@ -28,15 +28,11 @@ const BaseButtonComponent = ({
Icon,
label,
style,
- containerVariantStyles,
- labelVariantStyles,
- pressedVariantStyles,
- iconVariantStyles,
- pressedLabelVariantStyles,
onPressIn: onPressInProp,
onPressOut: onPressOutProp,
...props
}: BaseButtonComponentProps) => {
+ const isDisabled = !!disabled
const [pressed, setPressed] = useState(false)
const onPressIn = useCallback(
@@ -56,62 +52,31 @@ const BaseButtonComponent = ({
)
return (
-
-
-
-
+
-
+ {...props}
+ >
+
+
+
+
+
)
}
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index 1ac8c549..539561d4 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -1,8 +1,8 @@
-import { memo } from 'react'
+import { memo, useMemo } from 'react'
import { BaseButton } from './BaseButton'
-import { useBasicButtonStyles } from './styles'
import type { ButtonBaseVariant, ButtonProps } from './types'
+import { ButtonVariantContext } from './utils/ButtonVariantContext'
/**
* Button component
@@ -18,10 +18,14 @@ import type { ButtonBaseVariant, ButtonProps } from './types'
* @param style - external style control for component
* @see BaseButton
*/
-export const Button = memo>(
- ({ variant = 'primary', ...props }) => {
- const buttonStyles = useBasicButtonStyles()
+export const Button = memo(
+ ({ variant = 'primary', ...props }: ButtonProps) => {
+ const variantContextValue = useMemo(() => ({ variant }), [variant])
- return
+ return (
+
+
+
+ )
}
)
diff --git a/src/components/Button/ButtonBadge.tsx b/src/components/Button/ButtonBadge.tsx
index 1dfad821..8e9ab6d0 100644
--- a/src/components/Button/ButtonBadge.tsx
+++ b/src/components/Button/ButtonBadge.tsx
@@ -6,12 +6,12 @@ import {
type ViewStyle,
} from 'react-native'
-import { makeStyles } from '../../utils/makeStyles'
+import { StyleSheet } from '../../utils'
import { Badge } from '../Badge'
import { BaseButton } from './BaseButton'
-import { useBasicButtonStyles } from './styles'
import type { ButtonBadgeProps, ButtonBaseVariant, ButtonProps } from './types'
+import { ButtonVariantContext } from './utils/ButtonVariantContext'
/**
* Button component with badge
@@ -29,59 +29,65 @@ import type { ButtonBadgeProps, ButtonBaseVariant, ButtonProps } from './types'
* @param badgeLabel - text label inside badge
* @see BaseButton
*/
-export const ButtonBadge = memo<
- ButtonProps & ButtonBadgeProps
->(({ badgeLabel, badgeSeverity, variant = 'primary', ...props }) => {
- const buttonStyles = useBasicButtonStyles()
- const styles = useStyles()
- const [badgeLayout, setBadgeLayout] = useState()
+export const ButtonBadge = memo(
+ ({
+ badgeLabel,
+ badgeSeverity,
+ variant = 'primary',
+ ...props
+ }: ButtonProps & ButtonBadgeProps) => {
+ const [badgeLayout, setBadgeLayout] = useState()
+ const variantContextValue = useMemo(() => ({ variant }), [variant])
- const badgeContainerStyle = useMemo(
- () => ({
- position: 'absolute',
- top: badgeLayout ? -Math.round(badgeLayout.height / 2) : 0,
- right: badgeLayout ? -Math.round(badgeLayout.width / 2) : 0,
- }),
- [badgeLayout]
- )
+ const badgeContainerStyle = useMemo(
+ () => ({
+ position: 'absolute',
+ top: badgeLayout ? -Math.round(badgeLayout.height / 2) : 0,
+ right: badgeLayout ? -Math.round(badgeLayout.width / 2) : 0,
+ }),
+ [badgeLayout]
+ )
- const onLayout = useCallback(
- (e: LayoutChangeEvent) => setBadgeLayout(e.nativeEvent.layout),
- []
- )
+ const onLayout = useCallback(
+ (e: LayoutChangeEvent) => setBadgeLayout(e.nativeEvent.layout),
+ []
+ )
- const badgeCommonProps = useMemo(
- () => ({ severity: badgeSeverity, testID: ButtonBadgeTestId.badge }),
- [badgeSeverity]
- )
+ const badgeCommonProps = useMemo(
+ () => ({ severity: badgeSeverity, testID: ButtonBadgeTestId.badge }),
+ [badgeSeverity]
+ )
- return (
-
-
-
-
- {badgeLabel ? (
-
+
+
- {badgeLabel}
-
- ) : (
-
- )}
-
-
- )
-})
+
+
+ {badgeLabel ? (
+
+ {badgeLabel}
+
+ ) : (
+
+ )}
+
+
+
+ )
+ }
+)
-const useStyles = makeStyles(() => ({
+const styles = StyleSheet.create(() => ({
root: { flexDirection: 'row' },
contentContainer: { flex: 1 },
iconOnlyContainer: { flex: 0 },
diff --git a/src/components/Button/ButtonSeverity.tsx b/src/components/Button/ButtonSeverity.tsx
index 0f017a3d..8bd9780d 100644
--- a/src/components/Button/ButtonSeverity.tsx
+++ b/src/components/Button/ButtonSeverity.tsx
@@ -1,12 +1,12 @@
-import { memo } from 'react'
+import { memo, useMemo } from 'react'
import { BaseButton } from './BaseButton'
-import { useSeverityButtonStyles } from './styles'
import type {
ButtonProps,
ButtonSeverityProps,
ButtonSeverityVariant,
} from './types'
+import { ButtonVariantContext } from './utils/ButtonVariantContext'
/**
* Button component
@@ -23,10 +23,21 @@ import type {
* @param severity - severity button styling variant
* @see BaseButton
*/
-export const ButtonSeverity = memo<
- ButtonProps & ButtonSeverityProps
->(({ severity, variant = 'basic', ...props }) => {
- const buttonStyles = useSeverityButtonStyles(severity)
+export const ButtonSeverity = memo(
+ ({
+ severity,
+ variant = 'basic',
+ ...props
+ }: ButtonProps & ButtonSeverityProps) => {
+ const variantContextValue = useMemo(
+ () => ({ variant, severity }),
+ [severity, variant]
+ )
- return
-})
+ return (
+
+
+
+ )
+ }
+)
diff --git a/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap b/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap
index 58dda768..cc40367f 100644
--- a/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap
+++ b/src/components/Button/__tests__/__snapshots__/Button.test.tsx.snap
@@ -33,21 +33,24 @@ exports[`Button component tests Button default props 1`] = `
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
- {
- "alignItems": "center",
- "backgroundColor": "#44e858",
- "borderColor": "rgba(255, 255, 255, 0.0001)",
- "borderRadius": 10.5,
- "borderWidth": 1,
- "flexDirection": "row",
- "gap": 7,
- "height": 35,
- "justifyContent": "center",
- "maxHeight": 35,
- "minHeight": 35,
- "paddingHorizontal": 14,
- "paddingVertical": 0,
- }
+ [
+ {
+ "alignItems": "center",
+ "backgroundColor": "#44e858",
+ "borderColor": "rgba(255, 255, 255, 0.0001)",
+ "borderRadius": 10.5,
+ "borderWidth": 1,
+ "flexDirection": "row",
+ "gap": 7,
+ "height": 35,
+ "justifyContent": "center",
+ "maxHeight": 35,
+ "minHeight": 35,
+ "paddingHorizontal": 14,
+ "paddingVertical": 0,
+ },
+ false,
+ ]
}
>
+
+
+
+
+
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - base, shape - circle, variant - secondary, loading - true, disabled - true 2`] = `
+
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - base, shape - circle, variant - secondary, loading - true, disabled - true 2`] = `
-
-
-
-
-
+
+
+
+
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - false, disabled - false 1`] = `
+
+
`;
-exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - false, disabled - false 1`] = `
+exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - false, disabled - false 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - false, disabled - false 2`] = `
+exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - false, disabled - true 1`] = `
`;
-exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - false, disabled - true 1`] = `
+exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - false, disabled - true 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - false, disabled - true 2`] = `
+exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - true, disabled - false 1`] = `
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - true, disabled - false 2`] = `
+
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - true, disabled - true 1`] = `
+
`;
-exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - true, disabled - false 1`] = `
+exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - true, disabled - true 2`] = `
-
+
+
+
+
+
+
`;
-exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - true, disabled - false 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - false, disabled - false 1`] = `
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - true, disabled - true 1`] = `
-
`;
-exports[`Button component tests Button with icon on left, size - base, shape - square, variant - text, loading - true, disabled - true 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - false, disabled - false 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - false, disabled - false 1`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - false, disabled - true 1`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - false, disabled - false 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - false, disabled - true 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - false, disabled - true 1`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - true, disabled - false 1`] = `
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - true, disabled - false 2`] = `
+
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - true, disabled - true 1`] = `
+
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - false, disabled - true 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - true, disabled - true 2`] = `
-
-
-
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - true, disabled - false 1`] = `
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - true, disabled - false 2`] = `
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - true, disabled - true 1`] = `
-
-
-
-
-
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - link, loading - true, disabled - true 2`] = `
-
-
-
-
-
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - false, disabled - true 2`] = `
-
-
-
-
-
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - true, disabled - false 1`] = `
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - true, disabled - false 2`] = `
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - true, disabled - true 1`] = `
-
-
-
-
-
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - true, disabled - true 2`] = `
-
-
-
-
-
-
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - false, disabled - false 1`] = `
-
-
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - false, disabled - false 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - false, disabled - true 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - false, disabled - true 1`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - true, disabled - false 1`] = `
+
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - true, disabled - false 2`] = `
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - true, disabled - true 1`] = `
+
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - false, disabled - true 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - primary, loading - true, disabled - true 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - true, disabled - false 1`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - false, disabled - false 1`] = `
-
+
+
+
+
+
+
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - true, disabled - false 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - false, disabled - false 2`] = `
-
+
+
+
+
+
+
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - true, disabled - true 1`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - false, disabled - true 1`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - true, disabled - true 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - false, disabled - true 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - false, disabled - false 1`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - true, disabled - false 1`] = `
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - true, disabled - false 2`] = `
+
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - true, disabled - true 1`] = `
+
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - false, disabled - false 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - secondary, loading - true, disabled - true 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - false, disabled - true 1`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - false, disabled - false 1`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - false, disabled - true 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - false, disabled - false 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - true, disabled - false 1`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - false, disabled - true 1`] = `
-
-
- Button
-
-
-`;
-
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - true, disabled - false 2`] = `
-
-
+
+
+
+
+
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - true, disabled - true 1`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - false, disabled - true 2`] = `
`;
-exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - true, disabled - true 2`] = `
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - true, disabled - false 1`] = `
+
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - true, disabled - false 2`] = `
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - true, disabled - true 1`] = `
+
+
+
+
+
+
+
+ Button
+
+
+`;
+
+exports[`Button component tests Button with icon on left, size - large, shape - circle, variant - tertiary, loading - true, disabled - true 2`] = `
+
+