diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml new file mode 100644 index 00000000..6b1e7afa --- /dev/null +++ b/.github/workflows/pr-verify.yml @@ -0,0 +1,24 @@ +name: PR Verify + +on: + push: + branches-ignore: + - 'master' + - 'main' + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: PR Verify + uses: actions/setup-node@v1 + with: + node-version: 10.16.0 + - name: Verify lint, TS, and tests + run: | + cd template + yarn + yarn ci diff --git a/template/.eslintrc.js b/template/.eslintrc.js index 6e1b14f4..45edb5c9 100644 --- a/template/.eslintrc.js +++ b/template/.eslintrc.js @@ -1,4 +1,8 @@ module.exports = { root: true, extends: ['plugin:echobind/react-native'], + rules: { + 'import/namespace': 0, + '@typescript-eslint/explicit-function-return-type': 0 + } }; diff --git a/template/.github/workflows/pr-verify.yml b/template/.github/workflows/pr-verify.yml new file mode 100644 index 00000000..a1045605 --- /dev/null +++ b/template/.github/workflows/pr-verify.yml @@ -0,0 +1,23 @@ +name: PR Verify + +on: + push: + branches-ignore: + - 'master' + - 'main' + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: PR Verify + uses: actions/setup-node@v1 + with: + node-version: 10.16.0 + - name: Verify lint, TS, and tests + run: | + yarn + yarn ci diff --git a/template/package.json b/template/package.json index 2b848052..6dafd475 100644 --- a/template/package.json +++ b/template/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "react-native start --reset-cache", "test": "jest", - "test:ci": "jest", + "ci": "yarn lint && yarn ts-check && yarn test", "test:watch": "jest --changedSince=master --watch", "lint": "eslint . --ext .ts,.tsx", "g:component": "hygen component new --name", @@ -24,7 +24,8 @@ "e2e:ios-debug": "detox build -c ios.sim.debug && detox test -c ios.sim.debug --cleanup", "e2e:ios": "detox build -c ios.sim.release && detox test -c ios.sim.release --cleanup", "e2e:android-debug": "detox build -c android.emu.debug && detox test -c android.emu.debug -l trace --cleanup", - "e2e:android": "detox build -c android.emu.release && detox test -c android.emu.release -l trace --cleanup" + "e2e:android": "detox build -c android.emu.release && detox test -c android.emu.release -l trace --cleanup", + "ts-check": "yarn tsc --skipLibCheck" }, "keywords": [], "author": "", @@ -36,10 +37,12 @@ "@react-navigation/bottom-tabs": "^5.6.1", "@react-navigation/native": "^5.6.1", "@react-navigation/stack": "^5.6.2", + "@shopify/restyle": "^1.4.0", "emotion-theming": "^10.0.19", "react": "16.13.1", "react-native": "0.63.2", "react-native-bootsplash": "^3.1.2", + "react-native-dynamic": "^1.0.0", "react-native-elements": "^1.2.7", "react-native-gesture-handler": "^1.6.1", "react-native-reanimated": "^1.9.0", @@ -53,8 +56,6 @@ "@babel/core": "^7.8.4", "@babel/runtime": "^7.8.4", "@react-native-community/eslint-config": "^1.1.0", - "metro-react-native-babel-preset": "^0.59.0", - "react-test-renderer": "16.13.1", "@storybook/addon-ondevice-backgrounds": "^5.2.5", "@storybook/addons": "^5.2.5", "@storybook/react-native": "^5.3.0-rc.1", @@ -73,8 +74,10 @@ "hygen": "^2.0.4", "jest": "^25.1.0", "lint-staged": "^10.0.0-beta.3", + "metro-react-native-babel-preset": "^0.59.0", "prettier": "^1.19.1", "react-native-flipper": "^0.73.0", + "react-test-renderer": "16.13.1", "solidarity": "^2.3.1", "ts-jest": "^23.10.5", "typescript": "^3.2.2", diff --git a/template/src/App.tsx b/template/src/App.tsx index 0c6f66ab..50331252 100644 --- a/template/src/App.tsx +++ b/template/src/App.tsx @@ -1,19 +1,24 @@ +import { NavigationContainer } from '@react-navigation/native'; +import { ThemeProvider as ShopifyThemeProvider } from '@shopify/restyle'; +import { ThemeProvider } from 'emotion-theming'; import React, { ReactElement, useEffect } from 'react'; import RNBootSplash from 'react-native-bootsplash'; +import { useDarkMode } from 'react-native-dynamic'; import { SafeAreaProvider } from 'react-native-safe-area-context'; -import { NavigationContainer } from '@react-navigation/native'; -import { ThemeProvider } from 'emotion-theming'; import Storybook from '../storybook'; import { AppNav } from './navigation/AppNav'; import { GuestNav } from './navigation/GuestNav'; - -import { theme } from './styles'; +import { theme as emotionTheme } from './styles'; +import { darkModeTheme, lightModeTheme } from './theme'; // NOTE: Change this boolean to true to render the Storybook view for development! const RENDER_STORYBOOK = false; const App = (): ReactElement | null => { + const isDarkMode = useDarkMode(); + const theme = isDarkMode ? darkModeTheme : lightModeTheme; + // hide the splashscreen on mount useEffect(() => { RNBootSplash.hide({ fade: true }); @@ -27,10 +32,12 @@ const App = (): ReactElement | null => { }; return ( - + - - {RENDER_STORYBOOK ? : renderNavigation()} + + + {RENDER_STORYBOOK ? : renderNavigation()} + diff --git a/template/src/components/Button/Button.stories.tsx b/template/src/components/Button/Button.stories.tsx index 4efa571f..a7c228fb 100644 --- a/template/src/components/Button/Button.stories.tsx +++ b/template/src/components/Button/Button.stories.tsx @@ -4,5 +4,5 @@ import React from 'react'; import { Button } from './Button'; storiesOf('components/Button', module).add('Default', () => ( - )); diff --git a/template/src/components/Button/Button.tsx b/template/src/components/Button/Button.tsx index b941e3d6..9b7eea2b 100644 --- a/template/src/components/Button/Button.tsx +++ b/template/src/components/Button/Button.tsx @@ -1,83 +1,31 @@ -import React, { FC } from 'react'; -import { ActivityIndicator } from 'react-native'; -import { - backgroundColor, - BorderProps, - ColorProps, - SpaceProps, - FlexProps, - LayoutProps, -} from 'styled-system'; +import { createRestyleComponent, createVariant, VariantProps } from '@shopify/restyle'; +import React from 'react'; -import { Container } from '../Container'; +import { Theme } from '../../theme'; +import { useVariantValue } from '../../utils/theme-utils/theme-utils'; import { Text } from '../Text'; -import { Touchable } from '../Touchable'; -import { colors } from '../../styles'; -import { AccessbilityRole } from '../../types/AccessibilityRole'; +import { Touchable, TouchableProps } from '../Touchable'; -interface ButtonProps { - /** - * Overrides the text that's read by the screen reader when the user interacts with the element. By default, the - * label is constructed by traversing all the children and accumulating all the Text nodes separated by space. - */ - accessibilityLabel: string; - /** Accessibility Role tells a person using either VoiceOver on iOS or TalkBack on Android the type of element that is focused on. */ - accessbilityRole?: AccessbilityRole; - /** disabled button state */ - disabled?: boolean; - /** loading button state */ - loading?: boolean; - /** the text label of the button */ - label: string; - /** the callback to be invoked onPress */ - onPress: () => void; -} +type ButtonProps = TouchableProps & VariantProps; -type ComponentProps = ButtonProps & BorderProps & ColorProps & SpaceProps & FlexProps & LayoutProps; +const VariantRestyleComponent = createVariant({ + themeKey: 'buttonVariants', +}); + +const ButtonWrapper = createRestyleComponent( + [VariantRestyleComponent], + Touchable +); /** - * notes: - * - restricting inner text style from being directly configurable to avoid style prop conflicts - * - if button is disabled it will not render a touchableOpacity at all + * A simple button */ -export const Button: FC = ({ - accessibilityLabel, - label, - onPress, - disabled, - loading, - color: componentColor, - ...props -}) => { - const ButtonContainer = disabled ? Container : Touchable; - const onPressAction = loading ? null : onPress; +export const Button = ({ children, ...props }: ButtonProps) => { + const textStyle = useVariantValue('buttonVariants', 'textStyle', props.variant); return ( - - {loading ? ( - - ) : ( - - {label} - - )} - + + {children} + ); }; - -Button.defaultProps = { - disabled: false, - borderColor: colors.transparent, - borderWidth: 1, - backgroundColor: colors.orange, -}; diff --git a/template/src/components/Button/index.ts b/template/src/components/Button/index.ts index 3d1f3cc3..8b166a86 100644 --- a/template/src/components/Button/index.ts +++ b/template/src/components/Button/index.ts @@ -1 +1 @@ -export * from './Button'; \ No newline at end of file +export * from './Button'; diff --git a/template/src/components/Container/Container.tsx b/template/src/components/Container/Container.tsx index 0a754fd4..db137fca 100644 --- a/template/src/components/Container/Container.tsx +++ b/template/src/components/Container/Container.tsx @@ -1,64 +1,18 @@ -import { - BorderProps, - borders, - color, - ColorProps, - flexbox, - FlexProps, - layout, - space, - SpaceProps, -} from 'styled-system'; -import styled from '@emotion/native'; +import { BoxProps, createBox } from '@shopify/restyle'; +import React from 'react'; -import { margins } from '../../styles'; +import { Theme } from '../../theme'; -interface ContainerProps { - /** applies "flex: 1" style */ - fill?: boolean; - /** applies "width: 100%" style */ - fullWidth?: boolean; - /** centers content both vertically and horizontally */ - centerContent?: boolean; - /** - * applies default horizontal screen margins. - * decoupled from Screen component to make layout-building more flexible. - */ - screenMargins?: boolean; -} +/** + * Renders a View with @shopify/restyle functionality. + */ +export const Container = createBox(); -type ComponentProps = ContainerProps & BorderProps & ColorProps & FlexProps & SpaceProps; +export type ContainerProps = React.ComponentProps & BoxProps; /** - * This is our primitive View component with styled-system props applied + * Renders a container with children that are horizontally and vertically centered. */ -export const Container = styled.View` - ${space}; - ${color}; - ${borders}; - ${layout}; - ${flexbox}; - ${props => - props.fill && - ` - flex: 1; - `} - ${props => - props.fullWidth && - ` - width: 100%; - `} - ${props => - props.centerContent && - ` - justifyContent: center; - alignItems: center; - `} - ${props => - props.screenMargins && - ` - paddingHorizontal: ${margins.screenMargin}; - `} -`; - -Container.defaultProps = {}; +export const CenteredContainer = (props: ContainerProps): React.ReactElement => ( + +); diff --git a/template/src/components/Login/Login.stories.tsx b/template/src/components/Login/Login.stories.tsx index 62212fa8..7aa61f77 100644 --- a/template/src/components/Login/Login.stories.tsx +++ b/template/src/components/Login/Login.stories.tsx @@ -2,17 +2,11 @@ import { storiesOf } from '@storybook/react-native'; import React from 'react'; import { Login } from './Login'; -import { Screen } from '../Screen'; -import { colors } from '../../styles'; -const ScreenDecorator = storyFn => {storyFn()}; - -storiesOf('components/Login', module) - .addDecorator(ScreenDecorator) - .add('Default', () => ( - console.log('login pressed')} - forgotPasswordPress={() => console.log('forgot password pressed')} - registrationPress={() => console.log('registration pressed')} - /> - )); +storiesOf('components/Login', module).add('Default', () => ( + console.log('Login pressed!')} + forgotPasswordPress={() => console.log('Forgot password pressed!')} + registrationPress={() => console.log('Registration pressed!')} + /> +)); diff --git a/template/src/components/Login/Login.tsx b/template/src/components/Login/Login.tsx index 724d21f8..ababa174 100644 --- a/template/src/components/Login/Login.tsx +++ b/template/src/components/Login/Login.tsx @@ -1,13 +1,12 @@ import React, { FC } from 'react'; -import { BorderProps, ColorProps, SpaceProps, FlexProps } from 'styled-system'; import { Icon } from 'react-native-elements'; + +import { useColor } from '../../theme'; import { Button } from '../Button'; +import { Container } from '../Container'; import { Text } from '../Text'; import { TextInput } from '../TextInput'; import { Touchable } from '../Touchable'; -import { Container } from '../Container'; - -import { colors } from '../../styles'; interface LoginProps { /** the callbacks to be invoked onPress */ @@ -16,103 +15,73 @@ interface LoginProps { forgotPasswordPress: () => void; } -type ComponentProps = LoginProps & FlexProps & SpaceProps & BorderProps & ColorProps; +export const Login: FC = ({ loginPress, registrationPress, forgotPasswordPress }) => { + const textSecondary = useColor('textSecondary'); -export const Login: FC = ({ - loginPress, - registrationPress, - forgotPasswordPress, - children, - ...props -}) => { return ( - - - Welcome, please {'\n'}sign in. - + + Welcome, please sign in. } - marginTop={2} - borderRadius={5} - borderColor={colors.lightGray} + icon={} /> } - marginTop={2} - borderRadius={5} - borderColor={colors.lightGray} + icon={} /> { forgotPasswordPress(); }} - fullWidth alignItems={'flex-end'} accessibilityLabel="Forgot Password Button" - accessbilityRole="button" > - + Forgot Password? - - -   or   - + + +   or   + ); }; - -Login.defaultProps = { - loginPress: console.log('implement login press'), - forgotPasswordPress: console.log('implement forgot password press'), - registrationPress: console.log('implement registration press'), -}; diff --git a/template/src/components/Login/index.ts b/template/src/components/Login/index.ts index f1d32a23..a10c3a83 100644 --- a/template/src/components/Login/index.ts +++ b/template/src/components/Login/index.ts @@ -1 +1 @@ -export * from './Login'; \ No newline at end of file +export * from './Login'; diff --git a/template/src/components/Registration/Registration.stories.tsx b/template/src/components/Registration/Registration.stories.tsx index acc51dc5..c5fba4ac 100644 --- a/template/src/components/Registration/Registration.stories.tsx +++ b/template/src/components/Registration/Registration.stories.tsx @@ -4,5 +4,5 @@ import React from 'react'; import { Registration } from './Registration'; storiesOf('components/Registration', module).add('Default', () => ( - + console.log('Create account pressed!')} /> )); diff --git a/template/src/components/Registration/Registration.tsx b/template/src/components/Registration/Registration.tsx index 56b802e1..bfa5b087 100644 --- a/template/src/components/Registration/Registration.tsx +++ b/template/src/components/Registration/Registration.tsx @@ -1,98 +1,45 @@ import React, { FC } from 'react'; -import { - color, - space, - layout, - flexbox, - borders, - BorderProps, - ColorProps, - SpaceProps, - FlexProps, -} from 'styled-system'; -import styled from '@emotion/native'; -import { Icon, Input } from 'react-native-elements'; +import { Icon } from 'react-native-elements'; + +import { useColor } from '../../theme'; import { Button } from '../Button'; +import { Container } from '../Container'; import { Text } from '../Text'; import { TextInput } from '../TextInput'; -import { Touchable } from '../Touchable'; -import { Container } from '../Container'; - -import { colors } from '../../styles'; interface RegistrationProps { /** the callbacks to be invoked onPress */ createPress: () => void; - goBack: () => void; } -type ComponentProps = RegistrationProps & FlexProps & SpaceProps & BorderProps & ColorProps; +export const Registration: FC = ({ createPress }) => { + const textSecondary = useColor('textSecondary'); -export const Registration: FC = ({ createPress, goBack, children, ...props }) => { return ( - - - goBack()} fullWidth alignItems="flex-start"> - - - - - - - Let's create your{'\n'}account! - - } - marginTop={2} - borderRadius={5} - borderColor={colors.lightGray} - /> - - } - marginTop={2} - borderRadius={5} - borderColor={colors.lightGray} - /> - } - marginTop={2} - borderRadius={5} - borderColor={colors.lightGray} - /> - - ); }; - -Registration.defaultProps = { - createPress: console.log('implement event'), -}; diff --git a/template/src/components/Registration/index.ts b/template/src/components/Registration/index.ts index 19562303..7ba6f602 100644 --- a/template/src/components/Registration/index.ts +++ b/template/src/components/Registration/index.ts @@ -1 +1 @@ -export * from './Registration'; \ No newline at end of file +export * from './Registration'; diff --git a/template/src/components/Screen/Screen.stories.tsx b/template/src/components/Screen/Screen.stories.tsx index a0b6fcc0..11d35588 100644 --- a/template/src/components/Screen/Screen.stories.tsx +++ b/template/src/components/Screen/Screen.stories.tsx @@ -1,8 +1,11 @@ import { storiesOf } from '@storybook/react-native'; import React from 'react'; +import { Text } from '../Text'; import { Screen } from './Screen'; -storiesOf('components/Screen', module) - .add('Default', () => ) - .add('with backgroundColor', () => ); +storiesOf('components/Screen', module).add('Default', () => ( + + {'Content wrapped in a screen'} + +)); diff --git a/template/src/components/Screen/Screen.tsx b/template/src/components/Screen/Screen.tsx index 8e6ada9a..45ca5585 100644 --- a/template/src/components/Screen/Screen.tsx +++ b/template/src/components/Screen/Screen.tsx @@ -1,62 +1,43 @@ -import React, { ReactNode, FC } from 'react'; -import styled from '@emotion/native'; +import React, { FC, ReactNode } from 'react'; +import { StatusBar } from 'react-native'; +import { useDarkMode } from 'react-native-dynamic'; import SafeAreaView from 'react-native-safe-area-view'; -import { color, space, FlexProps, SpaceProps } from 'styled-system'; -import { Container } from '../Container'; - -import { theme } from '../../styles'; -import { colors } from '../../styles'; - -const VerticallyPaddedView = styled.View` - flex: 1; - ${space}; - ${color}; -`; - -const InnerView = styled.View` - flex: 1; - ${space}; -`; +import { Container, ContainerProps } from '../Container'; interface ScreenProps { /** The content to render within the screen */ children?: ReactNode; /** Whether to force the topInset. Use to prevent screen jank on tab screens */ - forceTopInset?: Boolean; + forceTopInset?: boolean; } -type ComponentProps = ScreenProps & FlexProps & SpaceProps; +type ComponentProps = ScreenProps & ContainerProps; export const Screen: FC = ({ - backgroundColor, paddingTop, paddingBottom, forceTopInset, children, ...screenProps -}) => ( - - - - {children} - - - -); - -SafeAreaView.defaultProps = { - bg: colors.white, +}) => { + const isDarkMode = useDarkMode(); + + return ( + <> + + + + + + {children} + + + + + + ); }; - -VerticallyPaddedView.defaultProps = { - pt: theme.space[2], - pb: theme.space[2], -}; - -InnerView.defaultProps = {}; - -Screen.defaultProps = {}; diff --git a/template/src/components/Text/Text.stories.tsx b/template/src/components/Text/Text.stories.tsx index 6b23db4e..b87b3b99 100644 --- a/template/src/components/Text/Text.stories.tsx +++ b/template/src/components/Text/Text.stories.tsx @@ -3,7 +3,4 @@ import React from 'react'; import { Text } from './Text'; -storiesOf('components/Text', module) - .add('Default', () => ) - .add('with text prop', () => ) - .add('with child prop', () => Hello there); +storiesOf('components/Text', module).add('Default', () => Hello world!); diff --git a/template/src/components/Text/Text.tsx b/template/src/components/Text/Text.tsx index 94004829..b212e53c 100644 --- a/template/src/components/Text/Text.tsx +++ b/template/src/components/Text/Text.tsx @@ -1,32 +1,7 @@ -import styled from '@emotion/native'; -import { - color, - space, - typography, - textStyle, - ColorProps, - SpaceProps, - TextStyleProps, - TypographyProps, -} from 'styled-system'; - -import { colors } from '../../styles'; - -interface TextProps {} - -type ComponentProps = TextProps & ColorProps & SpaceProps & TextStyleProps & TypographyProps; +import { createText } from '@shopify/restyle'; +import { Theme } from '../../theme'; /** - * This is our primitive Text component with styled-system props applied + * Renders a Text component with @shopify/restyle functionality. */ -export const Text = styled.Text` - ${space}; - ${color}; - ${typography}; - ${textStyle}; -`; - -Text.defaultProps = { - color: colors.black, - fontSize: 3, -}; +export const Text = createText(); diff --git a/template/src/components/TextInput/TextInput.stories.tsx b/template/src/components/TextInput/TextInput.stories.tsx index 0bb17c3c..01fce095 100644 --- a/template/src/components/TextInput/TextInput.stories.tsx +++ b/template/src/components/TextInput/TextInput.stories.tsx @@ -3,4 +3,6 @@ import React from 'react'; import { TextInput } from './TextInput'; -storiesOf('components/TextInput', module).add('Default', () => ); +storiesOf('components/TextInput', module).add('Default', () => ( + +)); diff --git a/template/src/components/TextInput/TextInput.tsx b/template/src/components/TextInput/TextInput.tsx index 622fa6fa..eeabe45e 100644 --- a/template/src/components/TextInput/TextInput.tsx +++ b/template/src/components/TextInput/TextInput.tsx @@ -1,59 +1,51 @@ -import React, { FC } from 'react'; -import { TextInputProps as TextInputBaseProps } from 'react-native'; -import styled from '@emotion/native'; import { - borders, - color, - layout, - space, - flex, - typography, - textStyle, + border, BorderProps, - FlexProps, + color, ColorProps, + createRestyleComponent, + layout, LayoutProps, - SpaceProps, - TextStyleProps, + spacing, + SpacingProps, + typography, TypographyProps, -} from 'styled-system'; -import { Icon } from 'react-native-elements'; +} from '@shopify/restyle'; +import React, { FC } from 'react'; +import { + TextInput as ReactNativeTextInput, + TextInputProps as TextInputBaseProps, +} from 'react-native'; + +import { colors } from '../../styles'; +import { Theme } from '../../theme'; import { Container } from '../Container'; import { Text } from '../Text'; -import { colors } from '../../styles'; interface TextInputProps extends TextInputBaseProps { /** An optional header label to render above the input */ topLabel?: string; //** An option icon to be displayed to the left of the input box */ - icon?: Icon; + icon?: JSX.Element; /** - * Overrides the text that's read by the screen reader when the user interacts with the element. By default, the - * label is constructed by traversing all the children and accumulating all the Text nodes separated by space. - */ - accessibilityLabel?: string; + * Overrides the text that's read by the screen reader when the user interacts with the element. By default, the + * label is constructed by traversing all the children and accumulating all the Text nodes separated by space. + */ + accessibilityLabel?: string; } type ComponentProps = TextInputProps & - ColorProps & - SpaceProps & - TextStyleProps & - TypographyProps & - BorderProps & - LayoutProps & - FlexProps; + SpacingProps & + BorderProps & + ColorProps & + LayoutProps & + TypographyProps & + LayoutProps; -const InputContainer = styled(Container)``; - -const Input = styled.TextInput` - ${flex}; - ${borders}; - ${color}; - ${layout}; - ${space}; - ${textStyle}; - ${typography}; -`; +const Input = createRestyleComponent( + [spacing, border, color, layout, typography, layout], + ReactNativeTextInput +); // NOTE: for layout and dimensioning of TextInput, wrap it in a Container export const TextInput: FC = ({ @@ -61,42 +53,41 @@ export const TextInput: FC = ({ icon, accessibilityLabel, multiline, - borderColor, - borderRadius, + borderColor = 'backgroundSecondary', + borderRadius = 'md', ...inputProps }) => ( - + {topLabel ? ( - + {topLabel} ) : null} - + {icon ? icon : null} - + ); -InputContainer.defaultProps = { - flexDirection: 'row', - bg: colors.white, - borderWidth: 1, - borderColor: colors.black, - minHeight: 40, - paddingLeft: 10, - alignItems: 'center', -}; - TextInput.defaultProps = { - p: 2, + padding: 2, textAlignVertical: 'center', width: '100%', }; diff --git a/template/src/components/TextInput/index.ts b/template/src/components/TextInput/index.ts index faecfaa0..a7fcf6f3 100644 --- a/template/src/components/TextInput/index.ts +++ b/template/src/components/TextInput/index.ts @@ -1 +1 @@ -export * from './TextInput'; \ No newline at end of file +export * from './TextInput'; diff --git a/template/src/components/Touchable/Touchable.stories.tsx b/template/src/components/Touchable/Touchable.stories.tsx index cf08cd4d..6a4122aa 100644 --- a/template/src/components/Touchable/Touchable.stories.tsx +++ b/template/src/components/Touchable/Touchable.stories.tsx @@ -1,6 +1,11 @@ import { storiesOf } from '@storybook/react-native'; import React from 'react'; +import { Text } from '../Text'; import { Touchable } from './Touchable'; -storiesOf('components/Touchable', module).add('Default', () => ); +storiesOf('components/Touchable', module).add('Default', () => ( + console.log('Touchable pressed!')}> + {'Touchable'} + +)); diff --git a/template/src/components/Touchable/Touchable.tsx b/template/src/components/Touchable/Touchable.tsx index ce54c402..e39a299a 100644 --- a/template/src/components/Touchable/Touchable.tsx +++ b/template/src/components/Touchable/Touchable.tsx @@ -1,55 +1,25 @@ -import styled from '@emotion/native'; import { - color, - borders, - space, + createRestyleComponent, layout, - flexbox, - BorderProps, - ColorProps, - SpaceProps, - FlexProps, -} from 'styled-system'; + LayoutProps, + spacing, + SpacingProps, +} from '@shopify/restyle'; +import { ReactNode } from 'react'; +import { TouchableOpacity, TouchableOpacityProps } from 'react-native'; -interface TouchableProps { - /** applies "flex: 1" style **/ - fill?: boolean; - /** applies "width: 100%" style **/ - fullWidth?: boolean; - /** centers content both vertically and horizontally **/ - centerContent?: boolean; -} +import { Theme } from '../../theme'; + +type RestyleProps = TouchableOpacityProps & LayoutProps & SpacingProps; -type ComponentProps = TouchableProps & BorderProps & ColorProps & FlexProps & SpaceProps; +export interface TouchableProps extends RestyleProps { + children: ReactNode; +} /** - * This is our primitive TouchableOpacity component with styled-system props applied + * This is our primitive TouchableOpacity component with restyle props applied */ -export const Touchable = styled.TouchableOpacity` - ${space}; - ${color}; - ${borders}; - ${layout}; - ${flexbox}; - - ${props => - props.fill && - ` - flex: 1; - `} - - ${props => - props.fullWidth && - ` - width: 100%; - `} - - ${props => - props.centerContent && - ` - justifyContent: center; - alignItems: center; - `} -`; - -Touchable.defaultProps = {}; +export const Touchable = createRestyleComponent( + [layout, spacing], + TouchableOpacity +); diff --git a/template/src/navigation/GuestNav.tsx b/template/src/navigation/GuestNav.tsx index c54a5c35..33ad3ae3 100644 --- a/template/src/navigation/GuestNav.tsx +++ b/template/src/navigation/GuestNav.tsx @@ -4,13 +4,11 @@ import { createStackNavigator } from '@react-navigation/stack'; import { IntroScreen } from '../screens/IntroScreen'; import { LoginScreen } from '../screens/LoginScreen'; import { RegistrationScreen } from '../screens/RegistrationScreen'; -import { OnboardingNav } from './OnboardingNav'; const Stack = createStackNavigator(); /** * Guest nav typically consists of screens where the user is logged out: - * Onboarding * Registration * Login * Forgot Password @@ -19,7 +17,6 @@ export const GuestNav = (): ReactElement => { return ( - diff --git a/template/src/navigation/MainNav.tsx b/template/src/navigation/MainNav.tsx index 46c7c4c4..abb8d04b 100644 --- a/template/src/navigation/MainNav.tsx +++ b/template/src/navigation/MainNav.tsx @@ -1,31 +1,35 @@ -import React, { ReactElement } from 'react'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import React, { ReactElement } from 'react'; import { createFakeScreen } from '../navigation/createFakeScreen'; -import { colors } from '../styles'; +import { useColor } from '../theme'; const Tab = createBottomTabNavigator(); -const TabScreen1 = createFakeScreen('Tab Screen 1', colors.white); -const TabScreen2 = createFakeScreen('Tab Screen 2', colors.gray); -const TabScreen3 = createFakeScreen('Tab Screen 3', colors.orange); +const TabScreen1 = createFakeScreen('Tab Screen 1', 'backgroundPrimary'); +const TabScreen2 = createFakeScreen('Tab Screen 2', 'backgroundSecondary'); +const TabScreen3 = createFakeScreen('Tab Screen 3', 'blue'); /** * Main Nav is main interface of the app, defaults to tabs. */ export const MainNav = (): ReactElement => { + const textColor = useColor('textPrimary'); + const inactiveTintColor = useColor('textSecondary'); + const backgroundColor = useColor('backgroundTertiary'); + return ( { - return ( - - - - - ); -}; diff --git a/template/src/navigation/createFakeScreen.tsx b/template/src/navigation/createFakeScreen.tsx index df8b4146..b8747ed1 100644 --- a/template/src/navigation/createFakeScreen.tsx +++ b/template/src/navigation/createFakeScreen.tsx @@ -1,13 +1,23 @@ import React, { ReactElement } from 'react'; -import { Screen } from '../components/Screen'; import { Container } from '../components/Container'; +import { Screen } from '../components/Screen'; import { Text } from '../components/Text'; -export const createFakeScreen = (screenName, backgroundColor) => (): ReactElement => ( - - - {screenName} - - -); +export const createFakeScreen = (screenName, backgroundColor) => (): ReactElement => { + return ( + + + + {screenName} + + + + ); +}; diff --git a/template/src/screens/IntroScreen/IntroScreen.tsx b/template/src/screens/IntroScreen/IntroScreen.tsx index d71a3fd1..1fcd8bbc 100644 --- a/template/src/screens/IntroScreen/IntroScreen.tsx +++ b/template/src/screens/IntroScreen/IntroScreen.tsx @@ -1,41 +1,28 @@ import React, { FC } from 'react'; -import { ImageBackground, StatusBar, StyleSheet } from 'react-native'; -import { NavigationScreenProps } from 'react-navigation'; -import styled from '@emotion/native'; import { Button } from '../../components/Button'; +import { Container } from '../../components/Container'; import { Screen } from '../../components/Screen'; import { Text } from '../../components/Text'; -import { Container } from '../../components/Container'; -import { colors } from '../../styles'; -import bgImage from '../../assets/images/background.png'; - -const BackgroundImage = styled(ImageBackground)` - ${StyleSheet.absoluteFillObject}; -`; /** * First screen a logged out user sees, welcoming them to the app. */ -export const IntroScreen: FC = ({ navigation }) => { +export const IntroScreen: FC<{ navigation: any }> = ({ navigation }) => { return ( - - - - - - Welcome to the intro Screen! - - + + ); }; diff --git a/template/src/screens/LoginScreen/LoginScreen.tsx b/template/src/screens/LoginScreen/LoginScreen.tsx index faffaa52..a0285eab 100644 --- a/template/src/screens/LoginScreen/LoginScreen.tsx +++ b/template/src/screens/LoginScreen/LoginScreen.tsx @@ -1,17 +1,10 @@ import React, { FC } from 'react'; -import { Alert, ImageBackground, StatusBar, StyleSheet } from 'react-native'; -import { NavigationScreenProps } from 'react-navigation'; -import styled from '@emotion/native'; +import { Alert } from 'react-native'; import { Login } from '../../components/Login'; import { Screen } from '../../components/Screen'; -import bgImage from '../../assets/images/background.png'; -import { colors } from '../../styles'; -const BackgroundImage = styled(ImageBackground)` - ${StyleSheet.absoluteFillObject}; -`; -export const LoginScreen: FC = ({ navigation }) => { +export const LoginScreen: FC<{ navigation: any }> = ({ navigation }) => { const loginClick = (): void => { navigation.navigate('Intro'); }; @@ -25,21 +18,12 @@ export const LoginScreen: FC = ({ navigation }) => { }; return ( - - + - - - - - + ); }; diff --git a/template/src/screens/RegistrationScreen/RegistrationScreen.tsx b/template/src/screens/RegistrationScreen/RegistrationScreen.tsx index 3b7d47d1..949e0c6c 100644 --- a/template/src/screens/RegistrationScreen/RegistrationScreen.tsx +++ b/template/src/screens/RegistrationScreen/RegistrationScreen.tsx @@ -1,38 +1,16 @@ import React, { FC } from 'react'; -import { ImageBackground, StatusBar, StyleSheet } from 'react-native'; -import { NavigationScreenProps } from 'react-navigation'; -import styled from '@emotion/native'; import { Registration } from '../../components/Registration'; import { Screen } from '../../components/Screen'; -import bgImage from '../../assets/images/background.png'; -import { colors } from '../../styles'; - -const BackgroundImage = styled(ImageBackground)` - ${StyleSheet.absoluteFillObject}; -`; - -export const RegistrationScreen: FC = ({ navigation }) => { - const createClick = () => { +export const RegistrationScreen: FC<{ navigation: any }> = ({ navigation }) => { + const createClick = (): void => { navigation.navigate('Intro'); }; + return ( - - - - navigation.goBack()} /> - - + + + ); }; diff --git a/template/src/styles/margins.ts b/template/src/styles/margins.ts index 54e0f501..38126acc 100644 --- a/template/src/styles/margins.ts +++ b/template/src/styles/margins.ts @@ -1,5 +1,5 @@ export const margins = { - screenMargin: 20 + screenMargin: 20, }; export const space = [0, 4, 8, 16, 24, 32, 64, 128]; diff --git a/template/src/theme/breakpoints.ts b/template/src/theme/breakpoints.ts new file mode 100644 index 00000000..2d6a96f1 --- /dev/null +++ b/template/src/theme/breakpoints.ts @@ -0,0 +1,4 @@ +export const breakpoints = { + phone: 0, + tablet: 768, +}; diff --git a/template/src/theme/buttonVariants.ts b/template/src/theme/buttonVariants.ts new file mode 100644 index 00000000..c90ce99a --- /dev/null +++ b/template/src/theme/buttonVariants.ts @@ -0,0 +1,28 @@ +import { Dimensions } from 'react-native'; + +export const buttonVariants = { + defaults: { + backgroundColor: 'blue', + borderColor: 'transparent', + borderRadius: 'rounded', + borderWidth: 1, + flexDirection: 'row', + justifyContent: 'center', + maxWidth: 335, + paddingVertical: 3, + textStyle: { + color: 'white', + fontWeight: '600', + }, + width: Dimensions.get('window').width - 32, + }, + secondary: { + backgroundColor: 'backgroundSecondary', + borderColor: 'transparent', + textStyle: { + color: 'white', + }, + }, +}; + +export type ButtonVariants = keyof typeof buttonVariants; diff --git a/template/src/theme/colors.ts b/template/src/theme/colors.ts new file mode 100644 index 00000000..60aa2b2d --- /dev/null +++ b/template/src/theme/colors.ts @@ -0,0 +1,50 @@ +import { useDarkMode } from 'react-native-dynamic'; + +enum Palette { + black = '#000000', + blackMedium = '#1D1D1D', + blackLight = '#303030', + + blueLight = '#257FA1', + blueDark = '#413E4E', + + grayLight = '#C4C4C4', + grayDark = '#303030', + + white = '#FFFFFF', + transparent = 'transparent', +} + +export const colors = { + black: Palette.black, + transparent: Palette.transparent, + blue: Palette.blueLight, + backgroundPrimary: Palette.white, + backgroundSecondary: Palette.grayLight, + backgroundTertiary: Palette.blackLight, + textPrimary: Palette.blackMedium, + textSecondary: Palette.grayLight, + textInputBackground: Palette.white, + white: Palette.white, +}; + +export type ColorTypes = keyof typeof colors; + +export const darkModeColors: { + [key in keyof typeof colors]: Palette; +} = { + ...colors, + blue: Palette.blueDark, + backgroundPrimary: Palette.blackMedium, + backgroundSecondary: Palette.blackLight, + backgroundTertiary: Palette.black, + textPrimary: Palette.white, + textSecondary: Palette.blackMedium, + textInputBackground: Palette.blackLight, +}; + +export const useColor = (key: keyof typeof colors): string => { + const isDarkMode = useDarkMode(); + + return isDarkMode ? darkModeColors[key] : colors[key]; +}; diff --git a/template/src/theme/index.ts b/template/src/theme/index.ts new file mode 100644 index 00000000..71a3903e --- /dev/null +++ b/template/src/theme/index.ts @@ -0,0 +1,68 @@ +import { createTheme } from '@shopify/restyle'; +import { buttonVariants } from './buttonVariants'; +import { colors, darkModeColors } from './colors'; +import { textVariants } from './textVariants'; +import { spacing } from './spacing'; +import { breakpoints } from './breakpoints'; + +const theme = { + borderRadii: { + none: 0, + xs: 2, + sm: 4, + md: 8, + lg: 16, + rounded: 50, + }, + breakpoints, + buttonVariants, + colors, + shadowRadii: { + none: 0, + xs: 2, + sm: 4, + md: 8, + lg: 16, + }, + spacing, + textVariants, + navigation: { + colors: { + background: colors.backgroundPrimary, + border: colors.backgroundSecondary, + card: colors.backgroundSecondary, + notification: colors.backgroundSecondary, + primary: colors.white, + text: colors.white, + }, + dark: false, + }, +}; + +export const lightModeTheme = createTheme(theme); + +export const darkModeTheme = createTheme({ + ...theme, + colors: darkModeColors, + navigation: { + colors: { + background: colors.backgroundPrimary, + border: colors.backgroundSecondary, + card: colors.backgroundSecondary, + notification: colors.backgroundSecondary, + primary: colors.white, + text: colors.white, + }, + dark: true, + }, +}); + +export * from './buttonVariants'; +export * from './colors'; +export * from './textVariants'; +export * from './spacing'; +export * from './breakpoints'; + +export type Theme = typeof theme; + +export default theme; diff --git a/template/src/theme/spacing.ts b/template/src/theme/spacing.ts new file mode 100644 index 00000000..e673ef22 --- /dev/null +++ b/template/src/theme/spacing.ts @@ -0,0 +1,22 @@ +const MAX_KEY = 48; +const MULTIPLIER = 4; + +const generateSpacingIncrements = () => { + const spacingKeys = Array.from(Array(MAX_KEY).keys()); + + return spacingKeys.reduce( + (accum, key) => ({ + ...accum, + [key]: key * MULTIPLIER, + }), + {} + ); +}; + +export const spacing: { + [key in number | 'container' | 'px']: number; +} = { + ...generateSpacingIncrements(), + container: 8, + px: 1, +}; diff --git a/template/src/theme/textVariants.ts b/template/src/theme/textVariants.ts new file mode 100644 index 00000000..a3d7f75d --- /dev/null +++ b/template/src/theme/textVariants.ts @@ -0,0 +1,19 @@ +const baseTextStyles = { + fontFamily: 'Helvetica', + color: 'textPrimary', + fontSize: 16, +}; + +export const textVariants = { + defaults: baseTextStyles, + regular: {}, + bold: { + fontWeight: '700', + }, + semibold: { + fontWeight: '500', + }, + medium: { + fontWeight: '600', + }, +}; diff --git a/template/src/types/AccessibilityRole.ts b/template/src/types/AccessibilityRole.ts index fffae482..b2a84d23 100644 --- a/template/src/types/AccessibilityRole.ts +++ b/template/src/types/AccessibilityRole.ts @@ -1,4 +1,4 @@ -/** +/** * Accessbility Role communicates the purpose of a component to the user of an assistive technology, * This tells a person using either VoiceOver on iOS or TalkBack on Android the type of element that is focused on. * values pulled from official react native docs diff --git a/template/src/utils/theme-utils/index.ts b/template/src/utils/theme-utils/index.ts new file mode 100644 index 00000000..0c180a5b --- /dev/null +++ b/template/src/utils/theme-utils/index.ts @@ -0,0 +1 @@ +export * from './theme-utils'; diff --git a/template/src/utils/theme-utils/theme-utils.ts b/template/src/utils/theme-utils/theme-utils.ts new file mode 100644 index 00000000..57d1289e --- /dev/null +++ b/template/src/utils/theme-utils/theme-utils.ts @@ -0,0 +1,54 @@ +import { ResponsiveValue, SafeVariants, useTheme } from '@shopify/restyle'; + +import { Dimensions } from 'react-native'; +import { Theme, breakpoints } from '../../theme'; + +/** + * Gets the variant value while taking into account responsive values + */ +const getVariantValue = >( + variant?: ResponsiveValue, Theme> +) => { + if (!variant) { + return 'defaults'; + } + + if (typeof variant === 'object') { + const sortedEntries = Object.entries(breakpoints).sort((a, b) => b[1] - a[1]) as [ + keyof typeof breakpoints, + number + ][]; + + const width = Dimensions.get('window').width; + + for (let i = 0; i < sortedEntries.length; i++) { + const [key, value] = sortedEntries[i]; + + if (width > value) { + return variant[key] || 'defaults'; + } + } + } + + return variant; +}; + +/** + * Pull a key off of the variant styles + */ +export const useVariantValue = >( + themeKey: T, + key: string, + responsiveVariantValue?: ResponsiveValue, Theme> +) => { + const theme = useTheme(); + const variant = getVariantValue(responsiveVariantValue); + const variantObject = theme[themeKey]; + const defaultStyles = variantObject.defaults; + const variantStyles = variantObject[variant]; + + return { + ...defaultStyles[key], + ...variantStyles[key], + }; +}; diff --git a/template/src/utils/trimText/index.ts b/template/src/utils/trimText/index.ts index 9b1f4756..5b707bd6 100644 --- a/template/src/utils/trimText/index.ts +++ b/template/src/utils/trimText/index.ts @@ -1 +1 @@ -export { default } from './trimText'; +export * from './trimText'; diff --git a/template/storybook/stories/index.ts b/template/storybook/stories/index.ts index 8143a9fb..59361d1c 100644 --- a/template/storybook/stories/index.ts +++ b/template/storybook/stories/index.ts @@ -5,3 +5,4 @@ import '../../src/components/Text/Text.stories'; import '../../src/components/TextInput/TextInput.stories'; import '../../src/components/Touchable/Touchable.stories'; import '../../src/components/Login/Login.stories'; +import '../../src/components/Registration/Registration.stories'; diff --git a/template/storybook/storybook.js b/template/storybook/storybook.js index c6b9be91..b7687c0a 100644 --- a/template/storybook/storybook.js +++ b/template/storybook/storybook.js @@ -1,19 +1,19 @@ +// addons! +import './rn-addons'; + +import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds'; +import { addDecorator, addParameters, configure, getStorybookUI } from '@storybook/react-native'; import React from 'react'; import { AppRegistry } from 'react-native'; -import { addParameters, getStorybookUI, configure, addDecorator } from '@storybook/react-native'; -import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds'; import { name as appName } from '../app.json'; -import { Container } from '../src/components/Container'; - -// addons! -import './rn-addons'; +import { Screen } from '../src/components/Screen'; // adding a centered-view layout! const CenterView = ({ children }) => ( - + {children} - + ); // global decorators!