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/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/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..b0d0ca70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,19 +44,21 @@ "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", "zustand": "5.0.4" }, @@ -9122,6 +9124,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 +9234,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", @@ -12675,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", @@ -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", @@ -16258,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 4a65e7d4..6b4d1bbe 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,19 +76,21 @@ "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", "zustand": "5.0.4" }, 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..bf6d1286 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..28418ebf 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,17 @@ export default function Signup() { } return ( - - + + @@ -238,21 +247,20 @@ export default function Signup() { > Signup - - + + ); } -const InnerStack = styled(Stack, { - padding: '$medium', - flex: 1, -}); - -const KeyboardAwareView = styled(KeyboardAwareScrollView, { - flex: 1, -}).attrs(() => ({ - keyboardShouldPersistTaps: 'handled', - contentContainerStyle: { +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + }, + contentStyle: { flexGrow: 1, }, + innerStack: { + padding: theme.space.medium, + flex: 1, + }, })); diff --git a/src/app/(tabs)/_layout.tsx b/src/app/(tabs)/_layout.tsx index 614168d3..b96aaa02 100644 --- a/src/app/(tabs)/_layout.tsx +++ b/src/app/(tabs)/_layout.tsx @@ -4,14 +4,14 @@ 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 { StyleSheet, 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..51a28808 100644 --- a/src/app/(tabs)/_layout.web.tsx +++ b/src/app/(tabs)/_layout.web.tsx @@ -1,16 +1,14 @@ import { useLingui } from '@lingui/react/macro'; import { Drawer } from 'expo-router/drawer'; -import { StyleSheet } from 'react-native'; +import { StyleSheet, 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..1698c435 100644 --- a/src/app/(tabs)/home.tsx +++ b/src/app/(tabs)/home.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 Home() { 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..5a296dd9 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,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 { styled, themeProp } from '~styles'; const typographyNames = Object.keys(typography).sort(); const radiiEntries = Object.entries(radii).sort((a, b) => a[1] - b[1]); @@ -14,7 +15,10 @@ const spacingEntries = Object.entries(spacing).sort((a, b) => a[1] - b[1]); export default function DesignSystem() { return ( - + - + {startCase(colorName)} @@ -73,9 +77,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 +104,11 @@ export default function DesignSystem() { accessible accessibilityLabel={`Radii token: ${name}, radii value: ${value} pixels`} > - + {value}px - + {startCase(name)} ))} @@ -129,7 +133,10 @@ export default function DesignSystem() { {startCase(name)} - + {value}px @@ -144,51 +151,44 @@ 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, + ...theme.utils.flexCenter, + }, + 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..5131f9ac 100644 --- a/src/app/playground/index.tsx +++ b/src/app/playground/index.tsx @@ -1,15 +1,16 @@ -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'; 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 +22,11 @@ export default function PlaygroundPage() { if (__DEV__) items.push({ id: 'sandbox', target: '/playground/sandbox', label: 'Sandbox' }); // prettier-ignore return ( - - + ( @@ -40,30 +44,30 @@ 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, + ...theme.utils.flexCenter, }, })); - -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..dc3496d7 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, ViewProps } 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 @@ -17,12 +19,12 @@ export default function Layout() { Stack - + Stack component is used to stack elements vertically or horizontally while applying uniform spacing between the elements. - + {`...`} @@ -34,9 +36,9 @@ export default function Layout() { - + - + {`...`} @@ -48,21 +50,21 @@ 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. - + - + {` @@ -79,17 +81,17 @@ export default function Layout() { - + Grid - + A Grid component can be used for grid-like layouts. - + {`...`} @@ -101,15 +103,15 @@ export default function Layout() { ))} - + - + 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. - + {`...`} @@ -117,38 +119,41 @@ 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 Box = (props: ViewProps & { fullWidth?: boolean }) => ( + +); + +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..76a29387 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 00000000..b1a94d47 Binary files /dev/null and b/src/assets/web_landing_background.jpg differ 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..62dc3b5b 100644 --- a/src/components/common/LoadingScreen.tsx +++ b/src/components/common/LoadingScreen.tsx @@ -1,19 +1,20 @@ -import { ActivityIndicator } from 'react-native'; - -import { styled, useTheme } from '~styles'; +import { ActivityIndicator, View } from 'react-native'; +import { StyleSheet, useUnistyles } from 'react-native-unistyles'; 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, + ...theme.utils.flexCenter, + }, +})); diff --git a/src/components/common/MenuList.tsx b/src/components/common/MenuList.tsx index 8da66e5a..d9ae7647 100644 --- a/src/components/common/MenuList.tsx +++ b/src/components/common/MenuList.tsx @@ -1,10 +1,10 @@ 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 { haptics } from '~utils/haptics'; export type Item = { @@ -45,18 +45,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 +67,47 @@ 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 +117,68 @@ 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, + ...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, + 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((theme) => ({ + wrapper: { + flex: 1, + }, + splashContent: { + ...theme.utils.absoluteFill, + }, + 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..3da1c813 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 ( - + @@ -14,18 +19,20 @@ 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..7cd2a16f 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,10 @@ export default function ImprovementForm({ }); const [isFocused, setFocused] = useState(false); + styles.useVariants({ + focused: isFocused, + }); + async function onSubmitFeedback() { try { await sleep(2000); @@ -49,7 +53,7 @@ export default function ImprovementForm({ justify="between" style={{ height: '100%' }} > - + How can we improve? @@ -66,8 +70,9 @@ export default function ImprovementForm({ rules={{ required: t`Feedback is required` }} render={({ field }) => { return ( - - + setFocused(true)} onBlur={() => setFocused(false)} /> - + ); }} /> @@ -98,38 +103,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..2556c373 100644 --- a/src/components/uikit/PickerModal.tsx +++ b/src/components/uikit/PickerModal.tsx @@ -6,12 +6,14 @@ 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 { announceForAccessibility } from '~utils/a11y'; import { Text } from './Text'; @@ -138,27 +140,32 @@ function PickerLayout({ const insets = useSafeAreaInsets(); return ( - + - + - {children} - - + + ); } @@ -213,13 +220,17 @@ function SinglePicker({ /> -
- + + Close - -
+ +
); } @@ -281,53 +292,55 @@ function MultiplePicker({ /> -
- + + Cancel - - + + Done - -
+ + ); } -const Wrapper = styled('View', { - flex: 1, - justifyContent: 'flex-end', -}); - -const Backdrop = Animated.createAnimatedComponent( - styled('View', { - absoluteFill: true, - backgroundColor: 'rgba(0, 0, 0, 0.5)', +const styles = StyleSheet.create((theme) => ({ + wrapper: { + flex: 1, + justifyContent: 'flex-end', + }, + backdrop: { zIndex: 1, - }) -); - -const Content = Animated.createAnimatedComponent( - styled('View', { - backgroundColor: '$surface', - shadow: 'large', - padding: '$medium', - borderTopLeftRadius: '$medium', - borderTopRightRadius: '$medium', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + ...theme.utils.absoluteFill, + }, + 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, + ...theme.utils.flexCenter, + }, +})); diff --git a/src/components/uikit/PickerSheet.tsx b/src/components/uikit/PickerSheet.tsx index b31e3f97..9f3d36e4 100644 --- a/src/components/uikit/PickerSheet.tsx +++ b/src/components/uikit/PickerSheet.tsx @@ -1,10 +1,16 @@ 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 { useEffectEvent } from '~utils/common'; import { Text } from './Text'; @@ -122,7 +128,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 +204,7 @@ type ListItemProps = { const ListItem = memo( ({ multiple, label, value, checked, onOptionSelect }: ListItemProps) => { return ( - + {multiple ? ( onOptionSelect(value)} /> )} - + ); } ); @@ -234,7 +242,7 @@ function ListHeader({ }) { const { t } = useLingui(); return ( - + {numSelected > 1 && ( - Clear selected ({numSelected}) - + )} - + ); } function ListEmpty({ children }: { children?: ReactNode }) { return ( - + {children || ( @@ -283,7 +292,7 @@ function ListEmpty({ children }: { children?: ReactNode }) { )} - + ); } @@ -291,42 +300,38 @@ 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, + ...theme.utils.flexCenter, + }, + 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, + ...theme.utils.flexCenter, + }, +})); diff --git a/src/components/uikit/ProgressBar.tsx b/src/components/uikit/ProgressBar.tsx index 476bc4a3..e4049729 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]); + }, [step]); // eslint-disable-line react-hooks/exhaustive-deps 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..9ab1f943 100644 --- a/src/components/uikit/SegmentedControl.tsx +++ b/src/components/uikit/SegmentedControl.tsx @@ -1,14 +1,19 @@ 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 { haptics } from '~utils/haptics'; import { Text } from './Text'; @@ -23,11 +28,12 @@ export function SegmentedControl(props: Props) { const [layout, setLayout] = useState(); return ( - setLayout(e.nativeEvent.layout)} > {!!layout && } - + ); } @@ -61,7 +67,9 @@ function Segments({ return ( <> - + {segments.map((segment, index) => { return ( @@ -110,8 +118,10 @@ function Segment({ return ( - {label} - - + + ); } @@ -135,39 +145,31 @@ 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, - backgroundColor: 'rgba(150, 150, 150, 0.15)', +const styles = StyleSheet.create((theme) => ({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.surface, + borderRadius: 10, + }, + segmentBackground: { 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', { + backgroundColor: 'rgba(150, 150, 150, 0.15)', + ...theme.utils.absoluteFill, + }, + 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..10e70a9c 100644 --- a/src/components/uikit/Text.tsx +++ b/src/components/uikit/Text.tsx @@ -1,19 +1,72 @@ -import { styled, themeProp, getTextTypographyVariants } from '~styles'; +import { type ComponentType, createElement, forwardRef } from 'react'; +import type { TextProps as RNTextProps } from 'react-native'; +import { StyleSheet } 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 * as typographyTokens from '~design-system/typography'; +import { type Color } from '~styles/styled'; +import { typography } from '~styles/utils'; + +type TextProps = RNTextProps & { + variant?: keyof typeof typographyTokens; + align?: 'left' | 'right' | 'center'; + uppercase?: boolean; + color?: Color; + 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) { + styles.useVariants({ + align, + uppercase, + color, + variant, + }); + + return ( + + {children} + + ); +} + +const styles = StyleSheet.create((theme) => ({ + 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, + { color: value }, + ]) + ), + align: { + left: { textAlign: 'left' }, + right: { textAlign: 'right' }, + center: { textAlign: 'center' }, + }, + uppercase: { + true: { textTransform: 'uppercase' }, + false: { textTransform: 'none' }, + }, }, }, -}); +})); diff --git a/src/components/uikit/buttons/Button.tsx b/src/components/uikit/buttons/Button.tsx index e39083b4..b99a60d0 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 TypographyToken } from '~design-system/typography'; 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 ( - )} - + ); } -const sizeToTextVariant: Record = { +const sizeToTextVariant: Record = { small: 'bodyExtraSmallBold', normal: 'bodySmallBold', large: 'bodySemiBold', @@ -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..9efa0d18 100644 --- a/src/components/uikit/buttons/IconButton.tsx +++ b/src/components/uikit/buttons/IconButton.tsx @@ -1,13 +1,17 @@ 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 { haptics } from '~utils/haptics'; import { Icon } from '../Icon'; @@ -30,7 +34,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 +85,26 @@ export function IconButton({ } } + styles.useVariants({ + size, + disabled, + }); + return ( - - + {loading ? ( ) : ( @@ -104,45 +112,43 @@ 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', + ...theme.utils.flexCenter, + 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..40597c62 100644 --- a/src/components/uikit/buttons/helpers.ts +++ b/src/components/uikit/buttons/helpers.ts @@ -1,6 +1,6 @@ import { type StyleProp, type ViewStyle } from 'react-native'; -import { type Color, type useTheme } from '~styles'; +import { type Color, type Theme } from '~styles/styled'; import { type ButtonProps, @@ -15,7 +15,7 @@ const getBaseStyle = ({ color = 'primary', disabled = false, }: Pick & { - theme: ReturnType; + theme: Theme; }): ViewStyle => { const baseStyle: ViewStyle = { backgroundColor: 'transparent', @@ -101,7 +101,7 @@ export const getButtonWrapperStyle = ({ color = 'primary', disabled = false, }: Pick & { - theme: ReturnType; + theme: Theme; }): StyleProp => { return getBaseStyle({ variant, color, disabled, theme }); }; @@ -113,7 +113,7 @@ export const getIconWrapperStyle = ({ color = 'primary', disabled = false, }: Pick & { - theme: ReturnType; + 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 a90f9404..f39b07c8 100644 --- a/src/components/uikit/inputs/Checkbox.tsx +++ b/src/components/uikit/inputs/Checkbox.tsx @@ -1,12 +1,12 @@ 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 { haptics } from '~utils/haptics'; import { Icon } from '../Icon'; @@ -39,8 +39,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 with icon width + marginRight: theme.space.small, + borderColor: theme.colors.text, + ...theme.utils.flexCenter, + 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..87038d53 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,12 @@ export function InputButton({ }: Props) { const { t } = useLingui(); + styles.useVariants({ + focused: isFocused, + valid: isValid, + disabled: isDisabled, + }); + return ( @@ -58,18 +63,22 @@ export function InputButton({ )} - - + - + {value || placeholder} - - {!!icon && ( - - - - )} - - + {!!icon && ( + + + + )} + + + {!!message && ( {!isValid && } @@ -98,44 +107,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..9eb7caa2 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,11 @@ export function Radio({ onChange, checked, value, label }: Props) { onChange(value); } + styles.useVariants({ checked }); + return ( - - {checked && } + {checked && } {label} - + ); } @@ -49,40 +52,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..93180aea 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) => ( ( accessibilityLabel={option} accessibilityHint={t`Double tap to select this suggestion`} > - + {option} @@ -85,7 +89,7 @@ export const SearchInput = forwardRef( ))} - + )} ); @@ -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: 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 1589522b..fb4caa8a 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,11 @@ export const TextInput = forwardRef( const inputRef = useRef(null); useImperativeHandle(ref, () => inputRef.current as RNTextInput); + styles.useVariants({ + valid: isValid, + disabled: isDisabled, + }); + function handleCancel() { onChange(''); inputRef.current?.blur(); @@ -120,26 +127,22 @@ export const TextInput = forwardRef( )} {showCharacterLimit && isFocused && ( - {characterCount} - {` / ${maxLength}`} - + / {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/utils.ts b/src/design-system/utils.ts index 1f277f19..700498c7 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,7 @@ const WEIGHT_TO_FONT = { 900: 'Black', } as const; -export function getFontFromWeight(weight: number) { +export function getFontFromWeight(weight: number): FontWeightVar { return WEIGHT_TO_FONT[weight as FontWeightNum].toLowerCase() as FontWeightVar; } @@ -127,12 +128,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 78ffe21b..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, - // 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, - }; - - 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..ae2825e5 100644 --- a/src/styles/styled.ts +++ b/src/styles/styled.ts @@ -1,43 +1,74 @@ -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 { 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, - }); - -export const darkTheme = createTheme({ - colors: designSystemUtils.transformColors(colors), // TODO: add dark theme support once we get dark mode colors from Figma +import { absoluteFill, flexCenter } from './utils'; + +const typographyTokens = typography as Typography; + +const lightTheme = { + typography: typographyTokens, + colors: designSystemUtils.transformColors(colors), + radii: { ...radii, none: 0 }, + space: { ...space, none: 0 }, + sizes: { hairlineWidth: StyleSheet.hairlineWidth }, + 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 = { + 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 type Theme = AppThemes[keyof AppThemes]; + +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']; +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' { + // 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 {} +} + +StyleSheet.configure({ + themes: appThemes, + breakpoints, + settings: { + initialTheme: 'light', + }, }); -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']; diff --git a/src/styles/utils.ts b/src/styles/utils.ts index 606e43b1..2c352eea 100644 --- a/src/styles/utils.ts +++ b/src/styles/utils.ts @@ -1,53 +1,44 @@ -import { StyleSheet } from 'react-native'; -import type * as Stitches from 'stitches-native'; +import { type ViewStyle } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; -import * as shadows from '~design-system/shadows'; import * as typographyTokens from '~design-system/typography'; import * as designSystemUtils from '~design-system/utils'; -type Typography = keyof typeof typographyTokens; +import { type AppThemes } from './styled'; -export const typography = (variant: Typography) => { - const { fontWeight, textTransform } = typographyTokens[variant]; - - return { - fontFamily: `$${designSystemUtils.getFontFromWeight(fontWeight)}`, - fontSize: `$${variant}`, - fontWeight: `$${variant}`, - letterSpacing: `$${variant}`, - lineHeight: `$${variant}`, - textTransform, - }; +type FlexCenter = { + flexDirection: ViewStyle['flexDirection']; + justifyContent: ViewStyle['justifyContent']; + alignItems: ViewStyle['alignItems']; }; -export const size = (value: Stitches.PropertyValue<'width'>) => ({ - width: value, - height: value, -}); - -export const shadow = ( - level: 'none' | designSystemUtils.ShadowName +export const typography = ( + theme: AppThemes['light'], + variant: typographyTokens.TypographyToken ) => { + const { fontWeight, textTransform } = typographyTokens[variant]; + return { - none: { - elevation: 0, - shadowOffset: { width: 0, height: 0 }, - shadowRadius: 0, - shadowOpacity: 0, - shadowColor: '#000', - }, - ...designSystemUtils.getShadows(shadows), - }[level]; + 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 flexCenter = ( - value?: Stitches.PropertyValue<'flexDirection'> -) => ({ - flexDirection: value || 'column', +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: typographyTokens.TypographyToken +): 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..9530fc6f --- /dev/null +++ b/src/types/declarations.d.ts @@ -0,0 +1,14 @@ +declare module '*.jpg' { + const value: ImageSourcePropType | undefined; + export default value; +} + +declare module '*.png' { + const value: ImageSourcePropType | undefined; + export default value; +} + +declare module '*.ttf' { + const src: FontSource; + 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"]