From 60799b0ececf9e3d5c60997b16042b40a68e51d4 Mon Sep 17 00:00:00 2001 From: Julien Texier Date: Mon, 9 Jun 2025 10:10:06 +0300 Subject: [PATCH 1/8] fix: update line height calculation for typography variants to prevent text cutting --- src/styles/helpers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/styles/helpers.ts b/src/styles/helpers.ts index 78ffe21b..8665056f 100644 --- a/src/styles/helpers.ts +++ b/src/styles/helpers.ts @@ -77,9 +77,9 @@ export function getTextTypographyVariants() { (Object.keys(typographyTokens) as Typography[]).forEach((variant) => { typographyVariants[variant] = { typography: variant, - // Apply line height only for multiline text since by default app UI text - // should not have a line height bigger than `1` (same as font size) - lineHeight: typographyTokens[variant].fontSize, + // The 1.25 multiplier ensures enough vertical space for ascenders and descenders, + // preventing text from being visually cropped. + lineHeight: typographyTokens[variant].fontSize * 1.25, }; compoundVariants.push({ From c7d6e149db47a1c4772b3ee521c4ab7a519f48c3 Mon Sep 17 00:00:00 2001 From: Julien Texier Date: Wed, 23 Jul 2025 18:58:44 +0300 Subject: [PATCH 2/8] chore: move styles to unistyles --- app.config.ts | 4 +- babel.config.js | 1 + index.ts | 2 + package-lock.json | 58 ++++- package.json | 5 +- src/Providers.tsx | 36 +-- src/app/(auth)/index.tsx | 216 +++++++++--------- src/app/(auth)/index.web.tsx | 167 -------------- src/app/(auth)/login.tsx | 37 +-- src/app/(auth)/signup.tsx | 33 +-- src/app/(tabs)/_layout.tsx | 15 +- src/app/(tabs)/_layout.web.tsx | 5 +- src/app/(tabs)/home.tsx | 25 +- src/app/(tabs)/profile.tsx | 22 +- src/app/(tabs)/search.tsx | 22 +- src/app/(tabs)/settings.tsx | 22 +- src/app/+html.tsx | 1 + src/app/+not-found.tsx | 19 +- src/app/menu-list/[item].tsx | 21 +- src/app/playground/accordion.tsx | 21 +- src/app/playground/bottom-sheet.tsx | 28 +-- src/app/playground/buttons.tsx | 21 +- src/app/playground/design-system.tsx | 108 ++++----- src/app/playground/icons.tsx | 42 ++-- src/app/playground/image.tsx | 31 +-- src/app/playground/index.tsx | 52 +++-- src/app/playground/inputs.tsx | 21 +- src/app/playground/layout.tsx | 103 ++++----- src/app/playground/progress.tsx | 23 +- src/app/playground/sandbox.tsx | 21 +- src/app/playground/toast.tsx | 21 +- src/assets/web_landing_background.jpg | Bin 0 -> 162032 bytes src/components/common/ErrorBoundary.tsx | 24 +- src/components/common/LoadingScreen.tsx | 22 +- src/components/common/MenuList.tsx | 171 +++++++------- .../common/NavigationThemeProvider.tsx | 9 +- src/components/common/SplashScreen.tsx | 41 ++-- src/components/common/StatusBar.tsx | 12 +- src/components/common/Toaster.tsx | 36 +-- .../common/custom-bottom-bar/BottomBar.tsx | 23 +- .../common/custom-bottom-bar/Tab.tsx | 13 +- src/components/playground/common.tsx | 27 ++- src/components/playground/utils.tsx | 12 +- .../settings/AppearanceMenuTarget.tsx | 61 +++-- src/components/settings/hooks.tsx | 23 +- .../store-review/ImprovementForm.tsx | 80 +++---- src/components/store-review/StoreReview.tsx | 21 +- src/components/uikit/Accordion.tsx | 27 ++- src/components/uikit/BottomSheet.tsx | 21 +- src/components/uikit/Card.tsx | 25 +- src/components/uikit/Icon.tsx | 8 +- src/components/uikit/Image.tsx | 1 + src/components/uikit/PickerModal.tsx | 134 ++++++----- src/components/uikit/PickerSheet.tsx | 118 +++++----- src/components/uikit/ProgressBar.tsx | 40 ++-- src/components/uikit/SegmentedControl.tsx | 86 +++---- src/components/uikit/Text.tsx | 71 ++++-- src/components/uikit/buttons/Button.tsx | 56 +++-- src/components/uikit/buttons/IconButton.tsx | 86 +++---- src/components/uikit/buttons/helpers.ts | 9 +- src/components/uikit/inputs/Checkbox.tsx | 56 ++--- src/components/uikit/inputs/DateInput.tsx | 24 +- src/components/uikit/inputs/InputButton.tsx | 114 ++++----- src/components/uikit/inputs/Radio.tsx | 68 +++--- src/components/uikit/inputs/SearchInput.tsx | 30 ++- src/components/uikit/inputs/TextInput.tsx | 99 ++++---- src/components/uikit/layout/Grid.tsx | 60 +++-- src/components/uikit/layout/Spacer.tsx | 54 +++-- src/components/uikit/layout/Stack.tsx | 84 ++++--- src/design-system/typography.ts | 52 +++-- src/design-system/utils.ts | 17 +- src/services/color-mode.tsx | 56 ----- src/styles/helpers.ts | 104 --------- src/styles/index.ts | 16 -- src/styles/styled.ts | 83 ++++--- src/styles/utils.ts | 58 ++--- src/types/declarations.d.ts | 14 ++ src/utils/navigation.tsx | 5 +- tsconfig.json | 1 + 79 files changed, 1726 insertions(+), 1729 deletions(-) create mode 100644 index.ts delete mode 100644 src/app/(auth)/index.web.tsx create mode 100644 src/assets/web_landing_background.jpg delete mode 100644 src/services/color-mode.tsx delete mode 100644 src/styles/helpers.ts delete mode 100644 src/styles/index.ts create mode 100644 src/types/declarations.d.ts diff --git a/app.config.ts b/app.config.ts index 9e7b6350..11b0298b 100644 --- a/app.config.ts +++ b/app.config.ts @@ -47,9 +47,7 @@ export default ({ config }: ConfigContext): ExpoConfig => { config: { usesNonExemptEncryption: false, }, - infoPlist: { - ITSAppUsesNonExemptEncryption: false, - }, + infoPlist: {}, }, android: { adaptiveIcon: { diff --git a/babel.config.js b/babel.config.js index e6967d8d..0a719a38 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,6 +4,7 @@ module.exports = function (api) { presets: ['babel-preset-expo'], plugins: [ '@lingui/babel-plugin-lingui-macro', + ['react-native-unistyles/plugin', { root: 'src' }], 'react-native-reanimated/plugin', // NOTE: this plugin MUST be last ], }; diff --git a/index.ts b/index.ts new file mode 100644 index 00000000..cd6ff454 --- /dev/null +++ b/index.ts @@ -0,0 +1,2 @@ +import 'expo-router/entry'; +import './src/styles/styled'; diff --git a/package-lock.json b/package-lock.json index 2a4290d2..9c2140bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,17 +44,20 @@ "react-native": "0.79.2", "react-native-collapsible": "1.6.2", "react-native-date-picker": "5.0.12", + "react-native-edge-to-edge": "1.6.2", "react-native-gesture-handler": "~2.24.0", "react-native-ios-context-menu": "3.1.2", "react-native-ios-utilities": "5.1.5", "react-native-keyboard-controller": "1.17.1", "react-native-mmkv": "3.2.0", + "react-native-nitro-modules": "0.26.4", "react-native-permissions": "5.4.0", "react-native-reanimated": "~3.17.5", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.10.0", "react-native-svg": "15.11.2", "react-native-toast-message": "2.3.0", + "react-native-unistyles": "3.0.7", "react-native-web": "~0.20.0", "stitches-native": "0.4.0", "zeego": "3.0.6", @@ -9122,6 +9125,15 @@ "react-native": "*" } }, + "node_modules/expo-status-bar/node_modules/react-native-edge-to-edge": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz", + "integrity": "sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-store-review": { "version": "8.1.5", "resolved": "https://registry.npmjs.org/expo-store-review/-/expo-store-review-8.1.5.tgz", @@ -9223,6 +9235,15 @@ "react-native": "*" } }, + "node_modules/expo/node_modules/react-native-edge-to-edge": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz", + "integrity": "sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", @@ -14865,9 +14886,9 @@ } }, "node_modules/react-native-edge-to-edge": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz", - "integrity": "sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.2.tgz", + "integrity": "sha512-koEF6WRAfdGNelXlP7NtHrQhFGtplwciYNWLHOCTCk0Thc7dlxtqxHRYSA5vPJgWOEkVBFqxcoOqKwAtqOmLNw==", "peerDependencies": { "react": "*", "react-native": "*" @@ -14940,6 +14961,16 @@ "react-native": "*" } }, + "node_modules/react-native-nitro-modules": { + "version": "0.26.4", + "resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.26.4.tgz", + "integrity": "sha512-sCZ0U+FY6JM73HaZYyc4kSRV7JQZXGfbimpYJzaAaZFQMGpJFkD5c3Jt66j1v83wN/m6D/SM9yyx+dN6XTfGAg==", + "hasInstallScript": true, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-permissions": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-5.4.0.tgz", @@ -15024,6 +15055,27 @@ "react-native": "*" } }, + "node_modules/react-native-unistyles": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/react-native-unistyles/-/react-native-unistyles-3.0.7.tgz", + "integrity": "sha512-lgbEgrMSAJOMYhD5TI1EcRfL0EaJpmbJIxOhLP6t6hziCWy4bcrC8tZ7e+KAhcipPGs9UEVfW1aWgVwQXqwykw==", + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@react-native/normalize-colors": "*", + "react": "*", + "react-native": ">=0.76.0", + "react-native-edge-to-edge": "*", + "react-native-nitro-modules": "*", + "react-native-reanimated": "*" + }, + "peerDependenciesMeta": { + "react-native-reanimated": { + "optional": true + } + } + }, "node_modules/react-native-web": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz", diff --git a/package.json b/package.json index 4a65e7d4..0171b3cf 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "taito-react-native-template", "version": "0.0.1", "private": true, - "main": "expo-router/entry", + "main": "index.ts", "scripts": { "start": "expo start", "start:clean": "expo start --clear", @@ -76,17 +76,20 @@ "react-native": "0.79.2", "react-native-collapsible": "1.6.2", "react-native-date-picker": "5.0.12", + "react-native-edge-to-edge": "1.6.2", "react-native-gesture-handler": "~2.24.0", "react-native-ios-context-menu": "3.1.2", "react-native-ios-utilities": "5.1.5", "react-native-keyboard-controller": "1.17.1", "react-native-mmkv": "3.2.0", + "react-native-nitro-modules": "0.26.4", "react-native-permissions": "5.4.0", "react-native-reanimated": "~3.17.5", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.10.0", "react-native-svg": "15.11.2", "react-native-toast-message": "2.3.0", + "react-native-unistyles": "3.0.7", "react-native-web": "~0.20.0", "stitches-native": "0.4.0", "zeego": "3.0.6", diff --git a/src/Providers.tsx b/src/Providers.tsx index 8e148b3f..1fe23ac8 100644 --- a/src/Providers.tsx +++ b/src/Providers.tsx @@ -1,36 +1,36 @@ import type { ReactNode } from 'react'; +import { View } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { KeyboardProvider } from 'react-native-keyboard-controller'; +import { StyleSheet } from 'react-native-unistyles'; import ErrorBoundary from '~components/common/ErrorBoundary'; import NavigationThemeProvider from '~components/common/NavigationThemeProvider'; import Toaster from '~components/common/Toaster'; -import { ColorModeProvider } from '~services/color-mode'; import { I18nProvider } from '~services/i18n'; -import { styled } from '~styles'; export default function Providers({ children }: { children: ReactNode }) { return ( - - - - - - {children} - - - - - - + + + + + {children} + + + + + ); } -const AppWrapper = styled('View', { - flex: 1, - backgroundColor: '$background', -}); +const styles = StyleSheet.create((theme) => ({ + appWrapper: { + flex: 1, + backgroundColor: theme.colors.surface, + }, +})); diff --git a/src/app/(auth)/index.tsx b/src/app/(auth)/index.tsx index 8ea004fa..e93897be 100644 --- a/src/app/(auth)/index.tsx +++ b/src/app/(auth)/index.tsx @@ -1,90 +1,100 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { Link } from 'expo-router'; -import { AccessibilityInfo, useWindowDimensions } from 'react-native'; +import { + AccessibilityInfo, + ImageBackground, + Platform, + TouchableHighlight, + useWindowDimensions, + View, +} from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { StyleSheet } from 'react-native-unistyles'; import * as DropdownMenu from 'zeego/dropdown-menu'; import LandingImage from '~assets/landing_background.jpg'; +import WebLandingImage from '~assets/web_landing_background.jpg'; import StatusBar from '~components/common/StatusBar'; import { IconButton, Stack, Text } from '~components/uikit'; import { useI18n } from '~services/i18n'; -import { styled, useTheme } from '~styles'; export default function Landing() { const { height } = useWindowDimensions(); const insets = useSafeAreaInsets(); - const theme = useTheme(); const { t } = useLingui(); return ( - - - - + + + + - - - - - - Welcome to - - - Taito Template - - - - By Taito United 💚 - - - - + + + + + Welcome to + + + Taito Template + + + By Taito United + + + - + - + ✨ Start your journey ✨ - + - + + - - + + Or - - + + - + + - + - + ); } @@ -130,60 +140,58 @@ function LanguageSelector() { // adhere to the design system 100%. In that case, it's ok to use custom styles // that are out of the design system like here we are using hard coded white color. -const Wrapper = styled('View', { - position: 'relative', - flex: 1, -}); - -const ImageBackground = styled('ImageBackground', { - flex: 1, - justifyContent: 'flex-end', - paddingHorizontal: '$xxs', -}); - -const BlackText = styled(Text, { - color: 'rgba(0, 0, 0, 0.8)', -}); - -const WhiteText = styled(Text, { - color: '#fff', -}); - -const TopSection = styled('View', { - flex: 1, -}); - -const TopSectionHeader = styled('View', { - flexDirection: 'row', - justifyContent: 'flex-end', - paddingHorizontal: '$regular', -}); - -const TopSectionBody = styled('View', { - flex: 1, - flexCenter: 'column', - padding: '$large', -}); - -const BottomSection = styled('View', { - padding: '$regular', - paddingTop: '$large', - backgroundColor: 'rgba(0, 0, 0, 0.65)', - borderRadius: '$large', -}); - -const Button = styled('TouchableHighlight', { - padding: '$medium', - borderRadius: '$full', - backgroundColor: 'rgba(0, 0, 0, 1)', - flexCenter: 'row', - width: '100%', -}).attrs(() => ({ - underlayColor: 'rgba(0, 0, 0, 0.6)', +const styles = StyleSheet.create((theme) => ({ + wrapper: { + flex: 1, + position: 'relative', + }, + imageBackground: { + flex: 1, + justifyContent: 'flex-end', + paddingHorizontal: theme.space.xxs, + }, + imageStyle: { + height: '100%', + }, + blackText: { + color: 'rgba(0, 0, 0, 0.8)', + }, + topSection: (paddingTop: number) => ({ + flex: 1, + paddingTop, + }), + topSectionHeader: { + flexDirection: 'row', + justifyContent: 'flex-end', + paddingHorizontal: theme.space.regular, + }, + topSectionBody: { + flex: 1, + padding: theme.space.large, + }, + bottomSection: (insetTop: number) => ({ + minHeight: Math.max(insetTop, theme.space.regular) * 0.4, + padding: theme.space.regular, + paddingTop: theme.space.large, + backgroundColor: 'rgba(0, 0, 0, 0.65)', + borderRadius: theme.space.large, + width: '100%', + maxWidth: 1000, + alignSelf: 'center', + marginBottom: theme.space.large, + }), + button: { + padding: theme.space.medium, + borderRadius: theme.radii.full, + backgroundColor: 'rgba(0, 0, 0, 1)', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + }, + line: { + height: 1, + width: 72, + backgroundColor: 'rgba(255, 255, 255, 0.15)', + }, })); - -const Line = styled('View', { - height: 1, - width: 72, - backgroundColor: 'rgba(255, 255, 255, 0.15)', -}); diff --git a/src/app/(auth)/index.web.tsx b/src/app/(auth)/index.web.tsx deleted file mode 100644 index 6f563c99..00000000 --- a/src/app/(auth)/index.web.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { Trans, useLingui } from '@lingui/react/macro'; -import { Link } from 'expo-router'; -import { useWindowDimensions } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import * as DropdownMenu from 'zeego/dropdown-menu'; - -import LandingImage from '~assets/landing_background.jpg'; -import StatusBar from '~components/common/StatusBar'; -import { IconButton, Stack, Text } from '~components/uikit'; -import { useI18n } from '~services/i18n'; -import { styled, useTheme } from '~styles'; - -export default function Landing() { - const { height } = useWindowDimensions(); - const insets = useSafeAreaInsets(); - const theme = useTheme(); - - return ( - - - - - - - - - - - Welcome to - - - Taito Template - - - - By Taito United 💚 - - - - - - - - - ✨Start your journey ✨ - - - - - - - - Or - - - - - - - - - - - - - ); -} - -function LanguageSelector() { - const { changeLocale } = useI18n(); - const { t } = useLingui(); - - return ( - - - - - - changeLocale('fi')}> - {t`Finnish`} - - changeLocale('en')}> - {t`English`} - - - - ); -} - -// NOTE: it's often the case that the landing screen is very custom and doesn't -// adhere to the design system 100%. In that case, it's ok to use custom styles -// that are out of the design system like here we are using hard coded white color. - -const Wrapper = styled('View', { - position: 'relative', - flex: 1, -}); - -const ImageBackground = styled('ImageBackground', { - height: '100%', - width: '100%', - justifyContent: 'flex-end', - paddingHorizontal: '$xxs', -}); - -const BlackText = styled(Text, { - color: 'rgba(0, 0, 0, 0.8)', -}); - -const WhiteText = styled(Text, { - color: '#fff', -}); - -const TopSection = styled('View', { - flex: 1, -}); - -const TopSectionHeader = styled('View', { - flexDirection: 'row', - justifyContent: 'flex-end', - paddingHorizontal: '$regular', -}); - -const TopSectionBody = styled('View', { - flex: 1, - flexCenter: 'column', - padding: '$large', -}); - -const BottomSection = styled('View', { - padding: '$regular', - paddingTop: '$large', - backgroundColor: 'rgba(0, 0, 0, 0.65)', - borderRadius: '$large', -}); - -const Button = styled('TouchableHighlight', { - padding: '$medium', - borderRadius: '$full', - backgroundColor: 'rgba(0, 0, 0, 1)', - flexCenter: 'row', - flexGrow: 1, - width: '50%', -}).attrs(() => ({ - underlayColor: 'rgba(0, 0, 0, 0.6)', -})); - -const Line = styled('View', { - height: 1, - width: 72, - backgroundColor: 'rgba(255, 255, 255, 0.15)', -}); diff --git a/src/app/(auth)/login.tsx b/src/app/(auth)/login.tsx index 0d0e3b8e..fedafb26 100644 --- a/src/app/(auth)/login.tsx +++ b/src/app/(auth)/login.tsx @@ -1,11 +1,11 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { Controller, useForm } from 'react-hook-form'; import { KeyboardAvoidingView } from 'react-native-keyboard-controller'; +import { StyleSheet } from 'react-native-unistyles'; import { showToast } from '~components/common/Toaster'; import { Button, Stack, Text, TextInput } from '~components/uikit'; import { useAuthStore } from '~services/auth'; -import { styled } from '~styles'; import { announceForAccessibility } from '~utils/a11y'; import { haptics } from '~utils/haptics'; @@ -33,8 +33,17 @@ export default function Login() { } return ( - - + + @@ -119,21 +128,17 @@ export default function Login() { > Login - - + + ); } -const InnerStack = styled(Stack, { - padding: '$medium', - flex: 1, -}); - -const KeyboardAwareView = styled(KeyboardAvoidingView, { - flex: 1, -}).attrs(() => ({ - keyboardShouldPersistTaps: 'handled', - contentContainerStyle: { - flexGrow: 1, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + innerStack: { + padding: theme.space.medium, + flex: 1, }, })); diff --git a/src/app/(auth)/signup.tsx b/src/app/(auth)/signup.tsx index 3397a206..0df7a2c9 100644 --- a/src/app/(auth)/signup.tsx +++ b/src/app/(auth)/signup.tsx @@ -1,11 +1,11 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { Controller, useForm } from 'react-hook-form'; import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; +import { StyleSheet } from 'react-native-unistyles'; import { showToast } from '~components/common/Toaster'; import { Button, Stack, Text, TextInput } from '~components/uikit'; import { useAuthStore } from '~services/auth'; -import { styled } from '~styles/styled'; import { announceForAccessibility } from '~utils/a11y'; import { haptics } from '~utils/haptics'; @@ -49,8 +49,13 @@ export default function Signup() { } return ( - - + + @@ -238,21 +243,17 @@ export default function Signup() { > Signup - - + + ); } -const InnerStack = styled(Stack, { - padding: '$medium', - flex: 1, -}); - -const KeyboardAwareView = styled(KeyboardAwareScrollView, { - flex: 1, -}).attrs(() => ({ - keyboardShouldPersistTaps: 'handled', - contentContainerStyle: { - flexGrow: 1, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + innerStack: { + padding: theme.space.medium, + flex: 1, }, })); diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index 614168d3..7790e56e 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -6,12 +6,12 @@ import { import { Tabs } from 'expo-router'; import { Pressable, StyleSheet } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useUnistyles } from 'react-native-unistyles'; import { BottomBar } from '~components/common/custom-bottom-bar/BottomBar'; import StoreReview from '~components/store-review/StoreReview'; import { Icon, Stack, Text } from '~components/uikit'; import type { IconName } from '~components/uikit/Icon'; -import { useTheme } from '~styles'; export type TabList = { id: string; @@ -47,7 +47,6 @@ const USE_STORE_REVIEW = true; export default function TabsLayout() { const { t } = useLingui(); - const theme = useTheme(); const tabs: TabList = [ { id: 'home', @@ -80,9 +79,9 @@ export default function TabsLayout() { return ( <> {USE_CUSTOM_TABS ? ( - + ) : ( - + )} {USE_STORE_REVIEW && } @@ -91,12 +90,12 @@ export default function TabsLayout() { type BottomBarProps = { tabs: TabList; - theme: ReturnType; }; -function DefaultBottomBar({ tabs, theme }: BottomBarProps) { +function DefaultBottomBar({ tabs }: BottomBarProps) { const { t } = useLingui(); const insets = useSafeAreaInsets(); + const { theme } = useUnistyles(); function renderTabIcon({ focused, @@ -192,7 +191,9 @@ function renderBottomBar(props: BottomTabBarProps & { tabs: TabList }) { return ; } -function CustomBottomBar({ tabs, theme }: BottomBarProps) { +function CustomBottomBar({ tabs }: BottomBarProps) { + const { theme } = useUnistyles(); + return ( renderBottomBar({ ...props, tabs })} diff --git a/src/app/(tabs)/_layout.web.tsx b/src/app/(tabs)/_layout.web.tsx index dbdf64ae..773fa754 100644 --- a/src/app/(tabs)/_layout.web.tsx +++ b/src/app/(tabs)/_layout.web.tsx @@ -1,16 +1,15 @@ import { useLingui } from '@lingui/react/macro'; import { Drawer } from 'expo-router/drawer'; import { StyleSheet } from 'react-native'; +import { useUnistyles } from 'react-native-unistyles'; import { Icon, type IconName } from '~components/uikit/Icon'; -import { useTheme } from '~styles'; import { type TabList as DrawerList } from './_layout'; export default function DrawerLayout() { const { t } = useLingui(); - - const theme = useTheme(); + const { theme } = useUnistyles(); // Note: the items are intentionally 'duplicated' from the tabs list as we assume they will differ from each other in a real project. const drawerItems: DrawerList = [ diff --git a/src/app/(tabs)/home.tsx b/src/app/(tabs)/home.tsx index 7a9e9aa5..9cf0a05d 100644 --- a/src/app/(tabs)/home.tsx +++ b/src/app/(tabs)/home.tsx @@ -1,22 +1,31 @@ import { Trans } from '@lingui/react/macro'; +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; +import { useHeaderPlaygroundButton } from '~components/playground/utils'; import { Text } from '~components/uikit'; -import { styled } from '~styles'; export default function Home() { + useHeaderPlaygroundButton(); + return ( - + Home - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/(tabs)/profile.tsx b/src/app/(tabs)/profile.tsx index 10a8013c..10bb1815 100644 --- a/src/app/(tabs)/profile.tsx +++ b/src/app/(tabs)/profile.tsx @@ -1,22 +1,28 @@ import { Trans } from '@lingui/react/macro'; +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Text } from '~components/uikit'; -import { styled } from '~styles'; export default function Profile() { return ( - + Profile - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/(tabs)/search.tsx b/src/app/(tabs)/search.tsx index a5417972..8fb44265 100644 --- a/src/app/(tabs)/search.tsx +++ b/src/app/(tabs)/search.tsx @@ -1,22 +1,28 @@ import { Trans } from '@lingui/react/macro'; +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Text } from '~components/uikit'; -import { styled } from '~styles'; export default function Search() { return ( - + Search - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/(tabs)/settings.tsx b/src/app/(tabs)/settings.tsx index 7662c9a6..b925768b 100644 --- a/src/app/(tabs)/settings.tsx +++ b/src/app/(tabs)/settings.tsx @@ -1,11 +1,12 @@ import { useLingui } from '@lingui/react/macro'; +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import MenuList from '~components/common/MenuList'; import { useHeaderPlaygroundButton } from '~components/playground/utils'; import { useMenuListItem } from '~components/settings/hooks'; import { Icon, alert } from '~components/uikit'; import { useAuthStore } from '~services/auth'; -import { styled } from '~styles'; import { announceForAccessibility } from '~utils/a11y'; import { haptics } from '~utils/haptics'; @@ -45,16 +46,21 @@ export default function Settings() { ]; return ( - + - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/+html.tsx b/src/app/+html.tsx index e76c7a9d..c622e116 100644 --- a/src/app/+html.tsx +++ b/src/app/+html.tsx @@ -1,4 +1,5 @@ import { ScrollViewStyleReset } from 'expo-router/html'; +import '../styles/styled'; // This file is web-only and used to configure the root HTML for every // web page during static rendering. diff --git a/src/app/+not-found.tsx b/src/app/+not-found.tsx index 9376ef77..0214ea44 100644 --- a/src/app/+not-found.tsx +++ b/src/app/+not-found.tsx @@ -1,9 +1,10 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { Stack as ExpoStack, Link } from 'expo-router'; +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Button, Stack, Text } from '~components/uikit'; import { useAuthStore } from '~services/auth'; -import { styled } from '~styles'; export default function NotFoundScreen() { const { t } = useLingui(); @@ -12,7 +13,10 @@ export default function NotFoundScreen() { return ( <> - + This screen does not exist @@ -23,15 +27,14 @@ export default function NotFoundScreen() { - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs(() => ({ - contentContainerStyle: { +const styles = StyleSheet.create({ + container: { + flex: 1, margin: 'auto', }, -})); +}); diff --git a/src/app/menu-list/[item].tsx b/src/app/menu-list/[item].tsx index 9df8564e..70f3e62e 100644 --- a/src/app/menu-list/[item].tsx +++ b/src/app/menu-list/[item].tsx @@ -1,8 +1,9 @@ import { Stack, router, useLocalSearchParams } from 'expo-router'; import { useEffect } from 'react'; +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { useMenuListItem } from '~components/settings/hooks'; -import { styled } from '~styles'; export default function MenuListItem() { const { item } = useLocalSearchParams<{ item: string }>(); @@ -18,17 +19,21 @@ export default function MenuListItem() { if (!Target) return null; return ( - + - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/playground/accordion.tsx b/src/app/playground/accordion.tsx index 86be1220..08dada00 100644 --- a/src/app/playground/accordion.tsx +++ b/src/app/playground/accordion.tsx @@ -1,9 +1,13 @@ +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Accordion, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; export default function Accordions() { return ( - + Accordion @@ -24,7 +28,7 @@ export default function Accordions() { - + ); } @@ -36,10 +40,11 @@ function AccordionContent() { ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/playground/bottom-sheet.tsx b/src/app/playground/bottom-sheet.tsx index 407081f9..83d06736 100644 --- a/src/app/playground/bottom-sheet.tsx +++ b/src/app/playground/bottom-sheet.tsx @@ -1,9 +1,10 @@ import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import { BottomSheetDefaultBackdropProps } from '@gorhom/bottom-sheet/lib/typescript/components/bottomSheetBackdrop/types'; import { useCallback, useMemo, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Button, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; /** * NOTE: This example implementation does not use the UI Kit Bottom Sheet because of the ref we are using to control the sheet @@ -52,7 +53,7 @@ export default function BottomSheets() { ); return ( - + @@ -72,7 +73,7 @@ export default function BottomSheets() { onAnimate={handleSheetAnimate} backdropComponent={renderBackdrop} > - + Awesome Bottom Sheet @@ -82,18 +83,17 @@ export default function BottomSheets() { > Close - + - + ); } -const Wrapper = styled('View', { - flex: 1, - padding: '$regular', -}); - -const ContentContainer = styled(Stack, { - padding: '$large', - zIndex: 1, -}); +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, + }, +})); diff --git a/src/app/playground/buttons.tsx b/src/app/playground/buttons.tsx index 5198f0a7..a5923776 100644 --- a/src/app/playground/buttons.tsx +++ b/src/app/playground/buttons.tsx @@ -1,3 +1,5 @@ +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Button, Card, IconButton, Stack, Text } from '~components/uikit'; import type { ButtonColor, @@ -5,18 +7,20 @@ import type { ButtonVariant, IconButtonProps, } from '~components/uikit/buttons/types'; -import { styled } from '~styles'; export default function Buttons() { return ( - + - + ); } @@ -159,10 +163,11 @@ function IconButtonExamples({ ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/playground/design-system.tsx b/src/app/playground/design-system.tsx index 9ff0e4cb..fde878e6 100644 --- a/src/app/playground/design-system.tsx +++ b/src/app/playground/design-system.tsx @@ -1,4 +1,6 @@ import startCase from 'lodash/startCase'; +import { ScrollView, View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Note } from '~components/playground/common'; import { Grid, Stack, Text } from '~components/uikit'; @@ -6,7 +8,7 @@ import * as colors from '~design-system/colors'; import * as radii from '~design-system/radii'; import spacing from '~design-system/spacing.json'; import * as typography from '~design-system/typography'; -import { styled, themeProp } from '~styles'; +import { flexCenter } from '~styles/utils'; const typographyNames = Object.keys(typography).sort(); const radiiEntries = Object.entries(radii).sort((a, b) => a[1] - b[1]); @@ -14,7 +16,10 @@ const spacingEntries = Object.entries(spacing).sort((a, b) => a[1] - b[1]); export default function DesignSystem() { return ( - + - + {startCase(colorName)} @@ -73,9 +78,9 @@ export default function DesignSystem() { {/* Accessibility note: Unless we have a description attached to the typography variant coming from Figma, we cannot make this very accessible */} {typographyNames.map((name) => ( - + {startCase(name)} - + ))} @@ -100,11 +105,17 @@ export default function DesignSystem() { accessible accessibilityLabel={`Radii token: ${name}, radii value: ${value} pixels`} > - + {value}px - + {startCase(name)} ))} @@ -129,7 +140,10 @@ export default function DesignSystem() { {startCase(name)} - + {value}px @@ -144,51 +158,43 @@ export default function DesignSystem() { - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, }, -})); - -const ColorBlock = styled('View', { - height: 80, - width: '100%', - borderRadius: '$medium', - borderWidth: 1, - borderColor: 'rgba(150, 150, 150, 0.15)', - variants: { - ...themeProp('bg', 'colors', (color) => ({ - backgroundColor: color, - })), + contentContainer: { + padding: theme.space.regular, }, -}); - -const TypographyBlock = styled('View', { - padding: '$regular', - borderRadius: '$medium', - borderWidth: 1, - borderColor: 'rgba(150, 150, 150, 0.15)', -}); - -const RadiiBlock = styled('View', { - height: 100, - width: 100, - borderWidth: 1, - borderColor: '$border', - backgroundColor: '$neutral5', - flexCenter: 'row', -}); - -const SpacingBlock = styled('View', { - height: 24, - borderWidth: 1, - borderRadius: 2, - borderColor: '$border', - backgroundColor: '$neutral5', -}); + colorBlock: (color: colors.ColorsToken) => ({ + height: 80, + width: '100%', + borderRadius: theme.radii.medium, + borderWidth: 1, + borderColor: 'rgba(150, 150, 150, 0.15)', + backgroundColor: theme.colors[color], + }), + typographyBlock: { + padding: theme.space.regular, + borderRadius: theme.radii.medium, + borderWidth: 1, + borderColor: 'rgba(150, 150, 150, 0.15)', + }, + radiiBlock: { + height: 100, + width: 100, + borderWidth: 1, + borderColor: theme.colors.neutral3, + backgroundColor: theme.colors.neutral5, + }, + spacingBlock: { + height: 24, + borderWidth: 1, + borderRadius: 2, + borderColor: theme.colors.neutral3, + backgroundColor: theme.colors.neutral5, + }, +})); diff --git a/src/app/playground/icons.tsx b/src/app/playground/icons.tsx index 0e53d348..863b3cfe 100644 --- a/src/app/playground/icons.tsx +++ b/src/app/playground/icons.tsx @@ -1,17 +1,20 @@ import { setStringAsync } from 'expo-clipboard'; -import { Pressable } from 'react-native'; +import { Pressable, ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { showToast } from '~components/common/Toaster'; import { Note } from '~components/playground/common'; import { Grid, Icon, Stack, Text } from '~components/uikit'; import type { IconName } from '~components/uikit/Icon'; import * as icons from '~design-system/icons'; -import { styled } from '~styles'; import { haptics } from '~utils/haptics'; export default function Icons() { return ( - + You can long press on an icon to copy its name to the clipboard. @@ -31,7 +34,8 @@ export default function Icons() { }); }} > - {name} - + ))} - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, + }, + iconWrapper: { + padding: theme.space.xs, + borderRadius: theme.radii.small, + backgroundColor: theme.colors.surface, + width: 80, + height: 80, }, })); - -const IconWrapper = styled(Stack, { - padding: '$xs', - borderRadius: '$small', - backgroundColor: '$surface', - width: 80, - height: 80, -}); diff --git a/src/app/playground/image.tsx b/src/app/playground/image.tsx index 2b27d36e..1b2a72f9 100644 --- a/src/app/playground/image.tsx +++ b/src/app/playground/image.tsx @@ -1,5 +1,6 @@ +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Grid, Stack, Text, Image as UiImage } from '~components/uikit'; -import { styled } from '~styles'; const photos = [ 'https://tinyurl.com/57ssptjn', @@ -9,14 +10,18 @@ const photos = [ export default function Image() { return ( - + Images {photos.map((photo, index) => ( - - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, + }, + img: { + borderRadius: theme.radii.regular, }, })); - -const Img = styled(UiImage, { - borderRadius: '$regular', -}); diff --git a/src/app/playground/index.tsx b/src/app/playground/index.tsx index f8c9dc90..48f4faea 100644 --- a/src/app/playground/index.tsx +++ b/src/app/playground/index.tsx @@ -1,15 +1,17 @@ -import { Stack, router } from 'expo-router'; +import { Stack as ExpoStack, router } from 'expo-router'; +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import MenuList, { Item } from '~components/common/MenuList'; -import { IconButton, Text } from '~components/uikit'; -import { styled } from '~styles'; +import { IconButton, Stack, Text } from '~components/uikit'; +import { flexCenter } from '~styles/utils'; export default function PlaygroundPage() { const items: Item[] = [ { id: 'design-system', target: '/playground/design-system', label: 'Design System' }, // prettier-ignore { id: 'icons', target: '/playground/icons', label: 'Icons' }, - { id: 'buttons', target: '/playground/buttons', label: 'Buttons' }, { id: 'inputs', target: '/playground/inputs', label: 'Inputs' }, + { id: 'buttons', target: '/playground/buttons', label: 'Buttons' }, { id: 'bottom', target: '/playground/bottom-sheet', label: 'Bottom Sheet' }, { id: 'layout', target: '/playground/layout', label: 'Layout' }, { id: 'accordion', target: '/playground/accordion', label: 'Accordion' }, @@ -21,8 +23,11 @@ export default function PlaygroundPage() { if (__DEV__) items.push({ id: 'sandbox', target: '/playground/sandbox', label: 'Sandbox' }); // prettier-ignore return ( - - + ( @@ -40,30 +45,33 @@ export default function PlaygroundPage() { label: item.label, target: item.target, leftSlot: ( - + {item.label.slice(0, 2)} - + ), }))} /> - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, + }, + menuListItemLeftSlot: { + width: 40, + height: 40, + borderRadius: theme.radii.regular, + backgroundColor: theme.colors.infoMuted, }, })); - -const MenuListItemLeftSlot = styled('View', { - width: 40, - height: 40, - flexCenter: 'row', - borderRadius: '$regular', - backgroundColor: '$infoMuted', -}); diff --git a/src/app/playground/inputs.tsx b/src/app/playground/inputs.tsx index 6dcf3327..97664521 100644 --- a/src/app/playground/inputs.tsx +++ b/src/app/playground/inputs.tsx @@ -1,4 +1,6 @@ import { useState } from 'react'; +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Checkbox, @@ -11,7 +13,6 @@ import { Text, TextInput, } from '~components/uikit'; -import { styled } from '~styles'; export default function Inputs() { const [selectedMultiple, setSelectedMultiple] = useState([]); @@ -23,7 +24,10 @@ export default function Inputs() { const [date, setDate] = useState(new Date()); return ( - + - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/playground/layout.tsx b/src/app/playground/layout.tsx index 2edbbba5..438fe993 100644 --- a/src/app/playground/layout.tsx +++ b/src/app/playground/layout.tsx @@ -1,12 +1,14 @@ -import { StyleSheet } from 'react-native'; - +import { ScrollView, View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Note } from '~components/playground/common'; import { Grid, Spacer, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; export default function Layout() { return ( - + If you have long lists do not use these layout components but instead @@ -22,33 +24,33 @@ export default function Layout() { while applying uniform spacing between the elements. - + {`...`} - - - + + + - + - + {`...`} - - - + + + - + @@ -60,26 +62,26 @@ export default function Layout() { elements. - + {` - + - - + + `.trim()} - + - - + + - + @@ -89,7 +91,7 @@ export default function Layout() { A Grid component can be used for grid-like layouts. - + {`...`} @@ -97,11 +99,11 @@ export default function Layout() { {Array.from({ length: 15 }).map((_, i) => ( - + ))} - + A number of columns can be provided to force the grid structure. By @@ -109,7 +111,7 @@ export default function Layout() { instrictic size with the given spacing. - + {`...`} @@ -117,38 +119,37 @@ export default function Layout() { {Array.from({ length: 15 }).map((_, i) => ( - + ))} - + - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, + }, + exampleBlock: { + padding: theme.space.small, + borderWidth: 1, + borderColor: theme.colors.neutral3, + borderRadius: theme.radii.small, + backgroundColor: theme.colors.neutral5, + }, + box: { + height: 60, + width: 60, + borderWidth: StyleSheet.hairlineWidth, + borderColor: theme.colors.info, + borderRadius: theme.radii.regular, + backgroundColor: theme.colors.infoMuted, }, })); - -const ExampleBlock = styled('View', { - padding: '$small', - borderWidth: 1, - borderColor: '$border', - borderRadius: '$small', - backgroundColor: '$neutral5', -}); - -const Box = styled('View', { - height: 60, - width: 60, - borderWidth: StyleSheet.hairlineWidth, - borderColor: '$info', - borderRadius: '$regular', - backgroundColor: '$infoMuted', -}); diff --git a/src/app/playground/progress.tsx b/src/app/playground/progress.tsx index bec9c162..81b39f72 100644 --- a/src/app/playground/progress.tsx +++ b/src/app/playground/progress.tsx @@ -1,30 +1,35 @@ +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { ProgressBar, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; const totalSteps = 5; export default function Progress() { return ( - + Progress bar {Array.from({ length: totalSteps + 1 }).map((_, i) => ( - + ))} - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/playground/sandbox.tsx b/src/app/playground/sandbox.tsx index 759a6c0f..9b31c243 100644 --- a/src/app/playground/sandbox.tsx +++ b/src/app/playground/sandbox.tsx @@ -1,23 +1,28 @@ +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; export default function Sandbox() { return ( - + You can play around with various components here if you don‘t want to add a new screen for them in the playground. - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/app/playground/toast.tsx b/src/app/playground/toast.tsx index c53a57d3..67e7f5ba 100644 --- a/src/app/playground/toast.tsx +++ b/src/app/playground/toast.tsx @@ -1,10 +1,14 @@ +import { ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { showToast } from '~components/common/Toaster'; import { Button, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; export default function Toast() { return ( - + Info toast @@ -183,14 +187,15 @@ export default function Toast() { - + ); } -const Wrapper = styled('ScrollView', { - flex: 1, -}).attrs((p) => ({ - contentContainerStyle: { - padding: p.theme.space.regular, +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentContainer: { + padding: theme.space.regular, }, })); diff --git a/src/assets/web_landing_background.jpg b/src/assets/web_landing_background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b1a94d47617b040b9319c75a8494cfd65f55a95f GIT binary patch literal 162032 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 c1&^Mxjj9 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 H|IPjniI7cc literal 0 HcmV?d00001 diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx index 2bbc937b..fb3bf924 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -1,8 +1,9 @@ import { Trans } from '@lingui/react/macro'; import { Component, type ReactNode } from 'react'; +import { SafeAreaView, ScrollView } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; interface Props { children: ReactNode; @@ -32,8 +33,8 @@ export default class ErrorBoundary extends Component { function ErrorView() { return ( - - + + Something went wrong @@ -42,19 +43,16 @@ function ErrorView() { Please try restarting the application. - - + + ); } -const SafeArea = styled('SafeAreaView', { - flex: 1, - backgroundColor: '$background', -}); - -const Scroller = styled('ScrollView', { - flex: 1, -}).attrs(() => ({ +const styles = StyleSheet.create((theme) => ({ + safeArea: { + flex: 1, + backgroundColor: theme.colors.surface, + }, contentContainerStyle: { flex: 1, alignItems: 'center', diff --git a/src/components/common/LoadingScreen.tsx b/src/components/common/LoadingScreen.tsx index f7f190a0..ce3c2550 100644 --- a/src/components/common/LoadingScreen.tsx +++ b/src/components/common/LoadingScreen.tsx @@ -1,19 +1,21 @@ -import { ActivityIndicator } from 'react-native'; +import { ActivityIndicator, View } from 'react-native'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -import { styled, useTheme } from '~styles'; +import { flexCenter } from '~styles/utils'; export default function LoadingScreen() { - const theme = useTheme(); + const { theme } = useUnistyles(); return ( - + - + ); } -const Wrapper = styled('View', { - flex: 1, - flexCenter: 'column', - backgroundColor: '$background', -}); +const styles = StyleSheet.create((theme) => ({ + wrapper: { + flex: 1, + backgroundColor: theme.colors.surface, + }, +})); diff --git a/src/components/common/MenuList.tsx b/src/components/common/MenuList.tsx index 8da66e5a..67b24bb3 100644 --- a/src/components/common/MenuList.tsx +++ b/src/components/common/MenuList.tsx @@ -1,10 +1,11 @@ import { useLingui } from '@lingui/react/macro'; import { router, type Href } from 'expo-router'; import { isValidElement, type FunctionComponent, type ReactNode } from 'react'; -import { Platform, StyleSheet } from 'react-native'; +import { Platform, TouchableHighlight, View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Icon, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; +import { flexCenter } from '~styles/utils'; import { haptics } from '~utils/haptics'; export type Item = { @@ -45,18 +46,21 @@ export default function MenuList({ items, title }: Props) { return ( {!!title && ( - + <Text style={styles.title} variant="overlineSmall" color="textMuted"> {title} - + )} - - {filteredItems.map((item, index) => { + + {filteredItems.map((item) => { + styles.useVariants({ withDivider: filteredItems.length > 1 }); + const isPressable = !!item.onPress || !!item.target; return ( - handleItemPress(item) : undefined} accessibilityRole={isPressable ? 'button' : 'text'} accessibilityLabel={`${t`Item`} ${item.label}${item.currentValue ? `, ${t`Selected value`}: ${item.currentValue}` : ''}`} @@ -64,44 +68,51 @@ export default function MenuList({ items, title }: Props) { isPressable ? t`Double tap to select ${item.label}` : '' } > - - {item.leftSlot ? {item.leftSlot} : null} - - + {item.leftSlot ? ( + + {item.leftSlot} + + ) : null} + + - + {item.rightSlot ? ( - {item.rightSlot} + + {item.rightSlot} + ) : ( <> {item.currentValue !== undefined && (isValidElement(item.currentValue) ? ( item.currentValue ) : ( - {item.currentValue} - + ))} {item.checked !== undefined && ( <> {item.checked ? ( - + - + ) : ( - + )} )} @@ -111,79 +122,65 @@ export default function MenuList({ items, title }: Props) { )} )} - - - + + + ); })} - + ); } -const Wrapper = styled('View', { - backgroundColor: '$surface', - borderRadius: '$medium', - overflow: 'hidden', -}); - -const Title = styled(Text, { - marginLeft: '$small', -}); - -const Label = styled(Text, { - flex: 1, - paddingVertical: '$xxs', -}); - -const Value = styled(Text, { - maxWidth: '75%', -}); - -const Pressable = styled('TouchableHighlight', {}).attrs(() => ({ - underlayColor: 'rgba(150, 150, 150, 0.2)', // TODO: Design system template do not have the pressed color for now. Might be added in the future. -})); - -const ContentWrapper = styled(Stack, { - paddingLeft: '$regular', -}); - -const Content = styled(Stack, { - flex: 1, - paddingRight: '$small', - paddingVertical: '$small', - variants: { - withDivider: { - true: { - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '$line3', +const styles = StyleSheet.create((theme) => ({ + wrapper: { + backgroundColor: theme.colors.surface, + borderRadius: theme.radii.medium, + overflow: 'hidden', + }, + title: { + marginLeft: theme.space.small, + }, + label: { + flex: 1, + paddingVertical: theme.space.xxs, + }, + value: { + maxWidth: '75%', + }, + contentWrapper: { + paddingLeft: theme.space.regular, + }, + content: { + flex: 1, + paddingRight: theme.space.small, + paddingVertical: theme.space.small, + variants: { + withDivider: { + true: { + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: theme.colors.line3, + }, }, }, }, -}); - -const LeftSlot = styled('View', { - flexCenter: 'row', - paddingVertical: '$small', -}); - -const RightSlot = styled('View', { - flexCenter: 'row', - minHeight: 24, -}); - -const CheckCircle = styled('View', { - width: 24, - height: 24, - borderRadius: '$full', - backgroundColor: '$info', - flexCenter: 'row', -}); - -const CheckOutline = styled('View', { - width: 24, - height: 24, - borderRadius: '$full', - borderWidth: 1, - borderColor: '$muted4', -}); + leftSlot: { + paddingVertical: theme.space.small, + }, + rightSlot: { + minHeight: 24, + }, + checkCircle: { + width: 24, + height: 24, + borderRadius: theme.radii.full, + backgroundColor: theme.colors.info, + }, + checkOutline: { + width: 24, + height: 24, + borderRadius: theme.radii.full, + borderWidth: 1, + borderColor: theme.colors.neutral4, + }, +})); diff --git a/src/components/common/NavigationThemeProvider.tsx b/src/components/common/NavigationThemeProvider.tsx index 5261b773..12ed1f96 100644 --- a/src/components/common/NavigationThemeProvider.tsx +++ b/src/components/common/NavigationThemeProvider.tsx @@ -4,22 +4,19 @@ import { ThemeProvider, } from '@react-navigation/native'; import { type ReactNode } from 'react'; - -import { useColorMode } from '~services/color-mode'; -import { useTheme } from '~styles'; +import { UnistylesRuntime, useUnistyles } from 'react-native-unistyles'; export default function NavigationThemeProvider({ children, }: { children: ReactNode; }) { - const theme = useTheme(); - const { colorScheme } = useColorMode(); + const { theme } = useUnistyles(); return ( - + - - - + + + ); } -const Wrapper = styled('View', { - flex: 1, -}); - -const SplashContent = styled('View', { - absoluteFill: true, -}); - -const SplashImage = styled('Image', { - width: '100%', - height: '100%', - resizeMode: 'contain', +const styles = StyleSheet.create({ + wrapper: { + flex: 1, + }, + splashImage: { + width: '100%', + height: '100%', + resizeMode: 'contain', + }, }); diff --git a/src/components/common/StatusBar.tsx b/src/components/common/StatusBar.tsx index 980bde62..7f1aa471 100644 --- a/src/components/common/StatusBar.tsx +++ b/src/components/common/StatusBar.tsx @@ -1,9 +1,11 @@ import { StatusBar as RNStatusBar } from 'expo-status-bar'; - -import { useColorMode } from '~services/color-mode'; +import { UnistylesRuntime } from 'react-native-unistyles'; export default function StatusBar({ transparent = false }) { - const { colorMode } = useColorMode(); - - return ; + return ( + + ); } diff --git a/src/components/common/Toaster.tsx b/src/components/common/Toaster.tsx index 5c8fbaa2..404647e9 100644 --- a/src/components/common/Toaster.tsx +++ b/src/components/common/Toaster.tsx @@ -1,11 +1,13 @@ +import { View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import ToastContainer, { type ToastConfigParams, } from 'react-native-toast-message'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; import { Icon, IconButton, Stack, Text } from '~components/uikit'; import { type IconName } from '~components/uikit/Icon'; -import { styled, useTheme, type Color } from '~styles/styled'; +import { type Color } from '~styles/styled'; import { announceForAccessibility } from '~utils/a11y'; import { haptics } from '~utils/haptics'; @@ -51,7 +53,7 @@ const toastConfig = { }; export default function Toaster() { - const theme = useTheme(); + const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); const topOffset = insets.top + theme.space.small; @@ -116,8 +118,10 @@ function Toast({ ToastContainer.hide(); } + styles.useVariants({ hasIcon }); + return ( - + {hasIcon && } @@ -134,7 +138,7 @@ function Toast({ - + ); } @@ -152,16 +156,18 @@ const variantToIcon: { [variant in Variant]?: IconName } = { success: 'checkCircle', }; -const ToastWrapper = styled('View', { - borderRadius: '$full', - paddingVertical: '$regular', - paddingHorizontal: '$medium', - backgroundColor: '$surface', - shadow: 'large', - variants: { - hasIcon: { - true: { paddingLeft: '$regular' }, - false: { paddingLeft: '$large' }, +const styles = StyleSheet.create((theme) => ({ + toastWrapper: { + borderRadius: theme.radii.full, + paddingVertical: theme.space.regular, + paddingHorizontal: theme.space.medium, + backgroundColor: theme.colors.surface, + ...theme.shadows.large, + variants: { + hasIcon: { + true: { paddingLeft: theme.space.regular }, + false: { paddingLeft: theme.space.large }, + }, }, }, -}); +})); diff --git a/src/components/common/custom-bottom-bar/BottomBar.tsx b/src/components/common/custom-bottom-bar/BottomBar.tsx index 73731b3c..6fea2f27 100644 --- a/src/components/common/custom-bottom-bar/BottomBar.tsx +++ b/src/components/common/custom-bottom-bar/BottomBar.tsx @@ -1,8 +1,9 @@ import { type BottomTabBarProps } from '@react-navigation/bottom-tabs'; +import { View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { StyleSheet } from 'react-native-unistyles'; import { type TabList } from '~app/(tabs)/_layout'; -import { styled } from '~styles'; import { haptics } from '~utils/haptics'; import { TabBarButton } from './Tab'; @@ -24,7 +25,7 @@ export function BottomBar({ }: CustomTabBarProps) { const { bottom } = useSafeAreaInsets(); return ( - + {state.routes.map((route, index) => { if (EXCLUDED_ROUTES.includes(route.name)) return null; @@ -61,14 +62,16 @@ export function BottomBar({ /> ); })} - + ); } -const TabBarContainer = styled('View', { - display: 'flex', - flexDirection: 'row', - backgroundColor: '$surface', - paddingVertical: '$xs', - shadow: 'small', -}); +const styles = StyleSheet.create((theme) => ({ + container: { + display: 'flex', + flexDirection: 'row', + backgroundColor: theme.colors.surface, + paddingVertical: theme.space.xs, + ...theme.shadows.small, + }, +})); diff --git a/src/components/common/custom-bottom-bar/Tab.tsx b/src/components/common/custom-bottom-bar/Tab.tsx index 42ba2358..afe802c4 100644 --- a/src/components/common/custom-bottom-bar/Tab.tsx +++ b/src/components/common/custom-bottom-bar/Tab.tsx @@ -8,10 +8,10 @@ import Animated, { withSpring, withTiming, } from 'react-native-reanimated'; +import { StyleSheet } from 'react-native-unistyles'; import { type TabList } from '~app/(tabs)/_layout'; import { Icon, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; const ANIMATION_DURATION = 350; @@ -54,9 +54,10 @@ export function TabBarButton({ })); return ( - (iconScale.value = withTiming(0.8, { duration: 150 }))} onPressOut={() => (iconScale.value = withTiming(1, { duration: 150 }))} accessibilityRole="button" @@ -79,10 +80,12 @@ export function TabBarButton({ - + ); } -const Wrapper = styled(Pressable, { - flex: 1, +const styles = StyleSheet.create({ + pressable: { + flex: 1, + }, }); diff --git a/src/components/playground/common.tsx b/src/components/playground/common.tsx index 3a0b05fb..817b7e4f 100644 --- a/src/components/playground/common.tsx +++ b/src/components/playground/common.tsx @@ -1,11 +1,16 @@ import type { ReactNode } from 'react'; +import { View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Icon, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; export function Note({ children }: { children: ReactNode }) { return ( - + @@ -18,14 +23,16 @@ export function Note({ children }: { children: ReactNode }) { {children} - + ); } -const NoteWrapper = styled('View', { - borderLeftWidth: 6, - borderColor: '$warn', - borderRadius: '$small', - backgroundColor: '$warnMuted', - padding: '$regular', -}); +const styles = StyleSheet.create((theme) => ({ + wrapper: { + borderLeftWidth: 6, + borderColor: theme.colors.warn, + borderRadius: theme.radii.small, + backgroundColor: theme.colors.warnMuted, + padding: theme.space.regular, + }, +})); diff --git a/src/components/playground/utils.tsx b/src/components/playground/utils.tsx index 7f016a27..88f7e538 100644 --- a/src/components/playground/utils.tsx +++ b/src/components/playground/utils.tsx @@ -1,19 +1,17 @@ import { router } from 'expo-router'; import { View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import * as DropdownMenu from 'zeego/dropdown-menu'; import { IconButton } from '~components/uikit'; import config from '~constants/config'; -import { useTheme } from '~styles'; import { useHeaderOptions } from '~utils/navigation'; export function useHeaderPlaygroundButton() { - const theme = useTheme(); - useHeaderOptions({ headerRight: () => { return config.appEnv !== 'production' ? ( - + @@ -36,3 +34,9 @@ export function useHeaderPlaygroundButton() { }, }); } + +const styles = StyleSheet.create((theme) => ({ + wrapper: { + marginHorizontal: theme.space.regular, + }, +})); diff --git a/src/components/settings/AppearanceMenuTarget.tsx b/src/components/settings/AppearanceMenuTarget.tsx index d19bbdb1..672c5b4b 100644 --- a/src/components/settings/AppearanceMenuTarget.tsx +++ b/src/components/settings/AppearanceMenuTarget.tsx @@ -1,34 +1,33 @@ -import { useLingui } from '@lingui/react/macro'; +// import { useLingui } from '@lingui/react/macro'; +// import { UnistylesRuntime } from 'react-native-unistyles'; -import MenuList from '~components/common/MenuList'; -import { useColorMode } from '~services/color-mode'; +// import MenuList from '~components/common/MenuList'; -export function AppearanceMenuTarget() { - const { t } = useLingui(); - const { setColorMode, colorMode } = useColorMode(); +// export function AppearanceMenuTarget() { +// const { t } = useLingui(); - return ( - setColorMode('auto'), - }, - { - id: 'dark', - label: t`Dark`, - checked: colorMode === 'dark', - onPress: () => setColorMode('dark'), - }, - { - id: 'light', - label: t`Light`, - checked: colorMode === 'light', - onPress: () => setColorMode('light'), - }, - ]} - /> - ); -} +// return ( +// UnistylesRuntime.setAdaptiveThemes(true), +// }, +// { +// id: 'dark', +// label: t`Dark`, +// checked: colorMode === 'dark', +// onPress: () => UnistylesRuntime.setTheme('dark'), +// }, +// { +// id: 'light', +// label: t`Light`, +// checked: colorMode === 'light', +// onPress: () => UnistylesRuntime.setTheme('light'), +// }, +// ]} +// /> +// ); +// } diff --git a/src/components/settings/hooks.tsx b/src/components/settings/hooks.tsx index f0f8f00e..63ab5095 100644 --- a/src/components/settings/hooks.tsx +++ b/src/components/settings/hooks.tsx @@ -2,17 +2,14 @@ import { useLingui } from '@lingui/react/macro'; import { type FunctionComponent } from 'react'; import { View } from 'react-native'; -import { useColorMode } from '~services/color-mode'; import { useI18n } from '~services/i18n'; -import { AppearanceMenuTarget } from './AppearanceMenuTarget'; import { LanguageMenuTarget } from './LanguageMenuTarget'; import { SystemInfoMenuTarget } from './SystemInfoMenuTarget'; export function useMenuListItem({ targetName }: { targetName: string }) { const { locale } = useI18n(); const { t } = useLingui(); - const { colorMode } = useColorMode(); let label = ''; let currentValue; @@ -24,16 +21,16 @@ export function useMenuListItem({ targetName }: { targetName: string }) { currentValue = locale === 'en' ? t`English` : t`Suomi`; target = LanguageMenuTarget; break; - case 'AppearanceMenuTarget': - label = t`Appearance`; - currentValue = - colorMode === 'light' - ? t`Light` - : colorMode === 'dark' - ? t`Dark` - : t`Automatic`; - target = AppearanceMenuTarget; - break; + // case 'AppearanceMenuTarget': + // label = t`Appearance`; + // currentValue = + // colorMode === 'light' + // ? t`Light` + // : colorMode === 'dark' + // ? t`Dark` + // : t`Automatic`; + // target = AppearanceMenuTarget; + // break; case 'SystemInfoMenuTarget': label = t`Info`; target = SystemInfoMenuTarget; diff --git a/src/components/store-review/ImprovementForm.tsx b/src/components/store-review/ImprovementForm.tsx index 81f86e2e..746d1bba 100644 --- a/src/components/store-review/ImprovementForm.tsx +++ b/src/components/store-review/ImprovementForm.tsx @@ -2,11 +2,11 @@ import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; import { Trans, useLingui } from '@lingui/react/macro'; import { useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import { ScrollView } from 'react-native'; +import { ScrollView, View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { showToast } from '~components/common/Toaster'; import { Button, IconButton, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; import { sleep } from '~utils/common'; type ImprovementFormType = { @@ -26,6 +26,11 @@ export default function ImprovementForm({ }); const [isFocused, setFocused] = useState(false); + // Controls the visual styles based on the input state + styles.useVariants({ + focused: isFocused, + }); + async function onSubmitFeedback() { try { await sleep(2000); @@ -49,7 +54,7 @@ export default function ImprovementForm({ justify="between" style={{ height: '100%' }} > - + How can we improve? @@ -66,8 +71,9 @@ export default function ImprovementForm({ rules={{ required: t`Feedback is required` }} render={({ field }) => { return ( - - + setFocused(true)} onBlur={() => setFocused(false)} /> - + ); }} /> @@ -98,38 +104,34 @@ export default function ImprovementForm({ ); } -const BackButton = styled(IconButton, { - position: 'absolute', - top: -10, - right: 10, -}); - -const InputWrapper = styled('View', { - alignItems: 'flex-end', - position: 'relative', - flexDirection: 'row', - borderBottomWidth: 1, - borderTopRightRadius: '$regular', - borderTopLeftRadius: '$regular', - variants: { - focused: { - true: { backgroundColor: 'rgba(150, 150, 150, 0.15)' }, - false: { backgroundColor: 'transparent' }, - }, - valid: { - true: { borderColor: '$text' }, - false: { borderColor: '$error' }, +const styles = StyleSheet.create((theme) => ({ + backButton: { + position: 'absolute', + top: -10, + right: 10, + }, + inputWrapper: { + alignItems: 'flex-end', + position: 'relative', + flexDirection: 'row', + borderBottomWidth: 1, + borderTopRightRadius: theme.radii.regular, + borderTopLeftRadius: theme.radii.regular, + width: '100%', + variants: { + focused: { + true: { backgroundColor: 'rgba(150, 150, 150, 0.15)' }, + false: { backgroundColor: 'transparent' }, + }, }, }, - width: '100%', -}); - -const Input = styled(BottomSheetTextInput, { - minHeight: 60, - typography: 'body', - color: '$text', - flexGrow: 1, - paddingHorizontal: '$small', - paddingBottom: 10, - paddingTop: '$medium', -}); + input: { + minHeight: 60, + ...theme.typography.body, + color: theme.colors.text, + flexGrow: 1, + paddingHorizontal: theme.space.small, + paddingBottom: 10, + paddingTop: theme.space.medium, + }, +})); diff --git a/src/components/store-review/StoreReview.tsx b/src/components/store-review/StoreReview.tsx index ede9477b..e64b0a57 100644 --- a/src/components/store-review/StoreReview.tsx +++ b/src/components/store-review/StoreReview.tsx @@ -2,10 +2,10 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { differenceInDays } from 'date-fns'; import * as ExpoStoreReview from 'expo-store-review'; import { useEffect, useState } from 'react'; +import { StyleSheet } from 'react-native-unistyles'; import ImprovementForm from '~components/store-review/ImprovementForm'; import { BottomSheet, Button, Stack, Text } from '~components/uikit'; -import { styled } from '~styles'; import storage, { STORAGE_KEYS } from '~utils/storage'; import { showToast } from '../common/Toaster'; @@ -87,7 +87,12 @@ export default function StoreReview() { function Feedback() { return ( - + Enjoying the app? @@ -106,7 +111,7 @@ export default function StoreReview() { > Could be better - + ); } @@ -136,7 +141,9 @@ export default function StoreReview() { ); } -const FeedbackWrapper = styled(Stack, { - width: '100%', - paddingHorizontal: '$regular', -}); +const styles = StyleSheet.create((theme) => ({ + feedbackWrapper: { + width: '100%', + paddingHorizontal: theme.space.regular, + }, +})); diff --git a/src/components/uikit/Accordion.tsx b/src/components/uikit/Accordion.tsx index 805e7951..e5205477 100644 --- a/src/components/uikit/Accordion.tsx +++ b/src/components/uikit/Accordion.tsx @@ -2,8 +2,9 @@ import { useLingui } from '@lingui/react/macro'; import { useState } from 'react'; import { TouchableOpacity } from 'react-native'; import Collapsible, { type CollapsibleProps } from 'react-native-collapsible'; +import { StyleSheet } from 'react-native-unistyles'; -import { styled, type Color } from '~styles'; +import { type Color } from '~styles/styled'; import { haptics } from '~utils/haptics'; import { Icon, type IconName } from './Icon'; @@ -62,12 +63,6 @@ export function Accordion({ ); } -const Title = styled(Stack, { - borderBottomWidth: 1, - borderBottomColor: '$line3', - paddingVertical: '$small', -}); - function AccordionHeader({ title, icon, @@ -80,7 +75,13 @@ function AccordionHeader({ collapsed: boolean; }) { return ( - + <Stack + style={styles.header} + axis="x" + spacing="small" + align="center" + justify="between" + > <Text variant="headingS" numberOfLines={1} style={{ flex: 1 }}> {title} </Text> @@ -88,6 +89,14 @@ function AccordionHeader({ {icon && <Icon name={icon} color={iconColor} size={24} />} <Icon name={collapsed ? 'chevronDown' : 'chevronUp'} size={24} /> - + ); } + +const styles = StyleSheet.create((theme) => ({ + header: { + borderBottomWidth: 1, + borderBottomColor: theme.colors.line3, + paddingVertical: theme.space.small, + }, +})); diff --git a/src/components/uikit/BottomSheet.tsx b/src/components/uikit/BottomSheet.tsx index 9f6eddd3..5bdd5dfe 100644 --- a/src/components/uikit/BottomSheet.tsx +++ b/src/components/uikit/BottomSheet.tsx @@ -11,8 +11,8 @@ import { useRef, type ReactNode, } from 'react'; - -import { styled, useTheme } from '~styles'; +import { View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; type BottomSheetProps = RNBottomSheetProps & { initialIndex?: number; @@ -35,8 +35,6 @@ export const BottomSheet = forwardRef( }: BottomSheetProps, ref ) => { - const theme = useTheme(); - const bottomSheetRef = useRef(null); useImperativeHandle(ref, () => ({ close: () => bottomSheetRef.current?.close(), @@ -91,7 +89,7 @@ export const BottomSheet = forwardRef( ( backdropComponent={renderBackdropComponent} accessible={false} // Important if you want to access the bottom sheet content > - {children} + {children} ); } @@ -111,6 +109,11 @@ export const BottomSheet = forwardRef( BottomSheet.displayName = 'BottomSheet'; -const ContentWrapper = styled('View', { - padding: '$regular', -}); +const styles = StyleSheet.create((theme) => ({ + contentWrapper: { + padding: theme.space.regular, + }, + background: { + backgroundColor: theme.colors.surface, + }, +})); diff --git a/src/components/uikit/Card.tsx b/src/components/uikit/Card.tsx index 2e5533f5..bd4c3989 100644 --- a/src/components/uikit/Card.tsx +++ b/src/components/uikit/Card.tsx @@ -1,12 +1,17 @@ -import { StyleSheet } from 'react-native'; +import { View, type ViewProps } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; -import { styled } from '~styles'; +export const Card = (props: ViewProps) => ( + +); -export const Card = styled('View', { - backgroundColor: '$surface', - borderRadius: '$regular', - borderWidth: StyleSheet.hairlineWidth, - borderColor: '$line3', - padding: '$regular', - shadow: 'small', -}); +const styles = StyleSheet.create((theme) => ({ + card: { + backgroundColor: theme.colors.surface, + borderRadius: theme.radii.regular, + borderWidth: StyleSheet.hairlineWidth, + borderColor: theme.colors.line3, + padding: theme.space.regular, + ...theme.shadows.small, + }, +})); diff --git a/src/components/uikit/Icon.tsx b/src/components/uikit/Icon.tsx index 6da1108e..a7d92efd 100644 --- a/src/components/uikit/Icon.tsx +++ b/src/components/uikit/Icon.tsx @@ -1,15 +1,16 @@ import { memo } from 'react'; import type { AccessibilityProps, ViewStyle } from 'react-native'; import { SvgXml } from 'react-native-svg'; +import { useUnistyles } from 'react-native-unistyles'; import * as icons from '~design-system/icons'; -import { type Theme, useTheme } from '~styles'; +import { type Color } from '~styles/styled'; export type IconName = keyof typeof icons; type Props = { name: IconName; - color?: keyof Theme['colors']; + color?: Color; size?: number; style?: ViewStyle; }; @@ -21,9 +22,8 @@ export const Icon = memo(function Icon({ style, ...rest }: Props & AccessibilityProps) { - const theme = useTheme(); + const { theme } = useUnistyles(); const iconColor = theme.colors[color]; - return ( ); } diff --git a/src/components/uikit/PickerModal.tsx b/src/components/uikit/PickerModal.tsx index d21671c8..ea496450 100644 --- a/src/components/uikit/PickerModal.tsx +++ b/src/components/uikit/PickerModal.tsx @@ -6,12 +6,15 @@ import { Easing, Modal, ScrollView, + TouchableOpacity, TouchableWithoutFeedback, useWindowDimensions, + View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { StyleSheet } from 'react-native-unistyles'; -import { styled } from '~styles'; +import { absoluteFill, flexCenter } from '~styles/utils'; import { announceForAccessibility } from '~utils/a11y'; import { Text } from './Text'; @@ -138,27 +141,36 @@ function PickerLayout({ const insets = useSafeAreaInsets(); return ( - + - + - {children} - - + + ); } @@ -213,13 +225,17 @@ function SinglePicker({ /> -
- + + Close - -
+ +
); } @@ -281,53 +297,53 @@ function MultiplePicker({ /> -
- + + Cancel - - + + Done - -
+ + ); } -const Wrapper = styled('View', { - flex: 1, - justifyContent: 'flex-end', -}); - -const Backdrop = Animated.createAnimatedComponent( - styled('View', { - absoluteFill: true, +const styles = StyleSheet.create((theme) => ({ + wrapper: { + flex: 1, + justifyContent: 'flex-end', + }, + backdrop: { backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: 1, - }) -); - -const Content = Animated.createAnimatedComponent( - styled('View', { - backgroundColor: '$surface', - shadow: 'large', - padding: '$medium', - borderTopLeftRadius: '$medium', - borderTopRightRadius: '$medium', + }, + content: { + backgroundColor: theme.colors.surface, + ...theme.shadows.large, + padding: theme.space.medium, + borderTopLeftRadius: theme.radii.medium, + borderTopRightRadius: theme.radii.medium, zIndex: 2, - }) -); - -const Footer = styled('View', { - flexDirection: 'row', -}); - -const ActionButton = styled('TouchableOpacity', { - flex: 1, - paddingTop: '$regular', - paddingBottom: '$medium', - flexCenter: 'row', -}); + }, + footer: { + flexDirection: 'row', + }, + actionButton: { + flex: 1, + paddingTop: theme.space.regular, + paddingBottom: theme.space.medium, + }, +})); diff --git a/src/components/uikit/PickerSheet.tsx b/src/components/uikit/PickerSheet.tsx index b31e3f97..dcc92f64 100644 --- a/src/components/uikit/PickerSheet.tsx +++ b/src/components/uikit/PickerSheet.tsx @@ -1,10 +1,17 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { FlashList } from '@shopify/flash-list'; import { memo, useState, type ReactNode } from 'react'; -import { Modal, Platform } from 'react-native'; +import { + Modal, + Platform, + SafeAreaView, + TouchableOpacity, + View, +} from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import StatusBar from '~components/common/StatusBar'; -import { styled } from '~styles'; +import { flexCenter } from '~styles/utils'; import { useEffectEvent } from '~utils/common'; import { Text } from './Text'; @@ -122,7 +129,7 @@ function ModalContent({ } return ( - + -
- + {multiple ? Cancel : Close} - + {multiple && ( - Done - + )} -
+ {/* On iOS the modal effect will reveal the black root background */} {Platform.OS === 'ios' && } -
+ ); } @@ -196,7 +205,7 @@ type ListItemProps = { const ListItem = memo( ({ multiple, label, value, checked, onOptionSelect }: ListItemProps) => { return ( - + {multiple ? ( onOptionSelect(value)} /> )} - + ); } ); @@ -234,7 +243,7 @@ function ListHeader({ }) { const { t } = useLingui(); return ( - + {numSelected > 1 && ( - Clear selected ({numSelected}) - + )} - + ); } function ListEmpty({ children }: { children?: ReactNode }) { return ( - + {children || ( @@ -283,7 +293,7 @@ function ListEmpty({ children }: { children?: ReactNode }) { )} - + ); } @@ -291,42 +301,36 @@ function ListSeparator() { return ; } -const SafeArea = styled('SafeAreaView', { - flex: 1, - backgroundColor: '$surface', -}); - -const ListEmptyWrapper = styled('View', { - padding: '$large', - flexCenter: 'row', -}); - -const ListHeaderWrapper = styled('View', { - marginBottom: '$small', - padding: '$regular', - backgroundColor: '$surface', - borderBottomWidth: 1, - borderColor: '$line3', -}); - -const ClearButton = styled('TouchableOpacity', { - alignSelf: 'flex-end', -}); - -const ListItemWrapper = styled('View', { - paddingHorizontal: '$small', -}); - -const Footer = styled('View', { - width: '100%', - borderTopWidth: 1, - borderColor: '$line3', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-around', -}); - -const ActionButton = styled('TouchableOpacity', { - padding: '$regular', - flexCenter: 'row', -}); +const styles = StyleSheet.create((theme) => ({ + safeArea: { + flex: 1, + backgroundColor: theme.colors.surface, + }, + listEmptyWrapper: { + padding: theme.space.large, + }, + listHeaderWrapper: { + marginBottom: theme.space.small, + padding: theme.space.regular, + backgroundColor: theme.colors.surface, + borderBottomWidth: 1, + borderColor: theme.colors.line3, + }, + clearButton: { + alignSelf: 'flex-end', + }, + listItemWrapper: { + paddingHorizontal: theme.space.small, + }, + footer: { + width: '100%', + borderTopWidth: 1, + borderColor: theme.colors.line3, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + }, + actionButton: { + padding: theme.space.regular, + }, +})); diff --git a/src/components/uikit/ProgressBar.tsx b/src/components/uikit/ProgressBar.tsx index 476bc4a3..877186ac 100644 --- a/src/components/uikit/ProgressBar.tsx +++ b/src/components/uikit/ProgressBar.tsx @@ -1,12 +1,11 @@ -import { type JSX } from 'react'; +import { useEffect, type JSX } from 'react'; +import { View } from 'react-native'; import Animated, { useAnimatedStyle, - useDerivedValue, useSharedValue, withTiming, } from 'react-native-reanimated'; - -import { styled } from '~styles'; +import { StyleSheet } from 'react-native-unistyles'; type Props = { step: number; @@ -35,11 +34,11 @@ export function ProgressBar({ const progressAnim = useSharedValue(0); - useDerivedValue(() => { + useEffect(() => { progressAnim.value = withTiming(progress, { duration: animated ? 200 : 0, }); - }, [step]); + }, [animated, progress, progressAnim, step]); const animatedStyle = useAnimatedStyle(() => { return { @@ -48,25 +47,24 @@ export function ProgressBar({ }); return ( - - - + + ); } -const ProgressContainer = styled('View', { - borderRadius: '$full', - backgroundColor: '$primaryMutedHover', -}); - -const Progress = styled('View', { - borderRadius: '$full', - backgroundColor: '$primary', -}); - -const AnimatedProgress = Animated.createAnimatedComponent(Progress); +const styles = StyleSheet.create((theme) => ({ + progressContainer: { + borderRadius: theme.radii.full, + backgroundColor: theme.colors.primaryMutedHover, + }, + progress: { + backgroundColor: theme.colors.primary, + borderRadius: theme.radii.full, + }, +})); diff --git a/src/components/uikit/SegmentedControl.tsx b/src/components/uikit/SegmentedControl.tsx index 348f8104..a0b6d50c 100644 --- a/src/components/uikit/SegmentedControl.tsx +++ b/src/components/uikit/SegmentedControl.tsx @@ -1,14 +1,20 @@ import { useLingui } from '@lingui/react/macro'; import { Fragment, useState } from 'react'; -import type { LayoutChangeEvent, LayoutRectangle } from 'react-native'; +import { + TouchableOpacity, + View, + type LayoutChangeEvent, + type LayoutRectangle, +} from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated'; +import { StyleSheet } from 'react-native-unistyles'; -import { styled } from '~styles'; +import { absoluteFill } from '~styles/utils'; import { haptics } from '~utils/haptics'; import { Text } from './Text'; @@ -23,11 +29,12 @@ export function SegmentedControl(props: Props) { const [layout, setLayout] = useState(); return ( - setLayout(e.nativeEvent.layout)} > {!!layout && } - + ); } @@ -61,7 +68,13 @@ function Segments({ return ( <> - + {segments.map((segment, index) => { return ( @@ -110,8 +123,10 @@ function Segment({ return ( - {label} - - + + ); } @@ -135,39 +150,30 @@ function Segment({ // NOTE: we are using hard coded border radii here in order to have the wrapper // and the segment button radii match perfectly -const Wrapper = styled('View', { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '$surface', - borderRadius: 10, -}); - -const SegmentBackground = Animated.createAnimatedComponent( - styled('View', { - absoluteFill: true, +const styles = StyleSheet.create((theme) => ({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.surface, + borderRadius: 10, + }, + segmentBackground: { backgroundColor: 'rgba(150, 150, 150, 0.15)', borderRadius: 8, - }) -); - -const SegmentButton = styled('TouchableOpacity', { - position: 'relative', - flex: 1, - flexCenter: 'row', - paddingVertical: '$small', - paddingHorizontal: '$regular', - zIndex: 1, - elevation: 1, -}).attrs(() => ({ - activeOpacity: 0.8, -})); - -const SegmentSeparator = Animated.createAnimatedComponent( - styled('View', { + }, + segmentButton: { + position: 'relative', + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.space.small, + paddingHorizontal: theme.space.regular, + zIndex: 1, + elevation: 1, + }, + segmentSeparator: { width: 1, height: '50%', - backgroundColor: '$line2', - zIndex: -1, - elevation: -1, - }) -); + backgroundColor: theme.colors.line2, + }, +})); diff --git a/src/components/uikit/Text.tsx b/src/components/uikit/Text.tsx index 51e3ca8d..f091ba89 100644 --- a/src/components/uikit/Text.tsx +++ b/src/components/uikit/Text.tsx @@ -1,19 +1,58 @@ -import { styled, themeProp, getTextTypographyVariants } from '~styles'; +import { Text as RNText } from 'react-native'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -export const Text = styled('Text', getTextTypographyVariants(), { - color: '$text', - variants: { - ...themeProp('color', 'colors', (color) => ({ - color, - })), - align: { - left: { textAlign: 'left' }, - right: { textAlign: 'right' }, - center: { textAlign: 'center' }, - }, - uppercase: { - true: { textTransform: 'uppercase' }, - false: { textTransform: 'none' }, +import { type Color, type Typography } from '~styles/styled'; +import { getTypography } from '~styles/utils'; + +type TextProps = RNText['props'] & { + variant?: Typography; + align?: 'left' | 'right' | 'center'; + uppercase?: boolean; + color?: Color; + children: React.ReactNode; +}; + +const styles = StyleSheet.create((theme) => ({ + text: { + color: theme.colors.text, + variants: { + color: Object.fromEntries( + Object.entries(theme.colors).map(([key, value]) => [ + key, + { color: value }, + ]) + ), + align: { + left: { textAlign: 'left' }, + right: { textAlign: 'right' }, + center: { textAlign: 'center' }, + }, + uppercase: { + true: { textTransform: 'uppercase' }, + false: { textTransform: 'none' }, + }, }, }, -}); +})); + +export function Text({ + variant = 'body', + align = 'left', + uppercase = false, + color, + children, + ...props +}: TextProps) { + const { theme } = useUnistyles(); + styles.useVariants({ + align, + uppercase, + color, + }); + + return ( + + {children} + + ); +} diff --git a/src/components/uikit/buttons/Button.tsx b/src/components/uikit/buttons/Button.tsx index e39083b4..099c28e6 100644 --- a/src/components/uikit/buttons/Button.tsx +++ b/src/components/uikit/buttons/Button.tsx @@ -1,6 +1,11 @@ -import { ActivityIndicator, type GestureResponderEvent } from 'react-native'; +import { + ActivityIndicator, + TouchableOpacity, + type GestureResponderEvent, +} from 'react-native'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -import { styled, useTheme, type Typography } from '~styles'; +import { type Typography } from '~styles/styled'; import { haptics } from '~utils/haptics'; import { Icon } from '../Icon'; @@ -23,7 +28,7 @@ export function Button({ accessibilityRole, ...rest }: ButtonProps) { - const theme = useTheme(); + const { theme } = useUnistyles(); const textVariant = sizeToTextVariant[size]; const iconSize = sizeToIconSize[size]; @@ -47,12 +52,16 @@ export function Button({ } } + styles.useVariants({ + size, + disabled, + }); + return ( - )} - + ); } @@ -100,29 +109,18 @@ const sizeToLineHeight: Record = { large: 26, }; -const Wrapper = styled('TouchableOpacity', { - borderRadius: '$full', - variants: { - size: { - small: { - minHeight: 32, - paddingHorizontal: '$small', +const styles = StyleSheet.create((theme) => ({ + wrapper: { + borderRadius: theme.radii.full, + variants: { + size: { + small: { minHeight: 32, paddingHorizontal: theme.space.small }, + normal: { minHeight: 44, paddingHorizontal: theme.space.regular }, + large: { minHeight: 60, paddingHorizontal: theme.space.medium }, }, - normal: { - minHeight: 44, - paddingHorizontal: '$regular', - }, - large: { - minHeight: 60, - paddingHorizontal: '$medium', - }, - }, - disabled: { - true: { - opacity: 0.9, + disabled: { + true: { opacity: 0.9 }, }, }, }, -}).attrs(({ disabled }) => ({ - activeOpacity: disabled ? 0.9 : 0.8, })); diff --git a/src/components/uikit/buttons/IconButton.tsx b/src/components/uikit/buttons/IconButton.tsx index 63619da7..749c03d3 100644 --- a/src/components/uikit/buttons/IconButton.tsx +++ b/src/components/uikit/buttons/IconButton.tsx @@ -1,13 +1,18 @@ import { useLingui } from '@lingui/react/macro'; -import { ActivityIndicator, type GestureResponderEvent } from 'react-native'; +import { + ActivityIndicator, + Pressable, + type GestureResponderEvent, +} from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, withSpring, withTiming, } from 'react-native-reanimated'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -import { styled, useTheme } from '~styles'; +import { flexCenter } from '~styles/utils'; import { haptics } from '~utils/haptics'; import { Icon } from '../Icon'; @@ -30,7 +35,7 @@ export function IconButton({ ...rest }: IconButtonProps) { const { t } = useLingui(); - const theme = useTheme(); + const { theme } = useUnistyles(); const pressed = useSharedValue(false); const iconSize = sizeToIconSize[size]; const wantedHitSize = iconSize * HIT_SLOP_FACTOR; @@ -81,22 +86,26 @@ export function IconButton({ } } + styles.useVariants({ + size, + disabled, + }); + return ( - - + {loading ? ( ) : ( @@ -104,45 +113,42 @@ export function IconButton({ )} - + ); } -const Wrapper = styled('Pressable', { - borderRadius: '$medium', - position: 'relative', - flexCenter: 'row', - variants: { - size: { - small: { - height: 16, - width: 16, - borderRadius: '$regular', - }, - normal: { - height: 24, - width: 24, - borderRadius: '$regular', +const styles = StyleSheet.create((theme) => ({ + wrapper: { + borderRadius: theme.radii.medium, + position: 'relative', + variants: { + size: { + small: { + height: 16, + width: 16, + borderRadius: theme.radii.regular, + }, + normal: { + height: 24, + width: 24, + borderRadius: theme.radii.regular, + }, + large: { + height: 44, + width: 44, + }, }, - large: { - height: 44, - width: 44, - }, - }, - disabled: { - true: { - opacity: 0.9, + disabled: { + true: { + opacity: 0.9, + }, }, }, }, -}); - -const PressHighlight = Animated.createAnimatedComponent( - styled('View', { - absoluteFill: true, + pressHighlight: { zIndex: -1, elevation: -1, - backgroundColor: '$pressHighlight', - borderRadius: '$full', - }) -); + backgroundColor: theme.colors.neutral5, // TODO: Add press highlight color to theme + borderRadius: theme.radii.full, + }, +})); diff --git a/src/components/uikit/buttons/helpers.ts b/src/components/uikit/buttons/helpers.ts index 4d7ddb0d..33b0005e 100644 --- a/src/components/uikit/buttons/helpers.ts +++ b/src/components/uikit/buttons/helpers.ts @@ -1,6 +1,7 @@ import { type StyleProp, type ViewStyle } from 'react-native'; +import { type UnistylesThemes } from 'react-native-unistyles'; -import { type Color, type useTheme } from '~styles'; +import { type Color } from '~styles/styled'; import { type ButtonProps, @@ -15,7 +16,7 @@ const getBaseStyle = ({ color = 'primary', disabled = false, }: Pick & { - theme: ReturnType; + theme: UnistylesThemes['light']; }): ViewStyle => { const baseStyle: ViewStyle = { backgroundColor: 'transparent', @@ -101,7 +102,7 @@ export const getButtonWrapperStyle = ({ color = 'primary', disabled = false, }: Pick & { - theme: ReturnType; + theme: UnistylesThemes['light']; }): StyleProp => { return getBaseStyle({ variant, color, disabled, theme }); }; @@ -113,7 +114,7 @@ export const getIconWrapperStyle = ({ color = 'primary', disabled = false, }: Pick & { - theme: ReturnType; + theme: UnistylesThemes['light']; }): StyleProp => { if (color === 'neutral') { return { backgroundColor: 'transparent' }; diff --git a/src/components/uikit/inputs/Checkbox.tsx b/src/components/uikit/inputs/Checkbox.tsx index a90f9404..261727df 100644 --- a/src/components/uikit/inputs/Checkbox.tsx +++ b/src/components/uikit/inputs/Checkbox.tsx @@ -1,12 +1,13 @@ import { useLingui } from '@lingui/react/macro'; -import { PixelRatio } from 'react-native'; +import { PixelRatio, TouchableOpacity, View } from 'react-native'; import Animated, { Easing, useAnimatedStyle, withTiming, } from 'react-native-reanimated'; +import { StyleSheet } from 'react-native-unistyles'; -import { styled } from '~styles'; +import { flexCenter } from '~styles/utils'; import { haptics } from '~utils/haptics'; import { Icon } from '../Icon'; @@ -39,8 +40,11 @@ export function Checkbox({ onChange, checked, value, label }: Props) { onChange(value); } + styles.useVariants({ checked }); + return ( - - + - + {label} - + ); } -const Wrapper = styled('TouchableOpacity', { - flexDirection: 'row', - alignItems: 'center', -}); - -const RadioOuter = styled('View', { - position: 'relative', - width: 24, - height: 24, - backgroundColor: 'transparent', - borderRadius: '$regular', - borderWidth: PixelRatio.roundToNearestPixel(1.5), // try to match icon width - marginRight: '$small', - borderColor: '$text', - flexCenter: 'row', - variants: { - checked: { - true: { - backgroundColor: '$primary', +const styles = StyleSheet.create((theme) => ({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + }, + radioOuter: { + position: 'relative', + width: 24, + height: 24, + backgroundColor: 'transparent', + borderRadius: theme.radii.regular, + borderWidth: PixelRatio.roundToNearestPixel(1.5), // try to match + marginRight: theme.space.small, + borderColor: theme.colors.text, + variants: { + checked: { + true: { + backgroundColor: theme.colors.primary, + }, }, }, }, -}); +})); diff --git a/src/components/uikit/inputs/DateInput.tsx b/src/components/uikit/inputs/DateInput.tsx index dc43c985..500bec59 100644 --- a/src/components/uikit/inputs/DateInput.tsx +++ b/src/components/uikit/inputs/DateInput.tsx @@ -3,15 +3,13 @@ import { DateTime } from 'luxon'; import { forwardRef, useImperativeHandle, useState } from 'react'; import { Keyboard, - Platform, type AccessibilityProps, type ViewStyle, } from 'react-native'; import DatePicker from 'react-native-date-picker'; +import { StyleSheet } from 'react-native-unistyles'; -import { useColorMode } from '~services/color-mode'; import { useI18n } from '~services/i18n'; -import { styled } from '~styles'; import { haptics } from '~utils/haptics'; import { type IconName } from '../Icon'; @@ -49,7 +47,6 @@ export const DateInput = forwardRef( const { t } = useLingui(); const [isPickerOpen, setPickerOpen] = useState(false); const { locale } = useI18n(); - const { colorScheme } = useColorMode(); useImperativeHandle(ref, () => ({ focus: () => { @@ -67,9 +64,6 @@ export const DateInput = forwardRef( ? DateTime.DATETIME_SHORT : DateTime.TIME_SIMPLE; - // TODO: fix Android dark mode support - const pickerTheme = Platform.OS === 'ios' ? colorScheme : 'light'; - return ( <> {!!message && ( - + {message} - + )} ({ + message: { + marginTop: theme.space.xs, + marginLeft: theme.space.small, + }, +})); diff --git a/src/components/uikit/inputs/InputButton.tsx b/src/components/uikit/inputs/InputButton.tsx index ef009573..07ad0549 100644 --- a/src/components/uikit/inputs/InputButton.tsx +++ b/src/components/uikit/inputs/InputButton.tsx @@ -1,7 +1,6 @@ import { useLingui } from '@lingui/react/macro'; -import { type ViewStyle } from 'react-native'; - -import { styled } from '~styles'; +import { TouchableOpacity, View, type ViewStyle } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import { Icon, type IconName } from '../Icon'; import { Text } from '../Text'; @@ -45,6 +44,13 @@ export function InputButton({ }: Props) { const { t } = useLingui(); + // Controls the visual styles based on the input state + styles.useVariants({ + focused: isFocused, + valid: isValid, + disabled: isDisabled, + }); + return ( @@ -58,15 +64,20 @@ export function InputButton({ )} - - + - + {value || placeholder} - - {!!icon && ( - - - - )} - - + {!!icon && ( + + + + )} + + + {!!message && ( {!isValid && } @@ -98,44 +109,37 @@ export function InputButton({ ); } -const Wrapper = styled('View', { - position: 'relative', - display: 'flex', -}); - -const InputWrapper = styled('TouchableOpacity', { - position: 'relative', - flexDirection: 'row', - borderWidth: 1, - borderRadius: '$small', - backgroundColor: '$surface', - overflow: 'hidden', - variants: { - focused: { - true: { opacity: 0.5 }, - }, - valid: { - true: { borderColor: '$line1' }, - false: { borderColor: '$errorContrast' }, - }, - disabled: { - true: { backgroundColor: '$neutral4', borderWidth: 0 }, +const styles = StyleSheet.create((theme) => ({ + wrapper: { + position: 'relative', + display: 'flex', + }, + inputWrapper: { + position: 'relative', + flexDirection: 'row', + borderWidth: 1, + borderRadius: theme.radii.small, + backgroundColor: theme.colors.surface, + overflow: 'hidden', + variants: { + focused: { + true: { opacity: 0.5 }, + }, + valid: { + true: { borderColor: theme.colors.line1 }, + false: { borderColor: theme.colors.errorContrast }, + }, + disabled: { + true: { backgroundColor: theme.colors.neutral4, borderWidth: 0 }, + }, }, }, -}).attrs(({ disabled }) => ({ - activeOpacity: disabled ? 1 : 0.5, + input: { + minHeight: 60, + flexGrow: 1, + paddingHorizontal: theme.space.small, + }, + inputDecoration: { + paddingRight: theme.space.xs, + }, })); - -const Input = styled('View', { - alignItems: 'flex-end', - flexDirection: 'row', - minHeight: 60, - flexGrow: 1, - paddingHorizontal: '$small', - flexCenter: 'row', -}); - -const InputDecoration = styled('View', { - flexCenter: 'row', - paddingRight: '$xs', -}); diff --git a/src/components/uikit/inputs/Radio.tsx b/src/components/uikit/inputs/Radio.tsx index e56a2121..85e27241 100644 --- a/src/components/uikit/inputs/Radio.tsx +++ b/src/components/uikit/inputs/Radio.tsx @@ -1,8 +1,8 @@ import { useLingui } from '@lingui/react/macro'; import { useEffect, useRef } from 'react'; -import { Animated, PixelRatio } from 'react-native'; +import { Animated, PixelRatio, TouchableOpacity, View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; -import { styled } from '~styles'; import { haptics } from '~utils/haptics'; import { Text } from '../Text'; @@ -22,8 +22,12 @@ export function Radio({ onChange, checked, value, label }: Props) { onChange(value); } + // Controls the visual styles based on the checked state + styles.useVariants({ checked }); + return ( - - {checked && } + {checked && } {label} - + ); } @@ -49,40 +53,40 @@ function RadioInner() { }).start(); }, []); // eslint-disable-line react-hooks/exhaustive-deps - return ; + return ( + + ); } -const Wrapper = styled('TouchableOpacity', { - flexDirection: 'row', - alignItems: 'center', -}); - -const RadioOuter = styled('View', { - position: 'relative', - width: 24, - height: 24, - backgroundColor: 'transparent', - borderRadius: '$full', - borderWidth: PixelRatio.roundToNearestPixel(1.5), // match checkbox - marginRight: '$small', - borderColor: '$line1', - variants: { - checked: { - true: { - borderColor: '$primary', +const styles = StyleSheet.create((theme) => ({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + }, + radioOuter: { + position: 'relative', + width: 24, + height: 24, + backgroundColor: 'transparent', + borderRadius: theme.radii.full, + borderWidth: PixelRatio.roundToNearestPixel(1.5), // match checkbox + marginRight: theme.space.small, + borderColor: theme.colors.line1, + variants: { + checked: { + true: { + borderColor: theme.colors.primary, + }, }, }, }, -}); - -const RadioCircle = Animated.createAnimatedComponent( - styled('View', { + radioCircle: { position: 'absolute', top: 5, right: 5, bottom: 5, left: 5, - borderRadius: '$full', - backgroundColor: '$primary', - }) -); + borderRadius: theme.radii.full, + backgroundColor: theme.colors.primary, + }, +})); diff --git a/src/components/uikit/inputs/SearchInput.tsx b/src/components/uikit/inputs/SearchInput.tsx index 4d487886..105b0f02 100644 --- a/src/components/uikit/inputs/SearchInput.tsx +++ b/src/components/uikit/inputs/SearchInput.tsx @@ -7,8 +7,7 @@ import { useState, } from 'react'; import { TouchableOpacity, type TextInput as RNTextInput } from 'react-native'; - -import { styled } from '~styles'; +import { StyleSheet } from 'react-native-unistyles'; import { Icon } from '../Icon'; import { Text } from '../Text'; @@ -68,7 +67,12 @@ export const SearchInput = forwardRef( {...rest} /> {showSuggestions && filteredSuggestions.length > 0 && ( - + {filteredSuggestions.map((option, index) => ( ( ))} - + )} ); @@ -94,11 +98,13 @@ export const SearchInput = forwardRef( SearchInput.displayName = 'SearchInput'; -const Suggestions = styled(Stack, { - padding: '$small', - borderRadius: '$regular', - backgroundColor: '$surface', - borderWidth: 0.5, - borderColor: '$line3', - shadow: 'medium', -}); +const styles = StyleSheet.create((theme) => ({ + suggestions: { + padding: theme.space.small, + borderRadius: theme.radii.regular, + backgroundColor: theme.colors.surface, + borderWidth: 0.5, + borderColor: theme.colors.line3, + ...theme.shadows.medium, + }, +})); diff --git a/src/components/uikit/inputs/TextInput.tsx b/src/components/uikit/inputs/TextInput.tsx index 1589522b..66efcab5 100644 --- a/src/components/uikit/inputs/TextInput.tsx +++ b/src/components/uikit/inputs/TextInput.tsx @@ -1,14 +1,15 @@ import { t } from '@lingui/core/macro'; import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import { + TextInput as RNTextInput, TouchableOpacity, + View, type NativeSyntheticEvent, - type TextInput as RNTextInput, type TextInputProps as RNTextInputProps, type TextInputFocusEventData, } from 'react-native'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -import { styled } from '~styles'; import { haptics } from '~utils/haptics'; import { Icon, type IconName } from '../Icon'; @@ -61,6 +62,7 @@ export const TextInput = forwardRef( }: TextInputProps, ref ) => { + const { theme } = useUnistyles(); const [secureTextVisible, setSecureTextVisible] = useState(false); const [isFocused, setFocused] = useState(false); const [characterCount, setCharacterCount] = useState(value?.length || 0); @@ -68,6 +70,12 @@ export const TextInput = forwardRef( const inputRef = useRef(null); useImperativeHandle(ref, () => inputRef.current as RNTextInput); + // Controls the visual styles based on the input state + styles.useVariants({ + valid: isValid, + disabled: isDisabled, + }); + function handleCancel() { onChange(''); inputRef.current?.blur(); @@ -120,26 +128,21 @@ export const TextInput = forwardRef( )} {showCharacterLimit && isFocused && ( - {characterCount} {` / ${maxLength}`} - + )} - + {!!icon && } - ( selectTextOnFocus={!isDisabled} multiline={multiline} maxLength={maxLength} - style={style} + style={[styles.input, style]} + placeholderTextColor={theme.colors.textMuted} accessibilityRole={accessibilityRole ?? 'text'} accessibilityLabel={accessibilityLabel ?? t`${label ?? 'text'} input field`} // prettier-ignore accessibilityHint={accessibilityHint ?? t`Enter your ${label ?? 'text'} here`} // prettier-ignore @@ -163,7 +167,7 @@ export const TextInput = forwardRef( /> {allowSecureTextToggle ? ( - + setSecureTextVisible((p) => !p)} accessible @@ -181,7 +185,7 @@ export const TextInput = forwardRef( )} - + ) : ( ( accessibilityHint={t`Double tap to clear the input`} /> )} - + {!!message && ( @@ -217,39 +221,36 @@ export const TextInput = forwardRef( TextInput.displayName = 'TextInput'; -const InputWrapper = styled(Stack, { - padding: '$regular', - borderRadius: '$small', - backgroundColor: '$surface', - borderWidth: 1, - variants: { - valid: { - true: { borderColor: '$line1' }, - false: { borderColor: '$errorContrast' }, - }, - disabled: { - true: { backgroundColor: '$neutral4', borderWidth: 0 }, +const styles = StyleSheet.create((theme) => ({ + inputWrapper: { + padding: theme.space.regular, + borderRadius: theme.radii.small, + backgroundColor: theme.colors.surface, + borderWidth: 1, + variants: { + valid: { + true: { borderColor: theme.colors.line1 }, + false: { borderColor: theme.colors.errorContrast }, + }, + disabled: { + true: { backgroundColor: theme.colors.neutral4, borderWidth: 0 }, + }, }, }, -}); - -const Input = styled('TextInput', { - typography: 'body', - color: '$text', - lineHeight: 20, - width: '70%', // This is to prevent the input from expanding with the text and pushing the icon out of view - flexGrow: 1, -}).attrs((p) => ({ - placeholderTextColor: p.theme.colors.textMuted, + input: { + ...theme.typography.body, + color: theme.colors.text, + lineHeight: 20, + width: '70%', // This is to prevent the input from expanding with the text and pushing the icon out of view + flexGrow: 1, + }, + inputDecoration: { + flexDirection: 'row', + paddingRight: theme.space.xs, + }, + characterCount: { + position: 'absolute', + top: theme.space.regular, + right: theme.space.xxs, + }, })); - -const InputDecoration = styled('View', { - flexCenter: 'row', - paddingRight: '$xs', -}); - -const CharacterCount = styled(Text, { - position: 'absolute', - top: '$regular', - right: '$xxs', -}); diff --git a/src/components/uikit/layout/Grid.tsx b/src/components/uikit/layout/Grid.tsx index 23079cf1..c5245c2b 100644 --- a/src/components/uikit/layout/Grid.tsx +++ b/src/components/uikit/layout/Grid.tsx @@ -1,12 +1,13 @@ import { cloneElement, isValidElement, type ReactNode, useState } from 'react'; import { type LayoutChangeEvent, View, type ViewProps } from 'react-native'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -import { styled, type Theme, useTheme } from '~styles'; +import { type Space } from '~styles/styled'; import { flattenChildren } from '../helpers'; type Props = ViewProps & { - spacing: keyof Theme['space']; + spacing: Space; align?: 'center' | 'start' | 'end' | 'stretch'; justify?: 'center' | 'start' | 'end' | 'between' | 'around'; columns?: number; @@ -23,17 +24,24 @@ export function Grid({ }: Props) { // Handle `Fragments` by flattening children const elements = flattenChildren(children).filter((e) => isValidElement(e)); - const theme = useTheme(); + const { theme } = useUnistyles(); const [width, setWidth] = useState(-1); const colWidth = columns !== undefined && width !== -1 ? width / columns : undefined; + styles.useVariants({ + align, + justify, + }); + return ( - setWidth(e.nativeEvent.layout.width)} > {elements.map((child, index) => { @@ -49,26 +57,28 @@ export function Grid({ ); })} - + ); } -const Wrapper = styled('View', { - flexDirection: 'row', - flexWrap: 'wrap', - variants: { - align: { - center: { alignItems: 'center' }, - start: { alignItems: 'flex-start' }, - end: { alignItems: 'flex-end' }, - stretch: { alignItems: 'stretch' }, - }, - justify: { - center: { justifyContent: 'center' }, - start: { justifyContent: 'flex-start' }, - end: { justifyContent: 'flex-end' }, - between: { justifyContent: 'space-between' }, - around: { justifyContent: 'space-around' }, +const styles = StyleSheet.create(() => ({ + wrapper: { + flexDirection: 'row', + flexWrap: 'wrap', + variants: { + align: { + center: { alignItems: 'center' }, + start: { alignItems: 'flex-start' }, + end: { alignItems: 'flex-end' }, + stretch: { alignItems: 'stretch' }, + }, + justify: { + center: { justifyContent: 'center' }, + start: { justifyContent: 'flex-start' }, + end: { justifyContent: 'flex-end' }, + between: { justifyContent: 'space-between' }, + around: { justifyContent: 'space-around' }, + }, }, }, -}); +})); diff --git a/src/components/uikit/layout/Spacer.tsx b/src/components/uikit/layout/Spacer.tsx index db83739b..1b806ca3 100644 --- a/src/components/uikit/layout/Spacer.tsx +++ b/src/components/uikit/layout/Spacer.tsx @@ -1,22 +1,42 @@ -import { styled, themeProp } from '~styles'; +import React from 'react'; +import { View, type ViewProps } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; -export const Spacer = styled('View', { - flexShrink: 0, - variants: { - ...themeProp('size', 'space', (value) => ({ - width: `$space${value}`, - height: `$space${value}`, - })), - axis: { - x: { height: 'auto' }, - y: { width: 'auto' }, - }, - debug: { - true: { backgroundColor: 'red' }, - false: { backgroundColor: 'transparent' }, +import { type Space } from '~styles/styled'; + +type SpacerProps = { + size: Space; + axis?: 'x' | 'y'; + debug?: boolean; + style?: ViewProps['style']; +}; + +export function Spacer({ size, axis, debug = false, style }: SpacerProps) { + styles.useVariants({ + axis: axis || 'y', + debug, + }); + + return ; +} + +const styles = StyleSheet.create((theme) => ({ + spacer: (size: Space) => ({ + flexShrink: 0, + width: theme.space[size], + height: theme.space[size], + variants: { + axis: { + x: { height: 'auto' }, + y: { width: 'auto' }, + }, + debug: { + true: { backgroundColor: 'red' }, + false: { backgroundColor: 'transparent' }, + }, }, - }, -}); + }), +})); // @ts-ignore Spacer.__SPACER__ = true; // This is used to detect spacers inside Stack component diff --git a/src/components/uikit/layout/Stack.tsx b/src/components/uikit/layout/Stack.tsx index d8d5d672..441d7a8c 100644 --- a/src/components/uikit/layout/Stack.tsx +++ b/src/components/uikit/layout/Stack.tsx @@ -1,11 +1,12 @@ import type { ReactNode } from 'react'; -import type { ViewProps } from 'react-native'; +import { View, type ViewProps } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; -import { styled, theme, type Theme } from '~styles'; +import { type Space } from '~styles/styled'; type Props = ViewProps & { - spacing: keyof Theme['space'] | 'none'; - axis?: 'x' | 'y'; + spacing: Space | 'none'; + axis: 'x' | 'y'; align?: 'center' | 'start' | 'end' | 'stretch' | 'baseline'; justify?: 'center' | 'start' | 'end' | 'between' | 'around'; children: ReactNode; @@ -17,52 +18,47 @@ export function Stack({ spacing, align, justify, + style, ...rest }: Props) { + styles.useVariants({ + axis, + align, + justify, + spacing, + }); + return ( - + {children} - + ); } -const spacingVariants: { [key in Props['spacing']]: { gap: number } } = - Object.entries(theme.space).reduce( - (acc, [key, value]) => { - acc[key as Props['spacing']] = { - gap: Number(value.value), - }; - return acc; - }, - {} as { [key in Props['spacing']]: { gap: number } } - ); - -const Wrapper = styled('View', { - variants: { - axis: { - x: { flexDirection: 'row' }, - y: { flexDirection: 'column' }, - }, - align: { - center: { alignItems: 'center' }, - start: { alignItems: 'flex-start' }, - end: { alignItems: 'flex-end' }, - stretch: { alignItems: 'stretch' }, - baseline: { alignItems: 'baseline' }, - }, - justify: { - center: { justifyContent: 'center' }, - start: { justifyContent: 'flex-start' }, - end: { justifyContent: 'flex-end' }, - between: { justifyContent: 'space-between' }, - around: { justifyContent: 'space-around' }, +const styles = StyleSheet.create((theme) => ({ + wrapper: { + variants: { + axis: { + x: { flexDirection: 'row' }, + y: { flexDirection: 'column' }, + }, + align: { + center: { alignItems: 'center' }, + start: { alignItems: 'flex-start' }, + end: { alignItems: 'flex-end' }, + stretch: { alignItems: 'stretch' }, + baseline: { alignItems: 'baseline' }, + }, + justify: { + center: { justifyContent: 'center' }, + start: { justifyContent: 'flex-start' }, + end: { justifyContent: 'flex-end' }, + between: { justifyContent: 'space-between' }, + around: { justifyContent: 'space-around' }, + }, + spacing: Object.fromEntries( + Object.entries(theme.space).map(([key, value]) => [key, { gap: value }]) + ), }, - spacing: spacingVariants, }, -}); +})); diff --git a/src/design-system/typography.ts b/src/design-system/typography.ts index 7b5220df..0b29d380 100644 --- a/src/design-system/typography.ts +++ b/src/design-system/typography.ts @@ -1,4 +1,6 @@ -export const body = { +import { TypographyDefinition } from './utils'; + +export const body: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 400, fontSize: 16, @@ -6,7 +8,7 @@ export const body = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodyBold = { +export const bodyBold: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 16, @@ -14,7 +16,7 @@ export const bodyBold = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodyExtraSmall = { +export const bodyExtraSmall: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 400, fontSize: 12, @@ -22,7 +24,7 @@ export const bodyExtraSmall = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodyExtraSmallBold = { +export const bodyExtraSmallBold: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 12, @@ -30,7 +32,7 @@ export const bodyExtraSmallBold = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodyLarge = { +export const bodyLarge: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 400, fontSize: 18, @@ -38,7 +40,7 @@ export const bodyLarge = { letterSpacing: 0, lineHeight: 1.556, }; -export const bodyLargeBold = { +export const bodyLargeBold: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 18, @@ -46,7 +48,7 @@ export const bodyLargeBold = { letterSpacing: 0, lineHeight: 1.556, }; -export const bodySemiBold = { +export const bodySemiBold: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 600, fontSize: 16, @@ -54,7 +56,7 @@ export const bodySemiBold = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodySmall = { +export const bodySmall: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 400, fontSize: 14, @@ -62,7 +64,7 @@ export const bodySmall = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodySmallBold = { +export const bodySmallBold: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 14, @@ -70,7 +72,7 @@ export const bodySmallBold = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodySmallSemiBold = { +export const bodySmallSemiBold: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 600, fontSize: 14, @@ -78,7 +80,7 @@ export const bodySmallSemiBold = { letterSpacing: 0, lineHeight: 1.5, }; -export const displayExtraSmall = { +export const displayExtraSmall: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 500, fontSize: 32, @@ -86,7 +88,7 @@ export const displayExtraSmall = { letterSpacing: 0, lineHeight: 1.5, }; -export const displayLarge = { +export const displayLarge: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 500, fontSize: 64, @@ -94,7 +96,7 @@ export const displayLarge = { letterSpacing: 0, lineHeight: 1.5, }; -export const displaySmall = { +export const displaySmall: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 500, fontSize: 48, @@ -102,7 +104,7 @@ export const displaySmall = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingL = { +export const headingL: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 28, @@ -110,7 +112,7 @@ export const headingL = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingM = { +export const headingM: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 600, fontSize: 22, @@ -118,7 +120,7 @@ export const headingM = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingS = { +export const headingS: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 600, fontSize: 16, @@ -126,7 +128,7 @@ export const headingS = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingXl = { +export const headingXl: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 42, @@ -134,7 +136,7 @@ export const headingXl = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingXxl = { +export const headingXxl: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 72, @@ -142,7 +144,7 @@ export const headingXxl = { letterSpacing: 0, lineHeight: 1.5, }; -export const label = { +export const label: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 500, fontSize: 14, @@ -150,7 +152,7 @@ export const label = { letterSpacing: 0, lineHeight: 1.5, }; -export const lead = { +export const lead: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 400, fontSize: 28, @@ -158,7 +160,7 @@ export const lead = { letterSpacing: 0, lineHeight: 1.5, }; -export const leadBold = { +export const leadBold: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 28, @@ -166,7 +168,7 @@ export const leadBold = { letterSpacing: 0, lineHeight: 1.5, }; -export const linkText = { +export const linkText: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 600, fontSize: 16, @@ -174,7 +176,7 @@ export const linkText = { letterSpacing: 0, lineHeight: 1.5, }; -export const linkTextHover = { +export const linkTextHover: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 600, fontSize: 16, @@ -182,7 +184,7 @@ export const linkTextHover = { letterSpacing: 0, lineHeight: 1.5, }; -export const overlineRegular = { +export const overlineRegular: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 14, @@ -190,7 +192,7 @@ export const overlineRegular = { letterSpacing: 0, lineHeight: 1.5, }; -export const overlineSmall = { +export const overlineSmall: TypographyDefinition = { fontFamily: 'Inter', fontWeight: 700, fontSize: 12, diff --git a/src/design-system/utils.ts b/src/design-system/utils.ts index 1f277f19..8f6b24c5 100644 --- a/src/design-system/utils.ts +++ b/src/design-system/utils.ts @@ -1,3 +1,4 @@ +import { TextStyle } from 'react-native'; import * as colors from '~design-system/colors'; const WEIGHT_TO_FONT = { @@ -12,7 +13,9 @@ const WEIGHT_TO_FONT = { 900: 'Black', } as const; -export function getFontFromWeight(weight: number) { +export function getFontFromWeight( + weight: TextStyle['fontWeight'] +): FontWeightVar { return WEIGHT_TO_FONT[weight as FontWeightNum].toLowerCase() as FontWeightVar; } @@ -127,12 +130,12 @@ export function transformColors( // Types ---------------------------------------------------------------------- export type TypographyDefinition = { - fontFamily: string; - fontWeight: number; - fontSize: number; - textTransform: string; - letterSpacing: number; - lineHeight: number; + fontFamily: NonNullable; + fontWeight: NonNullable; + fontSize: NonNullable; + textTransform: NonNullable; + letterSpacing: NonNullable; + lineHeight: NonNullable; }; export type FontWeightToName = typeof WEIGHT_TO_FONT; diff --git a/src/services/color-mode.tsx b/src/services/color-mode.tsx deleted file mode 100644 index 5bd7e317..00000000 --- a/src/services/color-mode.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { type ReactNode, createContext, useContext } from 'react'; -import { type ColorSchemeName, useColorScheme } from 'react-native'; - -import { ThemeProvider, darkTheme, theme as lightTheme } from '~styles'; -import { STORAGE_KEYS, useStorageString } from '~utils/storage'; - -type ColorMode = 'light' | 'dark' | 'auto'; -type ColorScheme = 'light' | 'dark'; - -type ContextValue = { - colorMode: ColorMode; - colorScheme: ColorScheme; - setColorMode: (t: ColorMode) => void; -}; - -const ColorModeContext = createContext(undefined); - -// If in the future there can be more than two color modes we want to ensure that -// our light/dark are always applied correctly. -const getColorScheme = (c: ColorSchemeName): ColorScheme => - c === 'dark' ? 'dark' : 'light'; - -export function ColorModeProvider({ children }: { children: ReactNode }) { - const systemColorMode = useColorScheme(); - const [persistedColorMode, setPersistedColorMode] = useStorageString( - STORAGE_KEYS.COLOR_MODE - ); - - const colorMode = (persistedColorMode || 'auto') as ColorMode; - - let colorScheme: ColorScheme; - - if (colorMode === 'auto') { - colorScheme = getColorScheme(systemColorMode); - } else if (colorMode === 'dark') { - colorScheme = 'dark'; - } else { - colorScheme = 'light'; - } - - const theme = colorScheme === 'light' ? lightTheme : darkTheme; - - return ( - - {children} - - ); -} - -export const useColorMode = () => { - const context = useContext(ColorModeContext); - if (!context) throw new Error('Missing ColorModeProvider!'); - return context; -}; diff --git a/src/styles/helpers.ts b/src/styles/helpers.ts deleted file mode 100644 index 8665056f..00000000 --- a/src/styles/helpers.ts +++ /dev/null @@ -1,104 +0,0 @@ -import * as typographyTokens from '../design-system/typography'; -import { theme, type Theme } from './styled'; - -type Typography = keyof typeof typographyTokens; -type ThemeKey = keyof Theme; - -/** - * Generate all variants for a given theme key, eg: - * ``` - * themeProp('color', 'colors', (color) => ({ color })) - * ``` - * would generate a variant prop called `color` with all color values from theme: - * { - * color: { - * primary: { color: '$primary' }, - * secondary: { color: '$secondary' }, - * text: { color: '$text' }, - * etc... - * } - * } - */ -export function themeProp

( - prop: P, - themeKey: T, - getStyles: (token: string) => R -) { - const values: Record> = { [prop]: {} }; - - Object.values(theme[themeKey]).forEach(({ token }) => { - values[prop][token] = getStyles(`$${token}`); - }); - - return values as { - [K in P]: { [TK in keyof Theme[T]]: R }; - }; -} - -/** - * Automatically generate Text component typography variants from design tokens. - * Also add `withLineHeight` prop to control when to apply line height. - * { - * variant: { - * title1: { typography: '$title1' }, - * title2: { typography: '$title2' }, - * body: { typography: '$body' }, - * etc... - * } - * } - */ -type TypographyVariant = { - typography: Typography; - lineHeight: number; -}; - -type CompoundVariant = { - variant: Typography; - withLineHeight: boolean; - css: { lineHeight: string }; -}; - -type DefaultVariants = { - variant: Typography; - withLineHeight: boolean; -}; - -export function getTextTypographyVariants() { - const typographyVariants: Record = - {} as Record; - - const compoundVariants: CompoundVariant[] = []; - - const defaultVariants: DefaultVariants = { - variant: 'body', - withLineHeight: false, - }; - - (Object.keys(typographyTokens) as Typography[]).forEach((variant) => { - typographyVariants[variant] = { - typography: variant, - // The 1.25 multiplier ensures enough vertical space for ascenders and descenders, - // preventing text from being visually cropped. - lineHeight: typographyTokens[variant].fontSize * 1.25, - }; - - compoundVariants.push({ - variant, - withLineHeight: true, - css: { - lineHeight: `$${variant}`, - }, - }); - }); - - return { - compoundVariants, - defaultVariants, - variants: { - variant: typographyVariants, - // NOTE: styles can be empty here since we use this value in compoundVariants - // to set the correct line height from theme based on the `variant` prop - withLineHeight: { true: {}, false: {} }, - }, - }; -} diff --git a/src/styles/index.ts b/src/styles/index.ts deleted file mode 100644 index eb0725fd..00000000 --- a/src/styles/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as stitches from './styled'; - -const { styled, css, createTheme, useTheme, theme, ThemeProvider, darkTheme } = - stitches; - -export type { - Theme, - Color, - Space, - Radii, - LineHeight, - Typography, -} from './styled'; - -export { themeProp, getTextTypographyVariants } from './helpers'; -export { styled, css, createTheme, useTheme, theme, ThemeProvider, darkTheme }; diff --git a/src/styles/styled.ts b/src/styles/styled.ts index 7700562a..6e9f20ab 100644 --- a/src/styles/styled.ts +++ b/src/styles/styled.ts @@ -1,43 +1,60 @@ -import { StyleSheet } from 'react-native'; -import type * as Stitches from 'stitches-native'; -import { createStitches } from 'stitches-native'; +import { StyleSheet } from 'react-native-unistyles'; import * as colors from '~design-system/colors'; import * as radii from '~design-system/radii'; +import * as shadows from '~design-system/shadows'; import space from '~design-system/spacing.json'; import * as typography from '~design-system/typography'; import * as designSystemUtils from '~design-system/utils'; -import * as utils from './utils'; +const lightTheme = { + typography, + colors: designSystemUtils.transformColors(colors), + radii: { ...radii, none: 0 }, + space: { ...space, none: 0 }, + sizes: { hairlineWidth: StyleSheet.hairlineWidth }, + fonts: designSystemUtils.getFonts(typography), + fontSizes: designSystemUtils.getFontSizes(typography), + fontWeights: designSystemUtils.getFontWeights(typography), + letterSpacings: designSystemUtils.getLetterSpacings(typography), // prettier-ignore + lineHeights: designSystemUtils.getLineHeights(typography), + shadows: designSystemUtils.getShadows(shadows), +}; -const { styled, css, createTheme, config, theme, useTheme, ThemeProvider } = - createStitches({ - theme: { - colors: designSystemUtils.transformColors(colors), - radii: { ...radii, none: 0 }, - space: { ...space, none: 0 }, - sizes: { hairlineWidth: StyleSheet.hairlineWidth }, - fonts: designSystemUtils.getFonts(typography), - fontSizes: designSystemUtils.getFontSizes(typography), - fontWeights: designSystemUtils.getFontWeights(typography), - letterSpacings: designSystemUtils.getLetterSpacings(typography), // prettier-ignore - lineHeights: designSystemUtils.getLineHeights(typography), - }, - utils, - }); +const appThemes = { + light: lightTheme, + dark: lightTheme, // TODO: Add dark theme when available +}; + +const breakpoints = { + xs: 0, + sm: 300, + md: 500, + lg: 800, + xl: 1200, +}; + +type AppBreakpoints = typeof breakpoints; +export type AppThemes = typeof appThemes; -export const darkTheme = createTheme({ - colors: designSystemUtils.transformColors(colors), // TODO: add dark theme support once we get dark mode colors from Figma -}); -export { styled, css, createTheme, useTheme, config, theme, ThemeProvider }; -export type CSS = Stitches.CSS; -export type Theme = typeof theme; export type Typography = keyof typeof typography; -export type Color = keyof Theme['colors']; -export type Space = keyof Theme['space']; -export type Radii = keyof Theme['radii']; -export type Fonts = keyof Theme['fonts']; -export type FontSize = keyof Theme['fontSizes']; -export type FontWeight = keyof Theme['fontWeights']; -export type LetterSpace = keyof Theme['letterSpacings']; -export type LineHeight = keyof Theme['lineHeights']; +export type Color = keyof AppThemes['light']['colors']; +export type Space = keyof AppThemes['light']['space']; +export type Radii = keyof AppThemes['light']['radii']; +export type Fonts = keyof AppThemes['light']['fonts']; +export type FontSize = keyof AppThemes['light']['fontSizes']; +export type FontWeight = keyof AppThemes['light']['fontWeights']; +export type LetterSpace = keyof AppThemes['light']['letterSpacings']; +export type LineHeight = keyof AppThemes['light']['lineHeights']; +declare module 'react-native-unistyles' { + export interface UnistylesThemes extends AppThemes {} + export interface UnistylesBreakpoints extends AppBreakpoints {} +} + +StyleSheet.configure({ + themes: appThemes, + breakpoints, + settings: { + initialTheme: 'light', + }, +}); diff --git a/src/styles/utils.ts b/src/styles/utils.ts index 606e43b1..01cc4443 100644 --- a/src/styles/utils.ts +++ b/src/styles/utils.ts @@ -1,49 +1,32 @@ -import { StyleSheet } from 'react-native'; -import type * as Stitches from 'stitches-native'; +import { StyleSheet, type ViewStyle } from 'react-native'; -import * as shadows from '~design-system/shadows'; import * as typographyTokens from '~design-system/typography'; import * as designSystemUtils from '~design-system/utils'; +import { type AppThemes } from './styled'; + type Typography = keyof typeof typographyTokens; +type FlexCenter = { + flexDirection: ViewStyle['flexDirection']; + justifyContent: ViewStyle['justifyContent']; + alignItems: ViewStyle['alignItems']; +}; -export const typography = (variant: Typography) => { +export const typography = (theme: AppThemes['light'], variant: Typography) => { const { fontWeight, textTransform } = typographyTokens[variant]; return { - fontFamily: `$${designSystemUtils.getFontFromWeight(fontWeight)}`, - fontSize: `$${variant}`, - fontWeight: `$${variant}`, - letterSpacing: `$${variant}`, - lineHeight: `$${variant}`, + fontFamily: `${designSystemUtils.getFontFromWeight(fontWeight)}`, + fontSize: theme.fontSizes[variant], + fontWeight: theme.fontWeights[variant], + letterSpacing: theme.letterSpacings[variant], + lineHeight: theme.lineHeights[variant], textTransform, - }; + } as designSystemUtils.TypographyDefinition; }; -export const size = (value: Stitches.PropertyValue<'width'>) => ({ - width: value, - height: value, -}); - -export const shadow = ( - level: 'none' | designSystemUtils.ShadowName -) => { - return { - none: { - elevation: 0, - shadowOffset: { width: 0, height: 0 }, - shadowRadius: 0, - shadowOpacity: 0, - shadowColor: '#000', - }, - ...designSystemUtils.getShadows(shadows), - }[level]; -}; - -export const flexCenter = ( - value?: Stitches.PropertyValue<'flexDirection'> -) => ({ - flexDirection: value || 'column', +export const flexCenter = (): FlexCenter => ({ + flexDirection: 'row', justifyContent: 'center', alignItems: 'center', }); @@ -51,3 +34,10 @@ export const flexCenter = ( export const absoluteFill = () => ({ ...StyleSheet.absoluteFillObject, }); + +export const getTypography = ( + theme: AppThemes['light'], + variant: Typography +): designSystemUtils.TypographyDefinition => { + return typography(theme, variant); +}; diff --git a/src/types/declarations.d.ts b/src/types/declarations.d.ts new file mode 100644 index 00000000..a21bda26 --- /dev/null +++ b/src/types/declarations.d.ts @@ -0,0 +1,14 @@ +declare module '*.jpg' { + const value: any; + export default value; +} + +declare module '*.png' { + const value: any; + export default value; +} + +declare module '*.ttf' { + const src: string; + export default src; +} diff --git a/src/utils/navigation.tsx b/src/utils/navigation.tsx index e2935873..07e7ac4b 100644 --- a/src/utils/navigation.tsx +++ b/src/utils/navigation.tsx @@ -6,8 +6,7 @@ import { import { type NativeStackNavigationOptions } from '@react-navigation/native-stack'; import { useNavigation } from 'expo-router'; import { useEffect } from 'react'; - -import { useTheme } from '~styles'; +import { useUnistyles } from 'react-native-unistyles'; export function getActiveRouteName( state: NavigationState | PartialState @@ -20,7 +19,7 @@ export function getActiveRouteName( export function useDefaultStackScreenOptions() { const { t } = useLingui(); - const theme = useTheme(); + const { theme } = useUnistyles(); const screenOptions: NativeStackNavigationOptions = { headerStyle: { diff --git a/tsconfig.json b/tsconfig.json index f2682572..9eea95b7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "strictNullChecks": true, "noImplicitAny": true, "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, "skipLibCheck": true }, "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] From 763d7942f0497d2885f8e4a887010683d60b64f9 Mon Sep 17 00:00:00 2001 From: Julien Texier Date: Fri, 25 Jul 2025 10:47:26 +0300 Subject: [PATCH 3/8] chore: update type declarations for image and font modules to improve type safety --- src/styles/styled.ts | 2 ++ src/types/declarations.d.ts | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/styles/styled.ts b/src/styles/styled.ts index 6e9f20ab..f11bd51b 100644 --- a/src/styles/styled.ts +++ b/src/styles/styled.ts @@ -47,7 +47,9 @@ export type FontWeight = keyof AppThemes['light']['fontWeights']; export type LetterSpace = keyof AppThemes['light']['letterSpacings']; export type LineHeight = keyof AppThemes['light']['lineHeights']; declare module 'react-native-unistyles' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface UnistylesThemes extends AppThemes {} + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface UnistylesBreakpoints extends AppBreakpoints {} } diff --git a/src/types/declarations.d.ts b/src/types/declarations.d.ts index a21bda26..9530fc6f 100644 --- a/src/types/declarations.d.ts +++ b/src/types/declarations.d.ts @@ -1,14 +1,14 @@ declare module '*.jpg' { - const value: any; + const value: ImageSourcePropType | undefined; export default value; } declare module '*.png' { - const value: any; + const value: ImageSourcePropType | undefined; export default value; } declare module '*.ttf' { - const src: string; + const src: FontSource; export default src; } From 72fb48187a34407cb3ea5d49fec6231dd5d579c1 Mon Sep 17 00:00:00 2001 From: Julien Texier Date: Fri, 25 Jul 2025 10:51:18 +0300 Subject: [PATCH 4/8] chore: remove stitches-native dependency --- package-lock.json | 19 ++----------------- package.json | 1 - 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c2140bb..b0d0ca70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,6 @@ "react-native-toast-message": "2.3.0", "react-native-unistyles": "3.0.7", "react-native-web": "~0.20.0", - "stitches-native": "0.4.0", "zeego": "3.0.6", "zustand": "5.0.4" }, @@ -12696,7 +12695,8 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/lodash.snakecase": { "version": "4.1.1", @@ -16310,21 +16310,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stitches-native": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/stitches-native/-/stitches-native-0.4.0.tgz", - "integrity": "sha512-thJ+FPiEhj6WHsphXRYTgeG4rnKHX86HU8rI4+/AFUAA4gUxJVDTjG0klbgrIGpp+eQa3PWrSrRJPcypPKFwTg==", - "dependencies": { - "lodash.merge": "4.6.2" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": ">=16", - "react-native": "*" - } - }, "node_modules/stream-buffers": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", diff --git a/package.json b/package.json index 0171b3cf..6b4d1bbe 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,6 @@ "react-native-toast-message": "2.3.0", "react-native-unistyles": "3.0.7", "react-native-web": "~0.20.0", - "stitches-native": "0.4.0", "zeego": "3.0.6", "zustand": "5.0.4" }, From 21b1ef0844a51d89ca423fafa4d186eeb66e32c3 Mon Sep 17 00:00:00 2001 From: Julien Texier Date: Fri, 25 Jul 2025 10:52:16 +0300 Subject: [PATCH 5/8] chore: remove unused useHeaderPlaygroundButton import from Home component --- src/app/(tabs)/home.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/(tabs)/home.tsx b/src/app/(tabs)/home.tsx index 9cf0a05d..1698c435 100644 --- a/src/app/(tabs)/home.tsx +++ b/src/app/(tabs)/home.tsx @@ -2,12 +2,9 @@ import { Trans } from '@lingui/react/macro'; import { ScrollView } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; -import { useHeaderPlaygroundButton } from '~components/playground/utils'; import { Text } from '~components/uikit'; export default function Home() { - useHeaderPlaygroundButton(); - return ( Date: Fri, 25 Jul 2025 16:21:36 +0300 Subject: [PATCH 6/8] chore: enhance unistyles migration --- eslint.config.js | 18 ++++++- src/app/(auth)/index.tsx | 2 +- src/app/(tabs)/_layout.tsx | 4 +- src/app/(tabs)/_layout.web.tsx | 3 +- src/app/playground/design-system.tsx | 10 +--- src/app/playground/index.tsx | 8 +-- src/app/playground/layout.tsx | 44 +++++++++------- src/app/playground/sandbox.tsx | 2 +- src/components/common/LoadingScreen.tsx | 5 +- src/components/common/MenuList.tsx | 14 +++-- src/components/common/SplashScreen.tsx | 13 +++-- src/components/playground/common.tsx | 2 +- .../store-review/ImprovementForm.tsx | 1 - src/components/uikit/PickerModal.tsx | 17 +++--- src/components/uikit/PickerSheet.tsx | 9 ++-- src/components/uikit/SegmentedControl.tsx | 10 ++-- src/components/uikit/Text.tsx | 5 +- src/components/uikit/buttons/Button.tsx | 4 +- src/components/uikit/buttons/IconButton.tsx | 4 +- src/components/uikit/inputs/Checkbox.tsx | 4 +- src/components/uikit/inputs/InputButton.tsx | 2 - src/components/uikit/inputs/Radio.tsx | 1 - src/components/uikit/inputs/SearchInput.tsx | 4 +- src/components/uikit/inputs/TextInput.tsx | 10 ++-- src/design-system/typography.ts | 52 +++++++++---------- src/design-system/utils.ts | 4 +- src/styles/styled.ts | 25 ++++++--- src/styles/utils.ts | 19 +++---- 28 files changed, 153 insertions(+), 143 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index c5b2aca4..dc5f87a5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -107,6 +107,7 @@ module.exports = defineConfig([ '*.startsWith', 'require', 'useState', + 'createElement', ], }, ], @@ -114,7 +115,7 @@ module.exports = defineConfig([ // Accessibility 'react-native-a11y/has-accessibility-hint': 'warn', - // Import sort/order + // Import sort/order etc. 'import/namespace': ['error', { allowComputed: true }], 'import/order': [ 'error', @@ -133,6 +134,21 @@ module.exports = defineConfig([ 'newlines-between': 'always', }, ], + 'no-restricted-imports': [ + 'error', + { + name: 'react-native', + importNames: ['StyleSheet'], + message: + 'Please import StyleSheet from unistyles instead of react-native.', + }, + { + name: 'react-native', + importNames: ['Text'], + message: + 'Please import Text from our design system instead of react-native.', + }, + ], }, }, diff --git a/src/app/(auth)/index.tsx b/src/app/(auth)/index.tsx index e93897be..bf6d1286 100644 --- a/src/app/(auth)/index.tsx +++ b/src/app/(auth)/index.tsx @@ -55,7 +55,7 @@ export default function Landing() { - + ✨ Start your journey ✨ diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index 7790e56e..b96aaa02 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -4,9 +4,9 @@ import { type BottomTabBarProps, } from '@react-navigation/bottom-tabs'; import { Tabs } from 'expo-router'; -import { Pressable, StyleSheet } from 'react-native'; +import { Pressable } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useUnistyles } from 'react-native-unistyles'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; import { BottomBar } from '~components/common/custom-bottom-bar/BottomBar'; import StoreReview from '~components/store-review/StoreReview'; diff --git a/src/app/(tabs)/_layout.web.tsx b/src/app/(tabs)/_layout.web.tsx index 773fa754..51a28808 100644 --- a/src/app/(tabs)/_layout.web.tsx +++ b/src/app/(tabs)/_layout.web.tsx @@ -1,7 +1,6 @@ import { useLingui } from '@lingui/react/macro'; import { Drawer } from 'expo-router/drawer'; -import { StyleSheet } from 'react-native'; -import { useUnistyles } from 'react-native-unistyles'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; import { Icon, type IconName } from '~components/uikit/Icon'; diff --git a/src/app/playground/design-system.tsx b/src/app/playground/design-system.tsx index fde878e6..5a296dd9 100644 --- a/src/app/playground/design-system.tsx +++ b/src/app/playground/design-system.tsx @@ -8,7 +8,6 @@ import * as colors from '~design-system/colors'; import * as radii from '~design-system/radii'; import spacing from '~design-system/spacing.json'; import * as typography from '~design-system/typography'; -import { flexCenter } from '~styles/utils'; const typographyNames = Object.keys(typography).sort(); const radiiEntries = Object.entries(radii).sort((a, b) => a[1] - b[1]); @@ -105,13 +104,7 @@ export default function DesignSystem() { accessible accessibilityLabel={`Radii token: ${name}, radii value: ${value} pixels`} > - + {value}px @@ -189,6 +182,7 @@ const styles = StyleSheet.create((theme) => ({ borderWidth: 1, borderColor: theme.colors.neutral3, backgroundColor: theme.colors.neutral5, + ...theme.utils.flexCenter, }, spacingBlock: { height: 24, diff --git a/src/app/playground/index.tsx b/src/app/playground/index.tsx index 48f4faea..5131f9ac 100644 --- a/src/app/playground/index.tsx +++ b/src/app/playground/index.tsx @@ -4,7 +4,6 @@ import { StyleSheet } from 'react-native-unistyles'; import MenuList, { Item } from '~components/common/MenuList'; import { IconButton, Stack, Text } from '~components/uikit'; -import { flexCenter } from '~styles/utils'; export default function PlaygroundPage() { const items: Item[] = [ @@ -45,11 +44,7 @@ export default function PlaygroundPage() { label: item.label, target: item.target, leftSlot: ( - + {item.label.slice(0, 2)} @@ -73,5 +68,6 @@ const styles = StyleSheet.create((theme) => ({ height: 40, borderRadius: theme.radii.regular, backgroundColor: theme.colors.infoMuted, + ...theme.utils.flexCenter, }, })); diff --git a/src/app/playground/layout.tsx b/src/app/playground/layout.tsx index 438fe993..dc3496d7 100644 --- a/src/app/playground/layout.tsx +++ b/src/app/playground/layout.tsx @@ -1,4 +1,4 @@ -import { ScrollView, View } from 'react-native'; +import { ScrollView, View, ViewProps } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; import { Note } from '~components/playground/common'; import { Grid, Spacer, Stack, Text } from '~components/uikit'; @@ -19,7 +19,7 @@ export default function Layout() { Stack - + Stack component is used to stack elements vertically or horizontally while applying uniform spacing between the elements. @@ -31,9 +31,9 @@ export default function Layout() { - - - + + + @@ -45,9 +45,9 @@ export default function Layout() { - - - + + + @@ -56,7 +56,7 @@ export default function Layout() { Spacer - + It‘s possible to intervine Spacer components within a Stack to apply a different spacing amount at specific places between elements. @@ -64,21 +64,21 @@ export default function Layout() { - + {` - + - - + + `.trim()} - + - - + + @@ -87,7 +87,7 @@ export default function Layout() { Grid - + A Grid component can be used for grid-like layouts. @@ -99,13 +99,13 @@ export default function Layout() { {Array.from({ length: 15 }).map((_, i) => ( - + ))} - + A number of columns can be provided to force the grid structure. By default the grid will just layout the children based on their instrictic size with the given spacing. @@ -119,7 +119,7 @@ export default function Layout() { {Array.from({ length: 15 }).map((_, i) => ( - + ))} @@ -130,6 +130,10 @@ export default function Layout() { ); } +const Box = (props: ViewProps & { fullWidth?: boolean }) => ( + +); + const styles = StyleSheet.create((theme) => ({ container: { flex: 1, diff --git a/src/app/playground/sandbox.tsx b/src/app/playground/sandbox.tsx index 9b31c243..76a29387 100644 --- a/src/app/playground/sandbox.tsx +++ b/src/app/playground/sandbox.tsx @@ -9,7 +9,7 @@ export default function Sandbox() { contentContainerStyle={styles.contentContainer} > - + You can play around with various components here if you don‘t want to add a new screen for them in the playground. diff --git a/src/components/common/LoadingScreen.tsx b/src/components/common/LoadingScreen.tsx index ce3c2550..62dc3b5b 100644 --- a/src/components/common/LoadingScreen.tsx +++ b/src/components/common/LoadingScreen.tsx @@ -1,13 +1,11 @@ import { ActivityIndicator, View } from 'react-native'; import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -import { flexCenter } from '~styles/utils'; - export default function LoadingScreen() { const { theme } = useUnistyles(); return ( - + ); @@ -17,5 +15,6 @@ const styles = StyleSheet.create((theme) => ({ wrapper: { flex: 1, backgroundColor: theme.colors.surface, + ...theme.utils.flexCenter, }, })); diff --git a/src/components/common/MenuList.tsx b/src/components/common/MenuList.tsx index 67b24bb3..d9ae7647 100644 --- a/src/components/common/MenuList.tsx +++ b/src/components/common/MenuList.tsx @@ -5,7 +5,6 @@ import { Platform, TouchableHighlight, View } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; import { Icon, Stack, Text } from '~components/uikit'; -import { flexCenter } from '~styles/utils'; import { haptics } from '~utils/haptics'; export type Item = { @@ -70,9 +69,7 @@ export default function MenuList({ items, title }: Props) { > {item.leftSlot ? ( - - {item.leftSlot} - + {item.leftSlot} ) : null} {item.rightSlot ? ( - - {item.rightSlot} - + {item.rightSlot} ) : ( <> {item.currentValue !== undefined && @@ -108,7 +103,7 @@ export default function MenuList({ items, title }: Props) { {item.checked !== undefined && ( <> {item.checked ? ( - + ) : ( @@ -166,15 +161,18 @@ const styles = StyleSheet.create((theme) => ({ }, leftSlot: { paddingVertical: theme.space.small, + ...theme.utils.flexCenter, }, rightSlot: { minHeight: 24, + ...theme.utils.flexCenter, }, checkCircle: { width: 24, height: 24, borderRadius: theme.radii.full, backgroundColor: theme.colors.info, + ...theme.utils.flexCenter, }, checkOutline: { width: 24, diff --git a/src/components/common/SplashScreen.tsx b/src/components/common/SplashScreen.tsx index a0e64f86..720e676b 100644 --- a/src/components/common/SplashScreen.tsx +++ b/src/components/common/SplashScreen.tsx @@ -3,7 +3,6 @@ import { Image, View } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; import config from '~constants/config'; -import { absoluteFill } from '~styles/utils'; import Splash from '../../design-system/assets/splash.png'; @@ -18,7 +17,10 @@ export default function SplashScreen() { ({ wrapper: { flex: 1, }, + splashContent: { + ...theme.utils.absoluteFill, + }, splashImage: { width: '100%', height: '100%', resizeMode: 'contain', }, -}); +})); diff --git a/src/components/playground/common.tsx b/src/components/playground/common.tsx index 817b7e4f..3da1c813 100644 --- a/src/components/playground/common.tsx +++ b/src/components/playground/common.tsx @@ -19,7 +19,7 @@ export function Note({ children }: { children: ReactNode }) { - + {children} diff --git a/src/components/store-review/ImprovementForm.tsx b/src/components/store-review/ImprovementForm.tsx index 746d1bba..7cd2a16f 100644 --- a/src/components/store-review/ImprovementForm.tsx +++ b/src/components/store-review/ImprovementForm.tsx @@ -26,7 +26,6 @@ export default function ImprovementForm({ }); const [isFocused, setFocused] = useState(false); - // Controls the visual styles based on the input state styles.useVariants({ focused: isFocused, }); diff --git a/src/components/uikit/PickerModal.tsx b/src/components/uikit/PickerModal.tsx index ea496450..2556c373 100644 --- a/src/components/uikit/PickerModal.tsx +++ b/src/components/uikit/PickerModal.tsx @@ -14,7 +14,6 @@ import { import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { StyleSheet } from 'react-native-unistyles'; -import { absoluteFill, flexCenter } from '~styles/utils'; import { announceForAccessibility } from '~utils/a11y'; import { Text } from './Text'; @@ -144,11 +143,7 @@ function PickerLayout({ @@ -299,7 +294,7 @@ function MultiplePicker({ @@ -308,7 +303,7 @@ function MultiplePicker({ @@ -327,8 +322,9 @@ const styles = StyleSheet.create((theme) => ({ justifyContent: 'flex-end', }, backdrop: { - backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + ...theme.utils.absoluteFill, }, content: { backgroundColor: theme.colors.surface, @@ -345,5 +341,6 @@ const styles = StyleSheet.create((theme) => ({ flex: 1, paddingTop: theme.space.regular, paddingBottom: theme.space.medium, + ...theme.utils.flexCenter, }, })); diff --git a/src/components/uikit/PickerSheet.tsx b/src/components/uikit/PickerSheet.tsx index dcc92f64..9f3d36e4 100644 --- a/src/components/uikit/PickerSheet.tsx +++ b/src/components/uikit/PickerSheet.tsx @@ -11,7 +11,6 @@ import { import { StyleSheet } from 'react-native-unistyles'; import StatusBar from '~components/common/StatusBar'; -import { flexCenter } from '~styles/utils'; import { useEffectEvent } from '~utils/common'; import { Text } from './Text'; @@ -164,7 +163,7 @@ function ModalContent({ + {children || ( @@ -315,6 +314,7 @@ const styles = StyleSheet.create((theme) => ({ backgroundColor: theme.colors.surface, borderBottomWidth: 1, borderColor: theme.colors.line3, + ...theme.utils.flexCenter, }, clearButton: { alignSelf: 'flex-end', @@ -332,5 +332,6 @@ const styles = StyleSheet.create((theme) => ({ }, actionButton: { padding: theme.space.regular, + ...theme.utils.flexCenter, }, })); diff --git a/src/components/uikit/SegmentedControl.tsx b/src/components/uikit/SegmentedControl.tsx index a0b6d50c..9ab1f943 100644 --- a/src/components/uikit/SegmentedControl.tsx +++ b/src/components/uikit/SegmentedControl.tsx @@ -14,7 +14,6 @@ import Animated, { } from 'react-native-reanimated'; import { StyleSheet } from 'react-native-unistyles'; -import { absoluteFill } from '~styles/utils'; import { haptics } from '~utils/haptics'; import { Text } from './Text'; @@ -69,11 +68,7 @@ function Segments({ return ( <> {segments.map((segment, index) => { @@ -158,8 +153,9 @@ const styles = StyleSheet.create((theme) => ({ borderRadius: 10, }, segmentBackground: { - backgroundColor: 'rgba(150, 150, 150, 0.15)', borderRadius: 8, + backgroundColor: 'rgba(150, 150, 150, 0.15)', + ...theme.utils.absoluteFill, }, segmentButton: { position: 'relative', diff --git a/src/components/uikit/Text.tsx b/src/components/uikit/Text.tsx index f091ba89..0176ea39 100644 --- a/src/components/uikit/Text.tsx +++ b/src/components/uikit/Text.tsx @@ -1,11 +1,12 @@ import { Text as RNText } from 'react-native'; import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -import { type Color, type Typography } from '~styles/styled'; +import type * as typographyTokens from '~design-system/typography'; +import { type Color } from '~styles/styled'; import { getTypography } from '~styles/utils'; type TextProps = RNText['props'] & { - variant?: Typography; + variant?: keyof typeof typographyTokens; align?: 'left' | 'right' | 'center'; uppercase?: boolean; color?: Color; diff --git a/src/components/uikit/buttons/Button.tsx b/src/components/uikit/buttons/Button.tsx index 099c28e6..b99a60d0 100644 --- a/src/components/uikit/buttons/Button.tsx +++ b/src/components/uikit/buttons/Button.tsx @@ -5,7 +5,7 @@ import { } from 'react-native'; import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -import { type Typography } from '~styles/styled'; +import { type TypographyToken } from '~design-system/typography'; import { haptics } from '~utils/haptics'; import { Icon } from '../Icon'; @@ -97,7 +97,7 @@ export function Button({ ); } -const sizeToTextVariant: Record = { +const sizeToTextVariant: Record = { small: 'bodyExtraSmallBold', normal: 'bodySmallBold', large: 'bodySemiBold', diff --git a/src/components/uikit/buttons/IconButton.tsx b/src/components/uikit/buttons/IconButton.tsx index 749c03d3..9efa0d18 100644 --- a/src/components/uikit/buttons/IconButton.tsx +++ b/src/components/uikit/buttons/IconButton.tsx @@ -12,7 +12,6 @@ import Animated, { } from 'react-native-reanimated'; import { StyleSheet, useUnistyles } from 'react-native-unistyles'; -import { flexCenter } from '~styles/utils'; import { haptics } from '~utils/haptics'; import { Icon } from '../Icon'; @@ -98,7 +97,7 @@ export function IconButton({ onPressOut={handlePressOut} disabled={disabled} onPress={_onPress} - style={[styles.wrapper, wrapperStyle, flexCenter()]} + style={[styles.wrapper, wrapperStyle]} accessibilityRole={accessibilityRole ?? 'button'} accessibilityLabel={accessibilityLabel ?? t`Icon button with ${icon} icon`} // prettier-ignore accessibilityHint={accessibilityHint ?? t`Double tap to perform action`} // prettier-ignore @@ -121,6 +120,7 @@ const styles = StyleSheet.create((theme) => ({ wrapper: { borderRadius: theme.radii.medium, position: 'relative', + ...theme.utils.flexCenter, variants: { size: { small: { diff --git a/src/components/uikit/inputs/Checkbox.tsx b/src/components/uikit/inputs/Checkbox.tsx index 261727df..43f631a3 100644 --- a/src/components/uikit/inputs/Checkbox.tsx +++ b/src/components/uikit/inputs/Checkbox.tsx @@ -7,7 +7,6 @@ import Animated, { } from 'react-native-reanimated'; import { StyleSheet } from 'react-native-unistyles'; -import { flexCenter } from '~styles/utils'; import { haptics } from '~utils/haptics'; import { Icon } from '../Icon'; @@ -57,7 +56,7 @@ export function Checkbox({ onChange, checked, value, label }: Props) { : t`Double tap to uncheck this option` } > - + @@ -82,6 +81,7 @@ const styles = StyleSheet.create((theme) => ({ borderWidth: PixelRatio.roundToNearestPixel(1.5), // try to match marginRight: theme.space.small, borderColor: theme.colors.text, + ...theme.utils.flexCenter, variants: { checked: { true: { diff --git a/src/components/uikit/inputs/InputButton.tsx b/src/components/uikit/inputs/InputButton.tsx index 07ad0549..87038d53 100644 --- a/src/components/uikit/inputs/InputButton.tsx +++ b/src/components/uikit/inputs/InputButton.tsx @@ -44,7 +44,6 @@ export function InputButton({ }: Props) { const { t } = useLingui(); - // Controls the visual styles based on the input state styles.useVariants({ focused: isFocused, valid: isValid, @@ -80,7 +79,6 @@ export function InputButton({ > ( accessibilityLabel={option} accessibilityHint={t`Double tap to select this suggestion`} > - + {option} @@ -103,7 +103,7 @@ const styles = StyleSheet.create((theme) => ({ padding: theme.space.small, borderRadius: theme.radii.regular, backgroundColor: theme.colors.surface, - borderWidth: 0.5, + borderWidth: StyleSheet.hairlineWidth, borderColor: theme.colors.line3, ...theme.shadows.medium, }, diff --git a/src/components/uikit/inputs/TextInput.tsx b/src/components/uikit/inputs/TextInput.tsx index 66efcab5..fb4caa8a 100644 --- a/src/components/uikit/inputs/TextInput.tsx +++ b/src/components/uikit/inputs/TextInput.tsx @@ -70,7 +70,6 @@ export const TextInput = forwardRef( const inputRef = useRef(null); useImperativeHandle(ref, () => inputRef.current as RNTextInput); - // Controls the visual styles based on the input state styles.useVariants({ valid: isValid, disabled: isDisabled, @@ -128,15 +127,16 @@ export const TextInput = forwardRef( )} {showCharacterLimit && isFocused && ( - {characterCount} - {` / ${maxLength}`} - + / {maxLength} + )} diff --git a/src/design-system/typography.ts b/src/design-system/typography.ts index 0b29d380..7b5220df 100644 --- a/src/design-system/typography.ts +++ b/src/design-system/typography.ts @@ -1,6 +1,4 @@ -import { TypographyDefinition } from './utils'; - -export const body: TypographyDefinition = { +export const body = { fontFamily: 'Inter', fontWeight: 400, fontSize: 16, @@ -8,7 +6,7 @@ export const body: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodyBold: TypographyDefinition = { +export const bodyBold = { fontFamily: 'Inter', fontWeight: 700, fontSize: 16, @@ -16,7 +14,7 @@ export const bodyBold: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodyExtraSmall: TypographyDefinition = { +export const bodyExtraSmall = { fontFamily: 'Inter', fontWeight: 400, fontSize: 12, @@ -24,7 +22,7 @@ export const bodyExtraSmall: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodyExtraSmallBold: TypographyDefinition = { +export const bodyExtraSmallBold = { fontFamily: 'Inter', fontWeight: 700, fontSize: 12, @@ -32,7 +30,7 @@ export const bodyExtraSmallBold: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodyLarge: TypographyDefinition = { +export const bodyLarge = { fontFamily: 'Inter', fontWeight: 400, fontSize: 18, @@ -40,7 +38,7 @@ export const bodyLarge: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.556, }; -export const bodyLargeBold: TypographyDefinition = { +export const bodyLargeBold = { fontFamily: 'Inter', fontWeight: 700, fontSize: 18, @@ -48,7 +46,7 @@ export const bodyLargeBold: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.556, }; -export const bodySemiBold: TypographyDefinition = { +export const bodySemiBold = { fontFamily: 'Inter', fontWeight: 600, fontSize: 16, @@ -56,7 +54,7 @@ export const bodySemiBold: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodySmall: TypographyDefinition = { +export const bodySmall = { fontFamily: 'Inter', fontWeight: 400, fontSize: 14, @@ -64,7 +62,7 @@ export const bodySmall: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodySmallBold: TypographyDefinition = { +export const bodySmallBold = { fontFamily: 'Inter', fontWeight: 700, fontSize: 14, @@ -72,7 +70,7 @@ export const bodySmallBold: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const bodySmallSemiBold: TypographyDefinition = { +export const bodySmallSemiBold = { fontFamily: 'Inter', fontWeight: 600, fontSize: 14, @@ -80,7 +78,7 @@ export const bodySmallSemiBold: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const displayExtraSmall: TypographyDefinition = { +export const displayExtraSmall = { fontFamily: 'Inter', fontWeight: 500, fontSize: 32, @@ -88,7 +86,7 @@ export const displayExtraSmall: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const displayLarge: TypographyDefinition = { +export const displayLarge = { fontFamily: 'Inter', fontWeight: 500, fontSize: 64, @@ -96,7 +94,7 @@ export const displayLarge: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const displaySmall: TypographyDefinition = { +export const displaySmall = { fontFamily: 'Inter', fontWeight: 500, fontSize: 48, @@ -104,7 +102,7 @@ export const displaySmall: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingL: TypographyDefinition = { +export const headingL = { fontFamily: 'Inter', fontWeight: 700, fontSize: 28, @@ -112,7 +110,7 @@ export const headingL: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingM: TypographyDefinition = { +export const headingM = { fontFamily: 'Inter', fontWeight: 600, fontSize: 22, @@ -120,7 +118,7 @@ export const headingM: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingS: TypographyDefinition = { +export const headingS = { fontFamily: 'Inter', fontWeight: 600, fontSize: 16, @@ -128,7 +126,7 @@ export const headingS: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingXl: TypographyDefinition = { +export const headingXl = { fontFamily: 'Inter', fontWeight: 700, fontSize: 42, @@ -136,7 +134,7 @@ export const headingXl: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const headingXxl: TypographyDefinition = { +export const headingXxl = { fontFamily: 'Inter', fontWeight: 700, fontSize: 72, @@ -144,7 +142,7 @@ export const headingXxl: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const label: TypographyDefinition = { +export const label = { fontFamily: 'Inter', fontWeight: 500, fontSize: 14, @@ -152,7 +150,7 @@ export const label: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const lead: TypographyDefinition = { +export const lead = { fontFamily: 'Inter', fontWeight: 400, fontSize: 28, @@ -160,7 +158,7 @@ export const lead: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const leadBold: TypographyDefinition = { +export const leadBold = { fontFamily: 'Inter', fontWeight: 700, fontSize: 28, @@ -168,7 +166,7 @@ export const leadBold: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const linkText: TypographyDefinition = { +export const linkText = { fontFamily: 'Inter', fontWeight: 600, fontSize: 16, @@ -176,7 +174,7 @@ export const linkText: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const linkTextHover: TypographyDefinition = { +export const linkTextHover = { fontFamily: 'Inter', fontWeight: 600, fontSize: 16, @@ -184,7 +182,7 @@ export const linkTextHover: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const overlineRegular: TypographyDefinition = { +export const overlineRegular = { fontFamily: 'Inter', fontWeight: 700, fontSize: 14, @@ -192,7 +190,7 @@ export const overlineRegular: TypographyDefinition = { letterSpacing: 0, lineHeight: 1.5, }; -export const overlineSmall: TypographyDefinition = { +export const overlineSmall = { fontFamily: 'Inter', fontWeight: 700, fontSize: 12, diff --git a/src/design-system/utils.ts b/src/design-system/utils.ts index 8f6b24c5..700498c7 100644 --- a/src/design-system/utils.ts +++ b/src/design-system/utils.ts @@ -13,9 +13,7 @@ const WEIGHT_TO_FONT = { 900: 'Black', } as const; -export function getFontFromWeight( - weight: TextStyle['fontWeight'] -): FontWeightVar { +export function getFontFromWeight(weight: number): FontWeightVar { return WEIGHT_TO_FONT[weight as FontWeightNum].toLowerCase() as FontWeightVar; } diff --git a/src/styles/styled.ts b/src/styles/styled.ts index f11bd51b..edd81b90 100644 --- a/src/styles/styled.ts +++ b/src/styles/styled.ts @@ -7,18 +7,26 @@ import space from '~design-system/spacing.json'; import * as typography from '~design-system/typography'; import * as designSystemUtils from '~design-system/utils'; +import { absoluteFill, flexCenter } from './utils'; + +const typographyTokens = typography as Typography; + const lightTheme = { - typography, + typography: typographyTokens, colors: designSystemUtils.transformColors(colors), radii: { ...radii, none: 0 }, space: { ...space, none: 0 }, sizes: { hairlineWidth: StyleSheet.hairlineWidth }, - fonts: designSystemUtils.getFonts(typography), - fontSizes: designSystemUtils.getFontSizes(typography), - fontWeights: designSystemUtils.getFontWeights(typography), - letterSpacings: designSystemUtils.getLetterSpacings(typography), // prettier-ignore - lineHeights: designSystemUtils.getLineHeights(typography), + fonts: designSystemUtils.getFonts(typographyTokens), + fontSizes: designSystemUtils.getFontSizes(typographyTokens), + fontWeights: designSystemUtils.getFontWeights(typographyTokens), + letterSpacings: designSystemUtils.getLetterSpacings(typographyTokens), // prettier-ignore + lineHeights: designSystemUtils.getLineHeights(typographyTokens), shadows: designSystemUtils.getShadows(shadows), + utils: { + absoluteFill, + flexCenter, + }, }; const appThemes = { @@ -37,7 +45,10 @@ const breakpoints = { type AppBreakpoints = typeof breakpoints; export type AppThemes = typeof appThemes; -export type Typography = keyof typeof typography; +export type Typography = Record< + T, + designSystemUtils.TypographyDefinition +>; export type Color = keyof AppThemes['light']['colors']; export type Space = keyof AppThemes['light']['space']; export type Radii = keyof AppThemes['light']['radii']; diff --git a/src/styles/utils.ts b/src/styles/utils.ts index 01cc4443..2c352eea 100644 --- a/src/styles/utils.ts +++ b/src/styles/utils.ts @@ -1,18 +1,21 @@ -import { StyleSheet, type ViewStyle } from 'react-native'; +import { type ViewStyle } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; import * as typographyTokens from '~design-system/typography'; import * as designSystemUtils from '~design-system/utils'; import { type AppThemes } from './styled'; -type Typography = keyof typeof typographyTokens; type FlexCenter = { flexDirection: ViewStyle['flexDirection']; justifyContent: ViewStyle['justifyContent']; alignItems: ViewStyle['alignItems']; }; -export const typography = (theme: AppThemes['light'], variant: Typography) => { +export const typography = ( + theme: AppThemes['light'], + variant: typographyTokens.TypographyToken +) => { const { fontWeight, textTransform } = typographyTokens[variant]; return { @@ -25,19 +28,17 @@ export const typography = (theme: AppThemes['light'], variant: Typography) => { } as designSystemUtils.TypographyDefinition; }; -export const flexCenter = (): FlexCenter => ({ +export const flexCenter: FlexCenter = { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', -}); +}; -export const absoluteFill = () => ({ - ...StyleSheet.absoluteFillObject, -}); +export const absoluteFill = StyleSheet.absoluteFillObject; export const getTypography = ( theme: AppThemes['light'], - variant: Typography + variant: typographyTokens.TypographyToken ): designSystemUtils.TypographyDefinition => { return typography(theme, variant); }; From 6c2757e15608eb8c20b5609b045956f2a75dbca1 Mon Sep 17 00:00:00 2001 From: Julien Texier Date: Mon, 28 Jul 2025 08:26:13 +0300 Subject: [PATCH 7/8] chore: refactor Text component for optimized performance --- src/components/uikit/Text.tsx | 57 ++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/components/uikit/Text.tsx b/src/components/uikit/Text.tsx index 0176ea39..76c3908b 100644 --- a/src/components/uikit/Text.tsx +++ b/src/components/uikit/Text.tsx @@ -1,11 +1,12 @@ -import { Text as RNText } from 'react-native'; +import { type ComponentType, createElement, forwardRef } from 'react'; +import type { TextProps as RNTextProps } from 'react-native'; import { StyleSheet, useUnistyles } from 'react-native-unistyles'; import type * as typographyTokens from '~design-system/typography'; import { type Color } from '~styles/styled'; import { getTypography } from '~styles/utils'; -type TextProps = RNText['props'] & { +type TextProps = RNTextProps & { variant?: keyof typeof typographyTokens; align?: 'left' | 'right' | 'center'; uppercase?: boolean; @@ -13,6 +14,36 @@ type TextProps = RNText['props'] & { children: React.ReactNode; }; +const LeanText = forwardRef((props, ref) => { + return createElement('RCTText', { ...props, ref }); +}) as ComponentType; + +LeanText.displayName = 'LeanText'; + +export function Text({ + variant = 'body', + align = 'left', + uppercase = false, + color, + children, + style, + ...props +}: TextProps) { + const { theme } = useUnistyles(); + const typographyStyle = getTypography(theme, variant); + + styles.useVariants({ + align, + uppercase, + color, + }); + return ( + + {children} + + ); +} + const styles = StyleSheet.create((theme) => ({ text: { color: theme.colors.text, @@ -35,25 +66,3 @@ const styles = StyleSheet.create((theme) => ({ }, }, })); - -export function Text({ - variant = 'body', - align = 'left', - uppercase = false, - color, - children, - ...props -}: TextProps) { - const { theme } = useUnistyles(); - styles.useVariants({ - align, - uppercase, - color, - }); - - return ( - - {children} - - ); -} From a556796c05c9f22c27f7b82777ac651a548cdf41 Mon Sep 17 00:00:00 2001 From: Julien Texier Date: Mon, 28 Jul 2025 08:42:56 +0300 Subject: [PATCH 8/8] chore: small unistyles improvements --- src/app/(auth)/signup.tsx | 11 +++++++++-- src/components/uikit/ProgressBar.tsx | 2 +- src/components/uikit/Text.tsx | 18 +++++++++++------- src/components/uikit/buttons/helpers.ts | 9 ++++----- src/components/uikit/inputs/Checkbox.tsx | 2 +- src/styles/styled.ts | 1 + 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/app/(auth)/signup.tsx b/src/app/(auth)/signup.tsx index 0df7a2c9..28418ebf 100644 --- a/src/app/(auth)/signup.tsx +++ b/src/app/(auth)/signup.tsx @@ -49,11 +49,15 @@ export default function Signup() { } return ( - + @@ -252,6 +256,9 @@ const styles = StyleSheet.create((theme) => ({ container: { flex: 1, }, + contentStyle: { + flexGrow: 1, + }, innerStack: { padding: theme.space.medium, flex: 1, diff --git a/src/components/uikit/ProgressBar.tsx b/src/components/uikit/ProgressBar.tsx index 877186ac..e4049729 100644 --- a/src/components/uikit/ProgressBar.tsx +++ b/src/components/uikit/ProgressBar.tsx @@ -38,7 +38,7 @@ export function ProgressBar({ progressAnim.value = withTiming(progress, { duration: animated ? 200 : 0, }); - }, [animated, progress, progressAnim, step]); + }, [step]); // eslint-disable-line react-hooks/exhaustive-deps const animatedStyle = useAnimatedStyle(() => { return { diff --git a/src/components/uikit/Text.tsx b/src/components/uikit/Text.tsx index 76c3908b..10e70a9c 100644 --- a/src/components/uikit/Text.tsx +++ b/src/components/uikit/Text.tsx @@ -1,10 +1,10 @@ import { type ComponentType, createElement, forwardRef } from 'react'; import type { TextProps as RNTextProps } from 'react-native'; -import { StyleSheet, useUnistyles } from 'react-native-unistyles'; +import { StyleSheet } from 'react-native-unistyles'; import type * as typographyTokens from '~design-system/typography'; import { type Color } from '~styles/styled'; -import { getTypography } from '~styles/utils'; +import { typography } from '~styles/utils'; type TextProps = RNTextProps & { variant?: keyof typeof typographyTokens; @@ -29,16 +29,15 @@ export function Text({ style, ...props }: TextProps) { - const { theme } = useUnistyles(); - const typographyStyle = getTypography(theme, variant); - styles.useVariants({ align, uppercase, color, + variant, }); + return ( - + {children} ); @@ -46,8 +45,13 @@ export function Text({ const styles = StyleSheet.create((theme) => ({ text: { - color: theme.colors.text, variants: { + variant: Object.fromEntries( + Object.keys(theme.typography).map((key) => [ + key, + typography(theme, key as typographyTokens.TypographyToken), + ]) + ), color: Object.fromEntries( Object.entries(theme.colors).map(([key, value]) => [ key, diff --git a/src/components/uikit/buttons/helpers.ts b/src/components/uikit/buttons/helpers.ts index 33b0005e..40597c62 100644 --- a/src/components/uikit/buttons/helpers.ts +++ b/src/components/uikit/buttons/helpers.ts @@ -1,7 +1,6 @@ import { type StyleProp, type ViewStyle } from 'react-native'; -import { type UnistylesThemes } from 'react-native-unistyles'; -import { type Color } from '~styles/styled'; +import { type Color, type Theme } from '~styles/styled'; import { type ButtonProps, @@ -16,7 +15,7 @@ const getBaseStyle = ({ color = 'primary', disabled = false, }: Pick & { - theme: UnistylesThemes['light']; + theme: Theme; }): ViewStyle => { const baseStyle: ViewStyle = { backgroundColor: 'transparent', @@ -102,7 +101,7 @@ export const getButtonWrapperStyle = ({ color = 'primary', disabled = false, }: Pick & { - theme: UnistylesThemes['light']; + theme: Theme; }): StyleProp => { return getBaseStyle({ variant, color, disabled, theme }); }; @@ -114,7 +113,7 @@ export const getIconWrapperStyle = ({ color = 'primary', disabled = false, }: Pick & { - theme: UnistylesThemes['light']; + theme: Theme; }): StyleProp => { if (color === 'neutral') { return { backgroundColor: 'transparent' }; diff --git a/src/components/uikit/inputs/Checkbox.tsx b/src/components/uikit/inputs/Checkbox.tsx index 43f631a3..f39b07c8 100644 --- a/src/components/uikit/inputs/Checkbox.tsx +++ b/src/components/uikit/inputs/Checkbox.tsx @@ -78,7 +78,7 @@ const styles = StyleSheet.create((theme) => ({ height: 24, backgroundColor: 'transparent', borderRadius: theme.radii.regular, - borderWidth: PixelRatio.roundToNearestPixel(1.5), // try to match + borderWidth: PixelRatio.roundToNearestPixel(1.5), // try to match with icon width marginRight: theme.space.small, borderColor: theme.colors.text, ...theme.utils.flexCenter, diff --git a/src/styles/styled.ts b/src/styles/styled.ts index edd81b90..ae2825e5 100644 --- a/src/styles/styled.ts +++ b/src/styles/styled.ts @@ -44,6 +44,7 @@ const breakpoints = { type AppBreakpoints = typeof breakpoints; export type AppThemes = typeof appThemes; +export type Theme = AppThemes[keyof AppThemes]; export type Typography = Record< T,