├── .nvmrc ├── .watchmanconfig ├── src ├── helpers │ ├── index.ts │ ├── colors.ts │ └── metrics.ts ├── components │ ├── Card │ │ ├── index.ts │ │ └── Card.tsx │ ├── Icon │ │ ├── index.ts │ │ └── Icon.tsx │ ├── Text │ │ ├── index.ts │ │ └── Text.tsx │ ├── Progress │ │ ├── index.ts │ │ └── Progress.tsx │ ├── CountDown │ │ └── index.ts │ ├── TextInput │ │ ├── index.ts │ │ ├── components │ │ │ ├── index.tsx │ │ │ ├── ErrorText.tsx │ │ │ └── CustomIcon.tsx │ │ └── constants.ts │ ├── Typography │ │ ├── index.ts │ │ └── Typography.tsx │ ├── Checkbox │ │ ├── index.ts │ │ └── constants.ts │ ├── CodeInput │ │ ├── index.ts │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── ErrorText.tsx │ │ │ └── HelperText.tsx │ │ └── Cursor.tsx │ ├── RadioButton │ │ ├── index.ts │ │ └── Bounceable.tsx │ ├── Slider │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── Track.tsx │ │ │ ├── TrackPoint.tsx │ │ │ └── Thumb.tsx │ │ ├── index.ts │ │ └── constants.ts │ ├── Accordion │ │ ├── index.ts │ │ ├── ToggleAnimation.ts │ │ ├── AccordionItem.tsx │ │ └── Accordion.tsx │ ├── Button │ │ ├── index.ts │ │ ├── ButtonPrimary.tsx │ │ ├── ButtonOutline.tsx │ │ ├── ButtonSecondary.tsx │ │ ├── ButtonTransparent.tsx │ │ └── Button.tsx │ └── index.ts ├── theme │ ├── components │ │ ├── TextInput │ │ │ ├── index.ts │ │ │ └── TextInput.ts │ │ ├── Button │ │ │ ├── index.ts │ │ │ ├── ButtonPrimary.ts │ │ │ ├── ButtonTransparent.ts │ │ │ ├── ButtonSecondary.ts │ │ │ ├── ButtonOutline.ts │ │ │ └── Button.ts │ │ ├── Text.ts │ │ ├── Checkbox.ts │ │ ├── Icon.ts │ │ ├── Card.ts │ │ ├── Progress.ts │ │ ├── Typography.ts │ │ ├── CountDown.ts │ │ ├── index.ts │ │ ├── Accordion.ts │ │ ├── RadioButton.ts │ │ ├── TextInput.ts │ │ ├── CodeInput.ts │ │ └── Slider.ts │ ├── index.ts │ ├── images.ts │ ├── base │ │ ├── opacity.ts │ │ ├── index.ts │ │ ├── sizes.ts │ │ ├── spacing.ts │ │ ├── borderWidths.ts │ │ ├── typography.ts │ │ ├── colors.ts │ │ └── shadows.ts │ └── theme.ts ├── hooks │ ├── index.ts │ ├── useTheme.tsx │ └── useBase.tsx ├── core │ ├── index.ts │ ├── color-mode │ │ └── type.ts │ ├── extendTheme.tsx │ └── BaseProvider.tsx ├── assets │ └── images │ │ └── check.png ├── __mocks__ │ └── react-native-reanimated.js ├── __tests__ │ ├── __snapshots__ │ │ └── Cursor.test.tsx.snap │ ├── TextBase.test.tsx │ ├── Typography.test.tsx │ ├── RadioButton.test.tsx │ ├── Checkbox.test.tsx │ └── Slider.test.tsx └── index.tsx ├── .prettierignore ├── example ├── hooks │ ├── useColorScheme.ts │ ├── useColorScheme.web.ts │ └── useThemeColor.ts ├── assets │ ├── images │ │ ├── icon.png │ │ ├── favicon.png │ │ ├── react-logo.png │ │ ├── adaptive-icon.png │ │ ├── react-logo@2x.png │ │ ├── react-logo@3x.png │ │ ├── splash-icon.png │ │ └── partial-react-logo.png │ └── fonts │ │ └── SpaceMono-Regular.ttf ├── components │ ├── ui │ │ ├── TabBarBackground.tsx │ │ ├── TabBarBackground.ios.tsx │ │ ├── IconSymbol.ios.tsx │ │ └── IconSymbol.tsx │ ├── ThemedView.tsx │ ├── HapticTab.tsx │ ├── CodeInputDemo │ │ ├── DisabledState.tsx │ │ ├── AdditionalFeatures.tsx │ │ ├── ValidationStates.tsx │ │ ├── utils.tsx │ │ ├── BasicExamples.tsx │ │ ├── index.tsx │ │ ├── IconsAndComponents.tsx │ │ ├── SecurityAndStyling.tsx │ │ └── InteractivePlayground.tsx │ ├── ExternalLink.tsx │ ├── HelloWave.tsx │ ├── Collapsible.tsx │ ├── ThemedText.tsx │ └── ParallaxScrollView.tsx ├── app │ ├── code-input.tsx │ └── _layout.tsx ├── eslint.config.js ├── tsconfig.json ├── .gitignore ├── constants │ ├── Colors.ts │ └── components.ts ├── app.json ├── metro.config.js ├── package.json ├── README.md ├── theme │ ├── demoColors.ts │ └── demoMetrics.ts └── scripts │ └── reset-project.js ├── jest.setupFilesAfterEnv.ts ├── .gitattributes ├── tsconfig.build.json ├── .husky ├── commit-msg └── pre-commit ├── .eslintignore ├── babel.config.js ├── .github ├── CODEOWNERS ├── pull_request_template.md ├── workflows │ └── on-pr-develop.yml └── actions │ └── setup │ └── action.yml ├── .prettierrc.js ├── jest.setup.js ├── .editorconfig ├── jest.config.js ├── scripts └── bootstrap.js ├── docs ├── component-text.md └── jest-config.md ├── tsconfig.json ├── .gitignore ├── .eslintrc.js ├── LICENSE ├── .cursor └── rules │ ├── project-structure.mdc │ ├── development-workflow.mdc │ ├── component-development.mdc │ ├── typescript-standards.mdc │ ├── theme-styling.mdc │ ├── testing-guidelines.mdc │ ├── accessibility.mdc │ └── performance.mdc ├── DEVELOPMENT.md ├── CONTRIBUTING.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.18.0 -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './metrics' 2 | -------------------------------------------------------------------------------- /src/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Card' 2 | -------------------------------------------------------------------------------- /src/components/Icon/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Icon' 2 | -------------------------------------------------------------------------------- /src/components/Text/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Text' 2 | -------------------------------------------------------------------------------- /src/components/Progress/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Progress' 2 | -------------------------------------------------------------------------------- /src/components/CountDown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CountDown' 2 | -------------------------------------------------------------------------------- /src/components/TextInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextInput' 2 | -------------------------------------------------------------------------------- /src/components/Typography/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Typography' 2 | -------------------------------------------------------------------------------- /src/theme/components/TextInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TextInput' 2 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme' 2 | export * from './images' 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ios 2 | android 3 | .github 4 | example 5 | lib 6 | example 7 | -------------------------------------------------------------------------------- /example/hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native'; 2 | -------------------------------------------------------------------------------- /jest.setupFilesAfterEnv.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-native/extend-expect' 2 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTheme' 2 | export * from './useBase' 3 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './extendTheme' 2 | export * from './BaseProvider' 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["example"] 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Checkbox' 2 | export * from './constants' 3 | -------------------------------------------------------------------------------- /src/components/CodeInput/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Cursor' 2 | export * from './CodeInput' 3 | -------------------------------------------------------------------------------- /src/components/RadioButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Bounceable' 2 | export * from './RadioButton' 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /src/components/TextInput/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './CustomIcon' 2 | export * from './ErrorText' 3 | -------------------------------------------------------------------------------- /src/theme/images.ts: -------------------------------------------------------------------------------- 1 | const Images = { 2 | check: require('../assets/images/check.png'), 3 | } 4 | 5 | export {Images} 6 | -------------------------------------------------------------------------------- /src/assets/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/src/assets/images/check.png -------------------------------------------------------------------------------- /example/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/example/assets/images/icon.png -------------------------------------------------------------------------------- /src/components/CodeInput/components/index.ts: -------------------------------------------------------------------------------- 1 | export {ErrorText} from './ErrorText' 2 | export {HelperText} from './HelperText' 3 | -------------------------------------------------------------------------------- /src/components/Slider/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Thumb' 2 | export * from './TrackPoint' 3 | export * from './Track' 4 | -------------------------------------------------------------------------------- /example/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/example/assets/images/favicon.png -------------------------------------------------------------------------------- /example/assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/example/assets/images/react-logo.png -------------------------------------------------------------------------------- /src/components/Accordion/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Accordion' 2 | export * from './AccordionItem' 3 | export * from './ToggleAnimation' 4 | -------------------------------------------------------------------------------- /example/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/example/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/example/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /example/assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/example/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /example/assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/example/assets/images/splash-icon.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn pretty && yarn lint:fix && yarn test 5 | yarn lint:staged 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | coverage/**/*.js 3 | storybook/storyLoader.js 4 | storybook-static 5 | scripts/bootstrap.js 6 | example 7 | lib 8 | example -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | plugins: ['react-native-reanimated/plugin'], 4 | } 5 | -------------------------------------------------------------------------------- /example/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/example/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /example/assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saigontechnology/rn-base-component/HEAD/example/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /src/__mocks__/react-native-reanimated.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | __esModule: true, 3 | 4 | ...jest.requireActual('react-native-reanimated/mock'), 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Slider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Slider' 2 | export * from './SliderFixed' 3 | export * from './SliderFixedRange' 4 | export * from './SliderRange' 5 | -------------------------------------------------------------------------------- /src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button' 2 | export * from './ButtonOutline' 3 | export * from './ButtonPrimary' 4 | export * from './ButtonSecondary' 5 | export * from './ButtonTransparent' 6 | -------------------------------------------------------------------------------- /src/theme/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Button' 2 | export * from './ButtonOutline' 3 | export * from './ButtonPrimary' 4 | export * from './ButtonSecondary' 5 | export * from './ButtonTransparent' 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. 2 | * @honghoangsts @hoangSTS @huydosgtech @loc-nguyenthien @loido @ThinhKimVo @ledutu2 @hangnguyensaigontech @danhmaisgt @ngochuyduong 3 | -------------------------------------------------------------------------------- /example/components/ui/TabBarBackground.tsx: -------------------------------------------------------------------------------- 1 | // This is a shim for web and Android where the tab bar is generally opaque. 2 | export default undefined; 3 | 4 | export function useBottomTabOverflow() { 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: false, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | semi: false, 8 | printWidth: 110, 9 | } 10 | -------------------------------------------------------------------------------- /example/app/code-input.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CodeInputDemo } from '../components/CodeInputDemo' 3 | 4 | const CodeInputDemoScreen = () => { 5 | return 6 | } 7 | 8 | export default CodeInputDemoScreen 9 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | global.ReanimatedDataMock = { 3 | now: () => Date.now(), 4 | } 5 | 6 | jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') 7 | 8 | require('react-native-reanimated/lib/module/reanimated2/jestUtils').setUpTests() 9 | -------------------------------------------------------------------------------- /src/components/Checkbox/constants.ts: -------------------------------------------------------------------------------- 1 | const BOUNCE_EFFECT_IN = 0.9 2 | const BOUNCE_EFFECT_OUT = 1 3 | const DISABLE_OPACITY = 0.5 4 | const DEFAULT_OPACITY = 1 5 | const DEFAULT_BOUNCE_EFFECT = 1 6 | 7 | export {BOUNCE_EFFECT_IN, BOUNCE_EFFECT_OUT, DISABLE_OPACITY, DEFAULT_OPACITY, DEFAULT_BOUNCE_EFFECT} 8 | -------------------------------------------------------------------------------- /src/theme/components/Button/ButtonPrimary.ts: -------------------------------------------------------------------------------- 1 | import base from '../../base' 2 | import {type ButtonThemeProps, ButtonTheme} from './Button' 3 | 4 | export const ButtonPrimaryTheme: ButtonThemeProps = { 5 | ...ButtonTheme, 6 | backgroundColor: base.colors.primary, 7 | textColor: base.colors.white, 8 | } 9 | -------------------------------------------------------------------------------- /src/theme/components/Button/ButtonTransparent.ts: -------------------------------------------------------------------------------- 1 | import base from '../../base' 2 | import {type ButtonThemeProps, ButtonTheme} from './Button' 3 | 4 | export const ButtonTransparentTheme: ButtonThemeProps = { 5 | ...ButtonTheme, 6 | backgroundColor: 'transparent', 7 | textColor: base.colors.primary, 8 | } 9 | -------------------------------------------------------------------------------- /src/theme/components/Button/ButtonSecondary.ts: -------------------------------------------------------------------------------- 1 | import base from '../../base' 2 | import {type ButtonThemeProps, ButtonTheme} from './Button' 3 | 4 | export const ButtonSecondaryTheme: ButtonThemeProps = { 5 | ...ButtonTheme, 6 | backgroundColor: base.colors.secondary, 7 | textColor: base.colors.white, 8 | } 9 | -------------------------------------------------------------------------------- /example/eslint.config.js: -------------------------------------------------------------------------------- 1 | // https://docs.expo.dev/guides/using-eslint/ 2 | const { defineConfig } = require('eslint/config'); 3 | const expoConfig = require('eslint-config-expo/flat'); 4 | 5 | module.exports = defineConfig([ 6 | expoConfig, 7 | { 8 | ignores: ['dist/*', 'node_modules/*'], 9 | }, 10 | ]); 11 | -------------------------------------------------------------------------------- /src/theme/components/TextInput/TextInput.ts: -------------------------------------------------------------------------------- 1 | import type {TextInputProps} from '../../../components/TextInput/TextInput' 2 | 3 | export type TextInputThemeProps = Pick< 4 | TextInputProps, 5 | 'containerStyle' | 'inputContainerStyle' | 'inputStyle' | 'labelStyle' 6 | > 7 | 8 | export const TextInputTheme: TextInputThemeProps = {} 9 | -------------------------------------------------------------------------------- /src/hooks/useTheme.tsx: -------------------------------------------------------------------------------- 1 | import {useTheme as useThemeStyled} from 'styled-components/native' 2 | 3 | export const useTheme = () => { 4 | const theme = useThemeStyled() 5 | 6 | if (!theme) { 7 | throw Error('`theme` is undefined. Seems you forgot to wrap your app in ``') 8 | } 9 | 10 | return theme 11 | } 12 | -------------------------------------------------------------------------------- /src/theme/components/Text.ts: -------------------------------------------------------------------------------- 1 | import type {TextProps} from '../../components' 2 | import {metrics} from '../../helpers' 3 | import base from '../base' 4 | 5 | export type TextThemeProps = Pick 6 | 7 | export const TextTheme: TextThemeProps = { 8 | fontSize: metrics.span, 9 | color: base.colors.black, 10 | } 11 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/Cursor.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Cursor Component Basic Rendering renders correctly with default props 1`] = ` 4 | 14 | | 15 | 16 | `; 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /src/hooks/useBase.tsx: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react' 2 | import {BaseContext, IBaseContext} from '../core/BaseProvider' 3 | 4 | export const useBase = () => { 5 | const base = useContext(BaseContext) 6 | 7 | if (!base) { 8 | throw Error('`base` is undefined. Seems you forgot to wrap your app in ``') 9 | } 10 | 11 | return base as IBaseContext 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Button/ButtonPrimary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {useTheme} from '../../hooks' 3 | import type {ButtonProps} from './Button' 4 | import {Button} from './Button' 5 | 6 | export const ButtonPrimary: React.FC = props => { 7 | const ButtonPrimaryTheme = useTheme().components.ButtonPrimary 8 | return ) 43 | expect(getByText('Test Button')).toBeTruthy() 44 | }) 45 | 46 | it('handles press events', () => { 47 | const mockPress = jest.fn() 48 | const {getByText} = renderWithProvider( 49 | 50 | ) 51 | 52 | fireEvent.press(getByText('Press Me')) 53 | expect(mockPress).toHaveBeenCalledTimes(1) 54 | }) 55 | 56 | it('applies correct styles for variants', () => { 57 | const {getByTestId} = renderWithProvider( 58 | 59 | ) 60 | 61 | const button = getByTestId('primary-button') 62 | // Test styling expectations 63 | }) 64 | }) 65 | ``` 66 | 67 | ## Snapshot Testing 68 | - Use snapshots sparingly, only for complex component structures 69 | - Update snapshots when making intentional changes: `yarn update-test` 70 | - Store snapshots in `src/__tests__/__snapshots__/` directory 71 | 72 | ## Coverage Requirements 73 | - Aim for high test coverage on public APIs 74 | - Focus on testing behavior, not implementation details 75 | - Test edge cases and error conditions 76 | - Ensure theme provider integration works correctly 77 | 78 | ## Mock Strategy 79 | - Mock external dependencies in `src/__mocks__/` 80 | - Mock react-native-reanimated as shown in [src/__mocks__/react-native-reanimated.js](mdc:src/__mocks__/react-native-reanimated.js) 81 | - Use jest.fn() for function props and callbacks 82 | - Mock complex native modules when needed 83 | 84 | ## Accessibility Testing 85 | - Test that components have proper accessibility props 86 | - Verify screen reader compatibility 87 | - Test keyboard navigation where applicable 88 | - Use accessibility testing utilities from Testing Library -------------------------------------------------------------------------------- /example/components/CodeInputDemo/InteractivePlayground.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react' 2 | import { Alert, Text, View } from 'react-native' 3 | import { Button, CodeInput, CodeInputRef } from 'rn-base-component' 4 | import { demoStyles } from './styles' 5 | 6 | export const InteractivePlayground = () => { 7 | const playgroundRef = useRef(null) 8 | const [playgroundValue, setPlaygroundValue] = useState('') 9 | const [hasError, setHasError] = useState(false) 10 | const [hasSuccess, setHasSuccess] = useState(false) 11 | const [isDisabled, setIsDisabled] = useState(false) 12 | const [isSecure, setIsSecure] = useState(false) 13 | 14 | const handlePlaygroundChange = (code: string) => { 15 | setPlaygroundValue(code) 16 | } 17 | 18 | const handlePlaygroundSubmit = (code: string) => { 19 | Alert.alert('Code Submitted', `You entered: ${code}`) 20 | setHasSuccess(true) 21 | setHasError(false) 22 | } 23 | 24 | const toggleError = () => { 25 | setHasError(!hasError) 26 | setHasSuccess(false) 27 | } 28 | 29 | const toggleSuccess = () => { 30 | setHasSuccess(!hasSuccess) 31 | setHasError(false) 32 | } 33 | 34 | const toggleDisabled = () => setIsDisabled(!isDisabled) 35 | const toggleSecure = () => setIsSecure(!isSecure) 36 | 37 | const clearInput = () => { 38 | playgroundRef.current?.clear() 39 | setPlaygroundValue('') 40 | setHasError(false) 41 | setHasSuccess(false) 42 | } 43 | 44 | const getCurrentValue = () => { 45 | const value = playgroundRef.current?.getValue() 46 | Alert.alert('Current Value', value || '(empty)') 47 | } 48 | 49 | return ( 50 | 51 | 🎮 Interactive Playground 52 | 53 | Test different states and configurations 54 | 55 | 56 | 71 | 72 | 73 | 76 | 79 | 82 | 85 | 88 | 91 | 92 | 93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /example/theme/demoColors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Demo App Color System 3 | * All colors used in the component demo application 4 | */ 5 | 6 | export const demoColors = { 7 | // Primary Colors 8 | primary: '#3b82f6', 9 | primaryLight: '#60a5fa', 10 | primaryDark: '#2563eb', 11 | primaryBackground: '#eff6ff', 12 | primaryBackgroundLight: '#f0f9ff', 13 | 14 | // Text Colors 15 | textPrimary: '#1a1a1a', 16 | textSecondary: '#64748b', 17 | textTertiary: '#94a3b8', 18 | textDark: '#1e293b', 19 | textLight: '#ffffff', 20 | 21 | // Background Colors 22 | background: '#f5f7fa', 23 | backgroundLight: '#f8f9fa', 24 | surface: '#ffffff', 25 | surfaceHover: '#fafafa', 26 | 27 | // Border Colors 28 | border: '#e2e8f0', 29 | borderLight: '#f1f5f9', 30 | borderDark: '#cbd5e1', 31 | 32 | // Status Colors 33 | success: '#10b981', 34 | successLight: '#d5f4e6', 35 | successBorder: '#27ae60', 36 | error: '#e74c3c', 37 | errorLight: '#fadbd8', 38 | errorBorder: '#e74c3c', 39 | warning: '#f39c12', 40 | warningLight: '#fff8e1', 41 | info: '#3498db', 42 | 43 | // Component Specific Colors 44 | // Purple theme 45 | purple: '#9b59b6', 46 | purpleDark: '#8e44ad', 47 | purpleLight: '#f8f3ff', 48 | purpleLighter: '#f0e6ff', 49 | purpleFilled: '#e8d5f5', 50 | 51 | // Gray theme 52 | gray: '#95a5a6', 53 | grayDark: '#7f8c8d', 54 | grayLight: '#ecf0f1', 55 | grayLighter: '#f5f5f5', 56 | grayBorder: '#bdc3c7', 57 | grayText: '#5d6d7e', 58 | grayPlaceholder: '#d5d8dc', 59 | 60 | // Orange/Yellow theme 61 | orange: '#f39c12', 62 | orangeDark: '#e67e22', 63 | orangeLight: '#fff8e1', 64 | yellow: '#ffd54f', 65 | yellowLight: '#ffe082', 66 | 67 | // Blue theme 68 | blue: '#3498db', 69 | blueDark: '#2980b9', 70 | blueLight: '#ebf5fb', 71 | blueLighter: '#d6eaf8', 72 | blueFilled: '#5dade2', 73 | blueActive: '#aed6f1', 74 | blueText: '#21618c', 75 | 76 | // Teal/Green theme 77 | teal: '#16a085', 78 | tealLight: '#1abc9c', 79 | tealLighter: '#e8f8f5', 80 | green: '#27ae60', 81 | greenDark: '#229954', 82 | greenLight: '#eafaf1', 83 | 84 | // Red theme 85 | red: '#c0392b', 86 | redLight: '#fadbd8', 87 | redFilled: '#f5b7b1', 88 | 89 | // Figma Light Style 90 | figmaLight: '#FAFAF9', 91 | figmaLightBorder: '#E5E5E5', 92 | figmaLightFilled: '#E0E0E0', 93 | figmaLightText: '#333333', 94 | 95 | // Shadow Colors 96 | shadow: '#000', 97 | shadowBlue: '#3498db', 98 | shadowDark: '#040A01', 99 | 100 | // Icon/Badge Colors 101 | iconBackground: '#ecf0f1', 102 | badgeBackground: '#f1f5f9', 103 | 104 | // Transparent 105 | transparent: 'transparent', 106 | } 107 | 108 | // Color aliases for semantic usage 109 | export const semanticColors = { 110 | // Text 111 | heading: demoColors.textPrimary, 112 | body: demoColors.textSecondary, 113 | caption: demoColors.textTertiary, 114 | 115 | // Backgrounds 116 | screen: demoColors.background, 117 | card: demoColors.surface, 118 | 119 | // Interactions 120 | active: demoColors.primary, 121 | hover: demoColors.surfaceHover, 122 | disabled: demoColors.grayLight, 123 | 124 | // Status 125 | success: demoColors.success, 126 | error: demoColors.error, 127 | warning: demoColors.warning, 128 | info: demoColors.info, 129 | } 130 | 131 | export type DemoColors = typeof demoColors 132 | export type SemanticColors = typeof semanticColors 133 | -------------------------------------------------------------------------------- /src/helpers/metrics.ts: -------------------------------------------------------------------------------- 1 | import {Dimensions, Platform} from 'react-native' 2 | 3 | export type IMetrics = { 4 | // Text Size 5 | readonly title: number 6 | readonly span: number 7 | 8 | // spacing 9 | readonly tiny: number 10 | readonly xxs: number 11 | readonly xs: number 12 | readonly small: number 13 | readonly sMedium: number 14 | readonly medium: number 15 | readonly large: number 16 | readonly xl: number 17 | readonly xxl: number 18 | readonly xxxl: number 19 | readonly huge: number 20 | readonly massive: number 21 | readonly giant: number 22 | 23 | readonly borderRadius: number 24 | readonly borderRadiusLarge: number 25 | readonly borderRadiusHuge: number 26 | 27 | // margin 28 | readonly marginTop: number 29 | readonly marginHorizontal: number 30 | readonly marginVertical: number 31 | readonly paddingHorizontal: number 32 | 33 | readonly voucherBorderRadius: number 34 | readonly logoWidth: number 35 | readonly logoHeight: number 36 | readonly icon: number 37 | } 38 | 39 | export type IHitSlop = { 40 | readonly top: number 41 | readonly bottom: number 42 | readonly right: number 43 | readonly left: number 44 | } 45 | 46 | const hitSlop: IHitSlop = { 47 | top: 10, 48 | bottom: 10, 49 | right: 10, 50 | left: 10, 51 | } 52 | 53 | const DESIGN_WIDTH = 375 54 | const DESIGN_HEIGHT = 812 55 | const {width, height} = Dimensions.get('window') 56 | 57 | const responsiveFont = (value: T) => ((width * value) / DESIGN_WIDTH) as T 58 | 59 | const deviceWidth = (): number => width 60 | 61 | const deviceHeight = (): number => height 62 | 63 | const responsiveWidth = (value: T) => ((width * value) / DESIGN_WIDTH) as T 64 | 65 | const responsiveHeight = (value: T) => ((height * value) / DESIGN_HEIGHT) as T 66 | 67 | export type IActiveOpacity = { 68 | readonly none: number 69 | readonly low: number 70 | readonly medium: number 71 | readonly high: number 72 | readonly veryHigh: number 73 | } 74 | 75 | const activeOpacity: IActiveOpacity = { 76 | none: 1, 77 | low: 0.8, 78 | medium: 0.6, 79 | high: 0.4, 80 | veryHigh: 0.2, 81 | } 82 | 83 | const isIOS: boolean = Platform.OS === 'ios' 84 | 85 | const metrics = { 86 | // Text Size 87 | title: responsiveFont(20), 88 | span: responsiveFont(14), 89 | 90 | // spacing 91 | line: responsiveHeight(1), 92 | tiny: responsiveHeight(4), 93 | xxs: responsiveHeight(8), 94 | xs: responsiveHeight(12), 95 | small: responsiveHeight(16), 96 | sMedium: responsiveHeight(18), 97 | medium: responsiveHeight(20), 98 | large: responsiveHeight(24), 99 | xl: responsiveHeight(28), 100 | xxl: responsiveHeight(32), 101 | xxxl: responsiveHeight(40), 102 | huge: responsiveHeight(48), 103 | massive: responsiveHeight(64), 104 | giant: responsiveHeight(80), 105 | 106 | borderRadius: responsiveHeight(5), 107 | borderRadiusLarge: responsiveHeight(10), 108 | borderRadiusHuge: responsiveHeight(20), 109 | // margin 110 | marginTop: responsiveHeight(12), 111 | marginHorizontal: responsiveWidth(24), 112 | marginVertical: responsiveWidth(16), 113 | paddingHorizontal: responsiveWidth(20), 114 | 115 | voucherBorderRadius: responsiveHeight(15), 116 | logoWidth: responsiveWidth(300), 117 | logoHeight: responsiveHeight(70), 118 | icon: responsiveHeight(30), 119 | } as const 120 | 121 | export { 122 | metrics, 123 | isIOS, 124 | hitSlop, 125 | activeOpacity, 126 | responsiveFont, 127 | responsiveHeight, 128 | responsiveWidth, 129 | deviceWidth, 130 | deviceHeight, 131 | } 132 | -------------------------------------------------------------------------------- /src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, {ReactNode} from 'react' 2 | import styled from 'styled-components/native' 3 | import { 4 | type StyleProp, 5 | StyleSheet, 6 | type TextStyle, 7 | type TouchableOpacityProps, 8 | type ViewStyle, 9 | } from 'react-native' 10 | import {useTheme} from '../../hooks' 11 | import {Text, TextProps} from '../Text/Text' 12 | 13 | export type ButtonProps = { 14 | /** 15 | * Color of the label 16 | */ 17 | textColor?: string 18 | /** 19 | * Color of the button background 20 | */ 21 | backgroundColor?: string 22 | /** 23 | * Disable interactions for the component 24 | */ 25 | disabled?: boolean 26 | /** 27 | * Color of the disabled button background 28 | */ 29 | disabledColor?: string 30 | /** 31 | * Button will have outline style 32 | */ 33 | outline?: boolean 34 | /** 35 | * The outline color 36 | */ 37 | outlineColor?: string 38 | /** 39 | * The outline width 40 | */ 41 | outlineWidth?: number 42 | /** 43 | * Custom border radius. 44 | */ 45 | borderRadius?: number 46 | /** 47 | * The size of text. 48 | */ 49 | textSize?: number 50 | /** 51 | * Custom left/right icon 52 | */ 53 | leftIcon?: ReactNode 54 | rightIcon?: ReactNode 55 | /** 56 | * Custom text props. 57 | */ 58 | textProps?: TextProps 59 | /** 60 | * Custom text style. 61 | */ 62 | textStyle?: StyleProp 63 | /** 64 | * Custom container style. 65 | */ 66 | style?: StyleProp 67 | } & TouchableOpacityProps 68 | 69 | export const Button: React.FC = ({ 70 | textColor, 71 | backgroundColor, 72 | outline, 73 | outlineColor, 74 | outlineWidth, 75 | borderRadius, 76 | disabled, 77 | disabledColor, 78 | textProps, 79 | textStyle, 80 | style, 81 | leftIcon, 82 | rightIcon, 83 | children, 84 | ...props 85 | }) => { 86 | const ButtonTheme = useTheme().components.Button 87 | return ( 88 | 100 | {!!leftIcon && leftIcon} 101 | {typeof children === 'string' ? ( 102 | 105 | ) : ( 106 | children 107 | )} 108 | {!!rightIcon && rightIcon} 109 | 110 | ) 111 | } 112 | 113 | const ButtonWrapper = styled.TouchableOpacity>( 114 | ({theme, backgroundColor, outline, outlineWidth, outlineColor, borderRadius, disabled}) => ({ 115 | paddingVertical: theme.spacing.small, 116 | flexDirection: 'row', 117 | paddingHorizontal: theme.spacing.slim, 118 | borderRadius, 119 | backgroundColor, 120 | justifyContent: 'center', 121 | alignItems: 'center', 122 | width: '100%', 123 | alignSelf: 'center', 124 | ...(outline && { 125 | borderWidth: outlineWidth || 1, 126 | borderColor: disabled ? theme.colors.gray : outlineColor || theme.colors.primaryBorder, 127 | }), 128 | }), 129 | ) 130 | 131 | const Label = styled(Text)<{color?: string}>(({theme, color}) => ({ 132 | color, 133 | fontWeight: theme.fontWeights.bold, 134 | })) 135 | -------------------------------------------------------------------------------- /.cursor/rules/accessibility.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | alwaysApply: true 3 | --- 4 | # Accessibility Guidelines 5 | 6 | ## Core Accessibility Requirements 7 | All components must be accessible and follow React Native accessibility best practices. 8 | 9 | ## Required Accessibility Props 10 | - `testID` - For automated testing and element identification 11 | - `accessible` - Mark components as accessible elements 12 | - `accessibilityLabel` - Descriptive text for screen readers 13 | - `accessibilityHint` - Additional context when needed 14 | - `accessibilityRole` - Semantic role (button, text, image, etc.) 15 | - `accessibilityState` - Dynamic states (disabled, selected, checked) 16 | 17 | ## Component-Specific Guidelines 18 | 19 | ### Interactive Components (Button, TouchableOpacity) 20 | ```typescript 21 | 30 | ``` 31 | 32 | ### Form Elements (TextInput, Checkbox, RadioButton) 33 | ```typescript 34 | 41 | 42 | 48 | ``` 49 | 50 | ### Informational Components (Text, Icon) 51 | ```typescript 52 | 57 | 58 | 62 | Error: Invalid input 63 | 64 | ``` 65 | 66 | ## Color and Contrast 67 | - Ensure sufficient color contrast (WCAG 2.1 AA standards) 68 | - Don't rely solely on color to convey information 69 | - Support high contrast mode preferences 70 | - Test in both light and dark themes 71 | 72 | ## Focus Management 73 | - Ensure proper focus order for keyboard navigation 74 | - Provide visible focus indicators 75 | - Handle focus trapping in modals/overlays 76 | - Support focus management in complex components 77 | 78 | ## Screen Reader Support 79 | - Provide meaningful content descriptions 80 | - Use semantic HTML equivalents where possible 81 | - Group related elements with proper labeling 82 | - Handle dynamic content announcements 83 | 84 | ## Testing Accessibility 85 | - Test with screen readers (TalkBack on Android, VoiceOver on iOS) 86 | - Verify keyboard navigation works properly 87 | - Check color contrast ratios 88 | - Test with high contrast and large text settings 89 | - Use accessibility inspector tools 90 | 91 | ## Common Patterns 92 | ```typescript 93 | // Grouping related elements 94 | 98 | Profile 99 | User details... 100 | 101 | 102 | // Custom accessibility announcements 103 | const announceChange = (message: string) => { 104 | AccessibilityInfo.announceForAccessibility(message) 105 | } 106 | 107 | // Conditional accessibility 108 | 114 | ``` 115 | 116 | ## Documentation Requirements 117 | - Document accessibility features in component README files 118 | - Include accessibility examples in component demos 119 | - Test and document keyboard shortcuts where applicable -------------------------------------------------------------------------------- /example/constants/components.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Component Library Demo Constants 3 | * List of all components available in the demo app 4 | */ 5 | 6 | export interface ComponentItem { 7 | id: string 8 | name: string 9 | description: string 10 | route?: string 11 | icon: string 12 | category: 'Input' | 'Display' | 'Navigation' | 'Feedback' 13 | status: 'Complete' | 'Coming Soon' 14 | } 15 | 16 | export const COMPONENTS: ComponentItem[] = [ 17 | { 18 | id: 'code-input', 19 | name: 'CodeInput', 20 | description: 'Verification code input with multiple styling options', 21 | route: '/code-input', 22 | icon: '🔢', 23 | category: 'Input', 24 | status: 'Complete', 25 | }, 26 | { 27 | id: 'text-input', 28 | name: 'TextInput', 29 | description: 'Text input fields with flat and outlined variants', 30 | icon: '📝', 31 | category: 'Input', 32 | status: 'Coming Soon', 33 | }, 34 | { 35 | id: 'button', 36 | name: 'Button', 37 | description: 'Various button styles and variants', 38 | icon: '🔘', 39 | category: 'Input', 40 | status: 'Coming Soon', 41 | }, 42 | { 43 | id: 'checkbox', 44 | name: 'Checkbox', 45 | description: 'Checkboxes with customizable styles', 46 | icon: '☑️', 47 | category: 'Input', 48 | status: 'Coming Soon', 49 | }, 50 | { 51 | id: 'radio-button', 52 | name: 'RadioButton', 53 | description: 'Radio button selections', 54 | icon: '🔘', 55 | category: 'Input', 56 | status: 'Coming Soon', 57 | }, 58 | { 59 | id: 'slider', 60 | name: 'Slider', 61 | description: 'Range and fixed sliders', 62 | icon: '🎚️', 63 | category: 'Input', 64 | status: 'Coming Soon', 65 | }, 66 | { 67 | id: 'card', 68 | name: 'Card', 69 | description: 'Card container component', 70 | icon: '🃏', 71 | category: 'Display', 72 | status: 'Coming Soon', 73 | }, 74 | { 75 | id: 'text', 76 | name: 'Text', 77 | description: 'Themed text component', 78 | icon: '📄', 79 | category: 'Display', 80 | status: 'Coming Soon', 81 | }, 82 | { 83 | id: 'typography', 84 | name: 'Typography', 85 | description: 'Typography styles and variants', 86 | icon: '✍️', 87 | category: 'Display', 88 | status: 'Coming Soon', 89 | }, 90 | { 91 | id: 'icon', 92 | name: 'Icon', 93 | description: 'Icon component system', 94 | icon: '⭐', 95 | category: 'Display', 96 | status: 'Coming Soon', 97 | }, 98 | { 99 | id: 'accordion', 100 | name: 'Accordion', 101 | description: 'Expandable accordion panels', 102 | icon: '📋', 103 | category: 'Navigation', 104 | status: 'Coming Soon', 105 | }, 106 | { 107 | id: 'progress', 108 | name: 'Progress', 109 | description: 'Progress indicators and bars', 110 | icon: '📊', 111 | category: 'Feedback', 112 | status: 'Coming Soon', 113 | }, 114 | { 115 | id: 'countdown', 116 | name: 'CountDown', 117 | description: 'Countdown timer component', 118 | icon: '⏱️', 119 | category: 'Feedback', 120 | status: 'Coming Soon', 121 | }, 122 | ] 123 | 124 | // Helper functions 125 | export const getComponentById = (id: string): ComponentItem | undefined => { 126 | return COMPONENTS.find(component => component.id === id) 127 | } 128 | 129 | export const getComponentsByCategory = (category: ComponentItem['category']): ComponentItem[] => { 130 | return COMPONENTS.filter(component => component.category === category) 131 | } 132 | 133 | export const getCompletedComponents = (): ComponentItem[] => { 134 | return COMPONENTS.filter(component => component.status === 'Complete') 135 | } 136 | 137 | export const getComponentsByStatus = (status: ComponentItem['status']): ComponentItem[] => { 138 | return COMPONENTS.filter(component => component.status === status) 139 | } 140 | -------------------------------------------------------------------------------- /src/theme/components/CodeInput.ts: -------------------------------------------------------------------------------- 1 | import type {StyleProp, ViewStyle, TextStyle} from 'react-native' 2 | import {metrics} from '../../helpers' 3 | import base from '../base' 4 | 5 | export type CodeInputThemeProps = { 6 | /** 7 | * Style for individual cell 8 | */ 9 | cellStyle?: StyleProp 10 | /** 11 | * Style for cell when it has a value 12 | */ 13 | filledCellStyle?: StyleProp 14 | /** 15 | * Style for cell when it's focused 16 | */ 17 | focusCellStyle?: StyleProp 18 | /** 19 | * Style for text inside cells 20 | */ 21 | textStyle?: StyleProp 22 | /** 23 | * Style for text when cell is focused 24 | */ 25 | focusTextStyle?: StyleProp 26 | /** 27 | * Style for secure text entry dots 28 | */ 29 | secureViewStyle?: StyleProp 30 | /** 31 | * Style for the container holding all cells 32 | */ 33 | cellContainerStyle?: StyleProp 34 | /** 35 | * Style for wrapper around each cell 36 | */ 37 | cellWrapperStyle?: StyleProp 38 | /** 39 | * Style for wrapper around focused cell 40 | */ 41 | focusCellWrapperStyle?: StyleProp 42 | /** 43 | * Color for placeholder text 44 | */ 45 | placeholderTextColor: string 46 | /** 47 | * Style for placeholder dot 48 | */ 49 | placeholderDotStyle?: StyleProp 50 | /** 51 | * Style for outer container 52 | */ 53 | containerStyle?: StyleProp 54 | /** 55 | * Styling for the label 56 | */ 57 | labelStyle?: StyleProp 58 | /** 59 | * Style for cell in error state 60 | */ 61 | errorCellStyle?: StyleProp 62 | /** 63 | * Style for cell in success state 64 | */ 65 | successCellStyle?: StyleProp 66 | /** 67 | * Style for cell in disabled state 68 | */ 69 | disabledCellStyle?: StyleProp 70 | /** 71 | * Style for cell in active state (alias for focusCellStyle) 72 | */ 73 | activeCellStyle?: StyleProp 74 | } 75 | 76 | export const CodeInputTheme: CodeInputThemeProps = { 77 | cellStyle: { 78 | width: 50, 79 | height: 50, 80 | borderWidth: 1, 81 | borderColor: base.colors.primaryBorder, 82 | borderRadius: metrics.borderRadius, 83 | backgroundColor: base.colors.white, 84 | justifyContent: 'center', 85 | alignItems: 'center', 86 | }, 87 | filledCellStyle: { 88 | borderColor: base.colors.primary, 89 | }, 90 | focusCellStyle: { 91 | borderColor: base.colors.primary, 92 | borderWidth: 2, 93 | }, 94 | textStyle: { 95 | fontSize: 18, 96 | fontWeight: 'normal', 97 | color: base.colors.black, 98 | }, 99 | focusTextStyle: { 100 | color: base.colors.primary, 101 | }, 102 | secureViewStyle: { 103 | width: 8, 104 | height: 8, 105 | borderRadius: 4, 106 | backgroundColor: base.colors.black, 107 | }, 108 | cellContainerStyle: { 109 | flexDirection: 'row', 110 | justifyContent: 'space-between', 111 | }, 112 | cellWrapperStyle: undefined, 113 | focusCellWrapperStyle: undefined, 114 | placeholderTextColor: base.colors.gray, 115 | placeholderDotStyle: { 116 | width: 6, 117 | height: 6, 118 | borderRadius: 3, 119 | backgroundColor: base.colors.gray, 120 | }, 121 | containerStyle: undefined, 122 | labelStyle: { 123 | fontSize: 14, 124 | color: base.colors.darkText, 125 | marginBottom: 8, 126 | }, 127 | errorCellStyle: { 128 | borderColor: base.colors.error, 129 | borderWidth: 2, 130 | }, 131 | successCellStyle: { 132 | borderColor: base.colors.success, 133 | borderWidth: 2, 134 | }, 135 | disabledCellStyle: { 136 | backgroundColor: '#f5f5f5', 137 | borderColor: base.colors.primaryBorder, 138 | opacity: 0.5, 139 | }, 140 | activeCellStyle: { 141 | borderColor: base.colors.primary, 142 | borderWidth: 2, 143 | }, 144 | } 145 | -------------------------------------------------------------------------------- /src/components/Slider/components/Thumb.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {GestureDetector, PanGesture} from 'react-native-gesture-handler' 3 | import styled from 'styled-components/native' 4 | import Animated, {useAnimatedProps, useAnimatedStyle} from 'react-native-reanimated' 5 | import type {Position, Size, TextAlign, ThumbContainerStyle} from '../Slider' 6 | import {isIOS, metrics, responsiveHeight} from '../../../helpers/metrics' 7 | import {TextInput, type StyleProp, type TextProps, type ViewStyle} from 'react-native' 8 | import type {ITheme} from '../../../theme' 9 | 10 | interface ThumbProps { 11 | text: string 12 | bgColorLabelView?: string 13 | labelStyle: StyleProp 14 | thumbSize: Size 15 | thumbStyle: StyleProp 16 | thumbComponent?: React.ReactElement 17 | alwaysShowValue?: boolean 18 | animatedThumbStyle: ReturnType 19 | opacityStyle: ReturnType 20 | animatedProps: ReturnType 21 | onGestureEvent: PanGesture 22 | } 23 | 24 | const Thumb: React.FunctionComponent = ({ 25 | text, 26 | bgColorLabelView, 27 | labelStyle, 28 | alwaysShowValue, 29 | thumbSize, 30 | thumbComponent, 31 | animatedProps, 32 | thumbStyle, 33 | animatedThumbStyle, 34 | opacityStyle, 35 | onGestureEvent, 36 | }) => ( 37 | 38 | 42 | 46 | {/* background is not existing */} 47 | 48 | 50 | {thumbComponent} 51 | 52 | 53 | ) 54 | 55 | const ThumbContainer = styled(Animated.View)(props => ({ 56 | position: 'absolute' as Position, 57 | height: props.thumbSize.height, 58 | width: props.thumbSize.width, 59 | borderRadius: props.theme.borderWidths?.huge, 60 | borderWidth: props.hasThumbComponent ? 0 : 1, 61 | // backgroundColor: props.hasThumbComponent ? 'transparent' : props.theme?.colors.backgroundColor, 62 | backgroundColor: 'transparent', 63 | })) 64 | 65 | const TriangleDown = styled.View(({background, theme}: {background?: string; theme: ITheme}) => ({ 66 | position: 'absolute' as Position, 67 | bottom: -5, 68 | width: 0, 69 | height: 0, 70 | backgroundColor: 'transparent', 71 | borderStyle: 'solid', 72 | borderLeftWidth: 5, 73 | borderRightWidth: 5, 74 | borderBottomWidth: 10, 75 | borderLeftColor: 'transparent', 76 | borderRightColor: 'transparent', 77 | borderBottomColor: background || theme?.colors?.primary, 78 | transform: [{rotate: '180deg'}] as unknown as string, 79 | })) 80 | 81 | const LabelContainer = styled(Animated.View)(props => ({ 82 | position: 'absolute' as Position, 83 | top: -responsiveHeight(props.theme?.spacing?.titanic || 0), 84 | bottom: props.thumbSize.height + metrics.xxs, 85 | borderRadius: props.theme?.borderWidths?.compact, 86 | backgroundColor: props.background || props.theme?.colors?.primary, 87 | alignSelf: 'center', 88 | justifyContent: 'center', 89 | alignItems: 'center', 90 | margin: !isIOS ? -(responsiveHeight(props.theme?.spacing.tiny || 0) || 0) : 0, 91 | })) 92 | 93 | const Label = styled(Animated.createAnimatedComponent(TextInput))(({theme}: {theme: ITheme}) => ({ 94 | color: theme.colors.white, 95 | padding: responsiveHeight(isIOS ? theme.borderWidths.small : theme.spacing.tiny), 96 | textAlign: 'center' as TextAlign, 97 | fontWeight: theme.fontWeights.bold, 98 | fontSize: theme.fontSizes.sm, 99 | width: '100%', 100 | })) 101 | 102 | export {Thumb} 103 | -------------------------------------------------------------------------------- /example/scripts/reset-project.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * This script is used to reset the project to a blank state. 5 | * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file. 6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it. 7 | */ 8 | 9 | const fs = require("fs"); 10 | const path = require("path"); 11 | const readline = require("readline"); 12 | 13 | const root = process.cwd(); 14 | const oldDirs = ["app", "components", "hooks", "constants", "scripts"]; 15 | const exampleDir = "app-example"; 16 | const newAppDir = "app"; 17 | const exampleDirPath = path.join(root, exampleDir); 18 | 19 | const indexContent = `import { Text, View } from "react-native"; 20 | 21 | export default function Index() { 22 | return ( 23 | 30 | Edit app/index.tsx to edit this screen. 31 | 32 | ); 33 | } 34 | `; 35 | 36 | const layoutContent = `import { Stack } from "expo-router"; 37 | 38 | export default function RootLayout() { 39 | return ; 40 | } 41 | `; 42 | 43 | const rl = readline.createInterface({ 44 | input: process.stdin, 45 | output: process.stdout, 46 | }); 47 | 48 | const moveDirectories = async (userInput) => { 49 | try { 50 | if (userInput === "y") { 51 | // Create the app-example directory 52 | await fs.promises.mkdir(exampleDirPath, { recursive: true }); 53 | console.log(`📁 /${exampleDir} directory created.`); 54 | } 55 | 56 | // Move old directories to new app-example directory or delete them 57 | for (const dir of oldDirs) { 58 | const oldDirPath = path.join(root, dir); 59 | if (fs.existsSync(oldDirPath)) { 60 | if (userInput === "y") { 61 | const newDirPath = path.join(root, exampleDir, dir); 62 | await fs.promises.rename(oldDirPath, newDirPath); 63 | console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`); 64 | } else { 65 | await fs.promises.rm(oldDirPath, { recursive: true, force: true }); 66 | console.log(`❌ /${dir} deleted.`); 67 | } 68 | } else { 69 | console.log(`➡️ /${dir} does not exist, skipping.`); 70 | } 71 | } 72 | 73 | // Create new /app directory 74 | const newAppDirPath = path.join(root, newAppDir); 75 | await fs.promises.mkdir(newAppDirPath, { recursive: true }); 76 | console.log("\n📁 New /app directory created."); 77 | 78 | // Create index.tsx 79 | const indexPath = path.join(newAppDirPath, "index.tsx"); 80 | await fs.promises.writeFile(indexPath, indexContent); 81 | console.log("📄 app/index.tsx created."); 82 | 83 | // Create _layout.tsx 84 | const layoutPath = path.join(newAppDirPath, "_layout.tsx"); 85 | await fs.promises.writeFile(layoutPath, layoutContent); 86 | console.log("📄 app/_layout.tsx created."); 87 | 88 | console.log("\n✅ Project reset complete. Next steps:"); 89 | console.log( 90 | `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${ 91 | userInput === "y" 92 | ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.` 93 | : "" 94 | }` 95 | ); 96 | } catch (error) { 97 | console.error(`❌ Error during script execution: ${error.message}`); 98 | } 99 | }; 100 | 101 | rl.question( 102 | "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ", 103 | (answer) => { 104 | const userInput = answer.trim().toLowerCase() || "y"; 105 | if (userInput === "y" || userInput === "n") { 106 | moveDirectories(userInput).finally(() => rl.close()); 107 | } else { 108 | console.log("❌ Invalid input. Please enter 'Y' or 'N'."); 109 | rl.close(); 110 | } 111 | } 112 | ); 113 | -------------------------------------------------------------------------------- /src/theme/components/Slider.ts: -------------------------------------------------------------------------------- 1 | import type {StyleProp, ViewStyle, TextStyle, Insets} from 'react-native' 2 | // metrics available but not used in default theme values 3 | import base from '../base' 4 | 5 | type Size = { 6 | width: number 7 | height: number 8 | } 9 | 10 | export type SliderThemeProps = { 11 | /** 12 | * The maximum value of the slider 13 | */ 14 | maximumValue: number 15 | /** 16 | * The minimum value of the slider 17 | */ 18 | minimumValue: number 19 | /** 20 | * The step value for the slider 21 | */ 22 | step: number 23 | /** 24 | * The alwaysShowValue indicates whether the value of the slider should always be displayed 25 | */ 26 | alwaysShowValue: boolean 27 | /** 28 | * Whether to show the point on the slider's track 29 | */ 30 | showTrackPoint: boolean 31 | /** 32 | * Determines whether the thumb can be moved by directly touching the thumb or only by dragging the slider track 33 | */ 34 | tapToSeek: boolean 35 | /** 36 | * The touchable area is used to increase the size of the thumb and make it easier to interact with 37 | */ 38 | hitSlopPoint?: Insets | number 39 | /** 40 | * Style for the slider component 41 | */ 42 | style?: StyleProp 43 | /** 44 | * Style of the slider's track 45 | */ 46 | trackStyle?: StyleProp 47 | /** 48 | * Style for the track's filled portion 49 | */ 50 | trackedStyle?: StyleProp 51 | /** 52 | * Style of the point on the slider's track 53 | */ 54 | trackPointStyle?: StyleProp 55 | /** 56 | * The bgColorLabelView sets the background color of the view that displays the value of the slider 57 | */ 58 | bgColorLabelView: string 59 | /** 60 | * The labelStyle sets the style of the text that displays the value of the slider 61 | */ 62 | labelStyle?: StyleProp 63 | /** 64 | * Style of the slider's thumb 65 | */ 66 | thumbStyle?: StyleProp 67 | /** 68 | * Size of the slider's thumb 69 | */ 70 | thumbSize: Size 71 | /** 72 | * Track height 73 | */ 74 | trackHeight: number 75 | /** 76 | * Track background color 77 | */ 78 | trackBackgroundColor: string 79 | /** 80 | * Track filled color 81 | */ 82 | trackFilledColor: string 83 | /** 84 | * Thumb background color 85 | */ 86 | thumbBackgroundColor: string 87 | /** 88 | * Thumb border color 89 | */ 90 | thumbBorderColor: string 91 | /** 92 | * Thumb border width 93 | */ 94 | thumbBorderWidth: number 95 | } 96 | 97 | const DEFAULT_MINIMUM_VALUE = 0 98 | const DEFAULT_MAXIMUM_VALUE = 100 99 | const DEFAULT_STEP = 1 100 | 101 | export const SliderTheme: SliderThemeProps = { 102 | maximumValue: DEFAULT_MAXIMUM_VALUE, 103 | minimumValue: DEFAULT_MINIMUM_VALUE, 104 | step: DEFAULT_STEP, 105 | alwaysShowValue: false, 106 | showTrackPoint: false, 107 | tapToSeek: true, 108 | hitSlopPoint: {top: 10, bottom: 10, left: 10, right: 10}, 109 | style: undefined, 110 | trackStyle: { 111 | height: 4, 112 | borderRadius: 2, 113 | backgroundColor: base.colors.backgroundSecondary, 114 | }, 115 | trackedStyle: { 116 | backgroundColor: base.colors.primary, 117 | }, 118 | trackPointStyle: { 119 | width: 2, 120 | height: 4, 121 | backgroundColor: base.colors.gray, 122 | }, 123 | bgColorLabelView: base.colors.black, 124 | labelStyle: { 125 | color: base.colors.white, 126 | fontSize: 12, 127 | }, 128 | thumbStyle: { 129 | backgroundColor: base.colors.white, 130 | borderColor: base.colors.primary, 131 | borderWidth: 2, 132 | shadowColor: base.colors.black, 133 | shadowOffset: {width: 0, height: 2}, 134 | shadowOpacity: 0.2, 135 | shadowRadius: 2, 136 | elevation: 3, 137 | }, 138 | thumbSize: { 139 | width: 20, 140 | height: 20, 141 | }, 142 | trackHeight: 4, 143 | trackBackgroundColor: base.colors.backgroundSecondary, 144 | trackFilledColor: base.colors.primary, 145 | thumbBackgroundColor: base.colors.white, 146 | thumbBorderColor: base.colors.primary, 147 | thumbBorderWidth: 2, 148 | } 149 | -------------------------------------------------------------------------------- /src/__tests__/Slider.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Slider} from '../components' 3 | 4 | // Mock react-native-reanimated 5 | jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock')) 6 | 7 | // Mock react-native-gesture-handler 8 | jest.mock('react-native-gesture-handler', () => { 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const {View} = require('react-native') 11 | 12 | return { 13 | GestureHandlerRootView: ({children, ...props}: React.PropsWithChildren>) => ( 14 | {children} 15 | ), 16 | GestureDetector: ({children}: React.PropsWithChildren) => children, 17 | Gesture: { 18 | Pan: () => ({ 19 | onStart: jest.fn().mockReturnThis(), 20 | onUpdate: jest.fn().mockReturnThis(), 21 | onEnd: jest.fn().mockReturnThis(), 22 | }), 23 | }, 24 | State: {}, 25 | } 26 | }) 27 | 28 | describe('Slider Component Tests', () => { 29 | describe('Component Structure', () => { 30 | it('exports the main Slider component', () => { 31 | expect(Slider).toBeDefined() 32 | expect(typeof Slider).toBe('function') 33 | }) 34 | 35 | it('has compound components attached', () => { 36 | expect(Slider.Range).toBeDefined() 37 | expect(typeof Slider.Range).toBe('function') 38 | 39 | expect(Slider.FixedRange).toBeDefined() 40 | expect(typeof Slider.FixedRange).toBe('function') 41 | 42 | expect(Slider.Fixed).toBeDefined() 43 | expect(typeof Slider.Fixed).toBe('function') 44 | }) 45 | }) 46 | 47 | describe('Props Interface', () => { 48 | it('accepts standard slider props without errors', () => { 49 | // Test that we can create instances with various props without TypeScript errors 50 | const propsTest = { 51 | minimumValue: 0, 52 | maximumValue: 100, 53 | step: 1, 54 | onValueChange: jest.fn(), 55 | showTrackPoint: true, 56 | tapToSeek: true, 57 | sliderWidth: 200, 58 | alwaysShowValue: false, 59 | roundToValue: 2, 60 | } 61 | 62 | // Just testing that props are correctly typed 63 | expect(typeof propsTest.minimumValue).toBe('number') 64 | expect(typeof propsTest.maximumValue).toBe('number') 65 | expect(typeof propsTest.step).toBe('number') 66 | expect(typeof propsTest.onValueChange).toBe('function') 67 | expect(typeof propsTest.showTrackPoint).toBe('boolean') 68 | expect(typeof propsTest.tapToSeek).toBe('boolean') 69 | expect(typeof propsTest.sliderWidth).toBe('number') 70 | expect(typeof propsTest.alwaysShowValue).toBe('boolean') 71 | expect(typeof propsTest.roundToValue).toBe('number') 72 | }) 73 | }) 74 | 75 | describe('Compound Components Tests', () => { 76 | describe('Slider.Range', () => { 77 | it('is available as a compound component', () => { 78 | expect(Slider.Range).toBeDefined() 79 | expect(typeof Slider.Range).toBe('function') 80 | expect(Slider.Range.name).toBe('SliderRange') 81 | }) 82 | }) 83 | 84 | describe('Slider.FixedRange', () => { 85 | it('is available as a compound component', () => { 86 | expect(Slider.FixedRange).toBeDefined() 87 | expect(typeof Slider.FixedRange).toBe('function') 88 | expect(Slider.FixedRange.name).toBe('SliderFixedRange') 89 | }) 90 | }) 91 | 92 | describe('Slider.Fixed', () => { 93 | it('is available as a compound component', () => { 94 | expect(Slider.Fixed).toBeDefined() 95 | expect(typeof Slider.Fixed).toBe('function') 96 | expect(Slider.Fixed.name).toBe('SliderFixed') 97 | }) 98 | }) 99 | }) 100 | 101 | describe('Component Exports', () => { 102 | it('maintains consistent API structure', () => { 103 | // Verify that the Slider component has all expected compound components 104 | const expectedCompoundComponents = ['Range', 'FixedRange', 'Fixed'] 105 | 106 | expectedCompoundComponents.forEach(componentName => { 107 | expect(Slider).toHaveProperty(componentName) 108 | expect(typeof Slider[componentName as keyof typeof Slider]).toBe('function') 109 | }) 110 | }) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development Setup 2 | 3 | This document explains how to set up the development environment for hot reloading between the library source code and the example app. 4 | 5 | ## Problem Solved 6 | 7 | Previously, when making changes to components in `src/components/`, the example app wouldn't reflect changes immediately. You had to: 8 | 9 | 1. Remove `node_modules` in the example directory 10 | 2. Run `yarn install` again 11 | 3. Wait for the rebuild process 12 | 13 | This was time-consuming and disrupted the development flow. 14 | 15 | ## Solution 16 | 17 | The development setup now directly resolves to the source code in `src/` instead of the built version in `lib/`, enabling hot reloading. 18 | 19 | ## Configuration Changes 20 | 21 | ### 1. Metro Configuration (`example/metro.config.js`) 22 | 23 | ```javascript 24 | // Add aliases to resolve to source code during development 25 | config.resolver.alias = { 26 | 'rn-base-component': path.resolve(workspaceRoot, 'src'), 27 | } 28 | 29 | // Watch the parent directory (the library source code) 30 | config.watchFolders = [workspaceRoot] 31 | ``` 32 | 33 | This tells Metro to: 34 | 35 | - Resolve `rn-base-component` imports to `../src` instead of the built package 36 | - Watch the entire workspace for changes 37 | 38 | ### 2. TypeScript Configuration (`example/tsconfig.json`) 39 | 40 | ```json 41 | { 42 | "compilerOptions": { 43 | "paths": { 44 | "rn-base-component": ["../src"], 45 | "rn-base-component/*": ["../src/*"] 46 | } 47 | }, 48 | "include": ["../src/**/*.ts", "../src/**/*.tsx"] 49 | } 50 | ``` 51 | 52 | This ensures TypeScript: 53 | 54 | - Resolves types correctly from the source 55 | - Includes the source files in type checking 56 | 57 | ## How It Works 58 | 59 | 1. **Import Resolution**: When the example app imports `from 'rn-base-component'`, Metro resolves it to `../src/index.tsx` instead of the built package. 60 | 61 | 2. **Hot Reloading**: Since Metro watches the source directory, any changes to components in `src/components/` trigger a hot reload in the example app. 62 | 63 | 3. **Type Safety**: TypeScript can now provide accurate IntelliSense and type checking based on the actual source code. 64 | 65 | ## Development Workflow 66 | 67 | 1. **Start the example app**: 68 | 69 | ```bash 70 | cd example 71 | npx expo start --clear 72 | ``` 73 | 74 | 2. **Make changes to components** in `src/components/`: 75 | 76 | - Edit any component file 77 | - Save the file 78 | - The example app will hot reload automatically 79 | 80 | 3. **No more manual rebuilds** required during development! 81 | 82 | ## File Structure 83 | 84 | ``` 85 | rn-base-component/ 86 | ├── src/ # Source code (development) 87 | │ ├── components/ 88 | │ │ ├── CountDown/ 89 | │ │ ├── Button/ 90 | │ │ └── ... 91 | │ └── index.tsx 92 | ├── lib/ # Built code (production) 93 | │ ├── commonjs/ 94 | │ ├── module/ 95 | │ └── typescript/ 96 | └── example/ # Example app 97 | ├── metro.config.js # Configured for source resolution 98 | ├── tsconfig.json # Configured for source types 99 | └── app/ 100 | └── index.tsx # Uses components from source 101 | ``` 102 | 103 | ## Dependencies 104 | 105 | Make sure all dependencies used by the source code are also available in the example app. For instance, if components use `dayjs`, add it to the example app: 106 | 107 | ```bash 108 | cd example 109 | yarn add dayjs 110 | ``` 111 | 112 | ## Troubleshooting 113 | 114 | ### Metro Cache Issues 115 | 116 | If you encounter import issues, clear the Metro cache: 117 | 118 | ```bash 119 | cd example 120 | npx expo start --clear 121 | ``` 122 | 123 | ### TypeScript Errors 124 | 125 | If TypeScript can't find types, restart the TypeScript server in your IDE or run: 126 | 127 | ```bash 128 | yarn typecheck 129 | ``` 130 | 131 | ### Module Resolution Issues 132 | 133 | Ensure the alias in `metro.config.js` points to the correct path: 134 | 135 | ```javascript 136 | 'rn-base-component': path.resolve(workspaceRoot, 'src') 137 | ``` 138 | 139 | ## Production Build 140 | 141 | When building for production, the library still uses the built version in `lib/`. The development setup only affects the example app during development. 142 | 143 | To build the library: 144 | 145 | ```bash 146 | yarn prepack # Builds to lib/ 147 | ``` 148 | 149 | The published package will use the built version, not the source code. 150 | -------------------------------------------------------------------------------- /src/components/Accordion/AccordionItem.tsx: -------------------------------------------------------------------------------- 1 | import React, {PropsWithChildren, useCallback, useMemo} from 'react' 2 | import {LayoutAnimation, TouchableOpacity} from 'react-native' 3 | import styled from 'styled-components/native' 4 | import type {CommonAccordionProps, Section} from './Accordion' 5 | import {toggleAnimation} from './ToggleAnimation' 6 | import {useTheme} from '../../hooks' 7 | 8 | export interface AccordionItemProps extends PropsWithChildren { 9 | /** 10 | * onPress item event 11 | */ 12 | onPress: (keyExtractorItem: string) => void 13 | /** 14 | * keyExtractor for item 15 | */ 16 | keyExtractorItem: string 17 | expanded?: boolean 18 | index: number 19 | item: Section 20 | } 21 | 22 | export const AccordionItem: React.FC = ({ 23 | title = '', 24 | onPress, 25 | keyExtractorItem, 26 | expanded = false, 27 | index, 28 | itemContainerStyle, 29 | headerContainerStyle, 30 | contentContainerStyle, 31 | titleStyle, 32 | item, 33 | openAnimation, 34 | closeAnimation, 35 | openDuration, 36 | closeDuration, 37 | renderHeader = () => null, 38 | renderSectionTitle, 39 | renderContent, 40 | children, 41 | }) => { 42 | const theme = useTheme() 43 | const AccordionTheme = theme.components.Accordion 44 | 45 | const content = useMemo(() => { 46 | if (expanded) { 47 | return renderContent ? ( 48 | renderContent(item, index, expanded, keyExtractorItem) 49 | ) : ( 50 | {children} 51 | ) 52 | } 53 | return null 54 | }, [renderContent, expanded, keyExtractorItem, index, item, children, contentContainerStyle]) 55 | 56 | const header = useMemo( 57 | () => 58 | renderHeader(item, index, expanded, keyExtractorItem) || ( 59 | 60 | {renderSectionTitle ? ( 61 | renderSectionTitle(item, index, expanded) 62 | ) : ( 63 | {title} 64 | )} 65 | 66 | ), 67 | [ 68 | renderHeader, 69 | item, 70 | index, 71 | expanded, 72 | keyExtractorItem, 73 | renderSectionTitle, 74 | headerContainerStyle, 75 | title, 76 | titleStyle, 77 | ], 78 | ) 79 | 80 | const onPressItem = useCallback(() => { 81 | LayoutAnimation.configureNext( 82 | toggleAnimation( 83 | openAnimation, 84 | closeAnimation, 85 | openDuration ?? AccordionTheme.animation.openDuration, 86 | closeDuration ?? AccordionTheme.animation.closeDuration, 87 | ), 88 | ) 89 | onPress(keyExtractorItem) 90 | }, [ 91 | closeAnimation, 92 | closeDuration, 93 | keyExtractorItem, 94 | onPress, 95 | openAnimation, 96 | openDuration, 97 | AccordionTheme.animation.openDuration, 98 | AccordionTheme.animation.closeDuration, 99 | ]) 100 | 101 | return ( 102 | 103 | 107 | {header} 108 | 109 | {content} 110 | 111 | ) 112 | } 113 | 114 | const AccordionContainer = styled.View(({theme}) => ({ 115 | paddingBottom: theme?.components?.Accordion?.container?.paddingBottom ?? theme?.spacing?.petite, 116 | overflow: theme?.components?.Accordion?.container?.overflow ?? 'hidden', 117 | })) 118 | 119 | const AccordionHeader = styled.View(({theme}) => ({ 120 | padding: theme?.components?.Accordion?.header?.padding ?? theme?.spacing?.compact, 121 | })) 122 | 123 | const Title = styled.Text(({theme}) => ({ 124 | fontSize: theme?.components?.Accordion?.title?.fontSize ?? theme?.fontSizes?.xl, 125 | textAlign: theme?.components?.Accordion?.title?.textAlign ?? 'center', 126 | color: theme?.components?.Accordion?.title?.color ?? theme?.colors?.amber, 127 | fontWeight: 128 | theme?.fontWeights?.[theme?.components?.Accordion?.title?.fontWeight ?? 'bold'] ?? 129 | theme?.fontWeights?.bold, 130 | })) 131 | 132 | const AccordionBody = styled.View(({theme}) => ({ 133 | padding: theme?.components?.Accordion?.body?.padding ?? theme?.spacing?.compact, 134 | justifyContent: theme?.components?.Accordion?.body?.justifyContent ?? 'center', 135 | alignItems: theme?.components?.Accordion?.body?.alignItems ?? 'center', 136 | })) 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 10 | 11 | ```sh 12 | yarn 13 | ``` 14 | 15 | > While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. 16 | 17 | While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. 18 | 19 | To start the packager: 20 | 21 | ```sh 22 | yarn example start 23 | ``` 24 | 25 | To run the example app on Android: 26 | 27 | ```sh 28 | yarn example android 29 | ``` 30 | 31 | To run the example app on iOS: 32 | 33 | ```sh 34 | yarn example ios 35 | ``` 36 | 37 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 38 | 39 | ```sh 40 | yarn typecheck 41 | yarn lint 42 | ``` 43 | 44 | To fix formatting errors, run the following: 45 | 46 | ```sh 47 | yarn lint --fix 48 | ``` 49 | 50 | Remember to add tests for your change if possible. Run the unit tests by: 51 | 52 | ```sh 53 | yarn test 54 | ``` 55 | 56 | To edit the Objective-C or Swift files, open `example/ios/RnBaseComponentExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > rn-base-component`. 57 | 58 | To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `rn-base-component` under `Android`. 59 | 60 | ### Commit message convention 61 | 62 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 63 | 64 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 65 | - `feat`: new features, e.g. add new method to the module. 66 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 67 | - `docs`: changes into documentation, e.g. add usage example for the module.. 68 | - `test`: adding or updating tests, e.g. add integration tests using detox. 69 | - `chore`: tooling changes, e.g. change CI config. 70 | 71 | Our pre-commit hooks verify that your commit message matches this format when committing. 72 | 73 | ### Linting and tests 74 | 75 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 76 | 77 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 78 | 79 | Our pre-commit hooks verify that the linter and tests pass when committing. 80 | 81 | ### Publishing to npm 82 | 83 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 84 | 85 | To publish new versions, run the following: 86 | 87 | ```sh 88 | yarn release 89 | ``` 90 | 91 | ### Scripts 92 | 93 | The `package.json` file contains various scripts for common tasks: 94 | 95 | - `yarn bootstrap`: setup project by installing all dependencies and pods. 96 | - `yarn typecheck`: type-check files with TypeScript. 97 | - `yarn lint`: lint files with ESLint. 98 | - `yarn test`: run unit tests with Jest. 99 | - `yarn example start`: start the Metro server for the example app. 100 | - `yarn example android`: run the example app on Android. 101 | - `yarn example ios`: run the example app on iOS. 102 | 103 | ### Sending a pull request 104 | 105 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 106 | 107 | When you're sending a pull request: 108 | 109 | - Prefer small pull requests focused on one change. 110 | - Verify that linters and tests are passing. 111 | - Review the documentation to make sure it looks good. 112 | - Follow the pull request template when opening a pull request. 113 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 114 | -------------------------------------------------------------------------------- /src/components/Accordion/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useMemo, useState} from 'react' 2 | import {FlatList, StyleProp, Text, TextStyle, ViewStyle} from 'react-native' 3 | import {AccordionItem} from './AccordionItem' 4 | import type {AnimationType} from './ToggleAnimation' 5 | 6 | type ViewStyleProp = StyleProp | Array> 7 | type TextTitleStyleProp = StyleProp | Array> 8 | 9 | export type Section = { 10 | [key: string]: string | number 11 | title: string 12 | content: string 13 | } 14 | 15 | export interface CommonAccordionProps { 16 | /** 17 | * onPress accordion event 18 | */ 19 | onPress?: (keyExtractorItem: string) => void 20 | /** 21 | * render custom accordion header 22 | */ 23 | renderHeader?: ( 24 | item: Section, 25 | index: number, 26 | expanded: boolean, 27 | keyExtractorItem: string, 28 | ) => React.ReactNode 29 | /** 30 | * render custom title section 31 | */ 32 | renderSectionTitle?: (item: Section, index: number, expanded: boolean) => React.ReactNode 33 | /** 34 | * render custom content 35 | */ 36 | renderContent?: ( 37 | item: Section, 38 | index: number, 39 | expanded: boolean, 40 | keyExtractorItem: string, 41 | ) => React.ReactNode 42 | /** 43 | * accordion title 44 | */ 45 | title?: string 46 | /** 47 | * accordion container style 48 | */ 49 | itemContainerStyle?: ViewStyleProp 50 | /** 51 | * accordion title style 52 | */ 53 | titleStyle?: TextTitleStyleProp 54 | /** 55 | * accordion header style 56 | */ 57 | headerContainerStyle?: ViewStyleProp 58 | /** 59 | * accordion body style 60 | */ 61 | contentContainerStyle?: ViewStyleProp 62 | /** 63 | * animation type when open 64 | * default: easeInEaseOut 65 | */ 66 | openAnimation?: AnimationType 67 | /** 68 | * animation type when close 69 | * default: easeInEaseOut 70 | */ 71 | closeAnimation?: AnimationType 72 | /** 73 | * duration when open 74 | * default: 300 75 | */ 76 | openDuration?: number 77 | /** 78 | * duration when close 79 | * default: 300 80 | */ 81 | closeDuration?: number 82 | } 83 | 84 | export interface AccordionProps extends CommonAccordionProps { 85 | /** 86 | * section data sets 87 | */ 88 | sections: Section[] 89 | /** 90 | * keyExtractor for Flatlist 91 | */ 92 | keyExtractor?: (item: Section, index: number) => string 93 | /** 94 | * enable expand multiple item 95 | * default: false 96 | */ 97 | expandMultiple?: boolean 98 | /** 99 | * accordion wrapper style 100 | */ 101 | wrapperStyle?: ViewStyleProp 102 | } 103 | 104 | const AccordionComponent = React.forwardRef( 105 | ({sections, expandMultiple = false, keyExtractor, wrapperStyle, ...rest}, ref) => { 106 | const [array, setArray] = useState([]) 107 | 108 | const _keyExtractor = useMemo( 109 | () => keyExtractor || ((_: Section, index: number) => index.toString()), 110 | [keyExtractor], 111 | ) 112 | 113 | const onPress = useCallback( 114 | (key: string) => { 115 | setArray(previousArray => { 116 | const index = previousArray.indexOf(key) 117 | let newArray = [...previousArray] 118 | if (expandMultiple) { 119 | if (index >= 0) { 120 | newArray.splice(index, 1) 121 | } else { 122 | newArray.push(key) 123 | } 124 | } else { 125 | newArray = index >= 0 ? [] : [key] 126 | } 127 | return newArray 128 | }) 129 | }, 130 | [expandMultiple], 131 | ) 132 | 133 | const renderItem = useCallback( 134 | ({item, index}: {item: Section; index: number}) => ( 135 | 144 | {item?.content ?? ''} 145 | 146 | ), 147 | [_keyExtractor, onPress, array, rest], 148 | ) 149 | 150 | return ( 151 | 160 | ) 161 | }, 162 | ) 163 | 164 | export const Accordion = React.memo(AccordionComponent) 165 | -------------------------------------------------------------------------------- /example/theme/demoMetrics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Demo App Metrics System 3 | * All spacing, sizing, and dimension values used in the component demo application 4 | */ 5 | 6 | export const demoMetrics = { 7 | // Spacing Scale 8 | spacing: { 9 | tiny: 4, 10 | small: 8, 11 | medium: 12, 12 | normal: 16, 13 | large: 20, 14 | xlarge: 24, 15 | xxlarge: 30, 16 | xxxlarge: 40, 17 | }, 18 | 19 | // Padding Scale 20 | padding: { 21 | tiny: 4, 22 | small: 8, 23 | medium: 12, 24 | normal: 16, 25 | large: 20, 26 | xlarge: 24, 27 | }, 28 | 29 | // Margin Scale 30 | margin: { 31 | tiny: 4, 32 | small: 8, 33 | medium: 10, 34 | normal: 15, 35 | large: 20, 36 | xlarge: 30, 37 | }, 38 | 39 | // Border Radius 40 | borderRadius: { 41 | tiny: 4, 42 | small: 6, 43 | medium: 8, 44 | normal: 10, 45 | large: 12, 46 | xlarge: 14, 47 | xxlarge: 16, 48 | round: 25, 49 | }, 50 | 51 | // Border Width 52 | borderWidth: { 53 | thin: 0.5, 54 | normal: 1, 55 | medium: 1.5, 56 | thick: 2, 57 | xthick: 3, 58 | xxthick: 4, 59 | xxxthick: 5, 60 | }, 61 | 62 | // Font Sizes 63 | fontSize: { 64 | tiny: 11, 65 | small: 12, 66 | caption: 13, 67 | body: 14, 68 | subheading: 15, 69 | normal: 16, 70 | medium: 18, 71 | large: 20, 72 | xlarge: 22, 73 | xxlarge: 24, 74 | title: 26, 75 | heading: 28, 76 | hero: 32, 77 | }, 78 | 79 | // Icon Sizes 80 | iconSize: { 81 | tiny: 16, 82 | small: 20, 83 | normal: 24, 84 | medium: 28, 85 | large: 32, 86 | xlarge: 40, 87 | xxlarge: 48, 88 | huge: 56, 89 | }, 90 | 91 | // Cell/Input Sizes (for CodeInput) 92 | cellSize: { 93 | tiny: 36, 94 | small: 38, 95 | compact: 40, 96 | normal: 45, 97 | medium: 48, 98 | large: 50, 99 | xlarge: 52, 100 | xxlarge: 55, 101 | huge: 60, 102 | xhuge: 65, 103 | }, 104 | 105 | // Button/Control Heights 106 | controlHeight: { 107 | small: 32, 108 | normal: 40, 109 | medium: 44, 110 | large: 48, 111 | xlarge: 52, 112 | }, 113 | 114 | // Shadow 115 | shadow: { 116 | small: { 117 | shadowOffset: { width: 0, height: 2 }, 118 | shadowOpacity: 0.08, 119 | shadowRadius: 4, 120 | elevation: 2, 121 | }, 122 | normal: { 123 | shadowOffset: { width: 0, height: 2 }, 124 | shadowOpacity: 0.08, 125 | shadowRadius: 8, 126 | elevation: 3, 127 | }, 128 | medium: { 129 | shadowOffset: { width: 0, height: 4 }, 130 | shadowOpacity: 0.2, 131 | shadowRadius: 6, 132 | elevation: 5, 133 | }, 134 | large: { 135 | shadowOffset: { width: 0, height: 6 }, 136 | shadowOpacity: 0.35, 137 | shadowRadius: 8, 138 | elevation: 8, 139 | }, 140 | glass: { 141 | shadowOffset: { width: 0, height: 6 }, 142 | shadowOpacity: 0.1, 143 | shadowRadius: 32, 144 | elevation: 8, 145 | }, 146 | }, 147 | 148 | // Opacity 149 | opacity: { 150 | disabled: 0.5, 151 | dimmed: 0.6, 152 | faded: 0.7, 153 | translucent: 0.8, 154 | }, 155 | 156 | // Line Height 157 | lineHeight: { 158 | tight: 20, 159 | normal: 24, 160 | relaxed: 28, 161 | }, 162 | 163 | // Gap (for flex/grid) 164 | gap: { 165 | tiny: 4, 166 | small: 8, 167 | medium: 12, 168 | normal: 16, 169 | }, 170 | 171 | // Scale transforms 172 | scale: { 173 | small: 1.03, 174 | normal: 1.05, 175 | medium: 1.08, 176 | }, 177 | } 178 | 179 | // Metric aliases for specific use cases 180 | export const componentMetrics = { 181 | // Card 182 | cardPadding: demoMetrics.padding.large, 183 | cardBorderRadius: demoMetrics.borderRadius.xxlarge, 184 | cardGap: demoMetrics.gap.medium, 185 | 186 | // Button 187 | buttonMinWidth: 100, 188 | buttonPaddingHorizontal: demoMetrics.padding.medium, 189 | buttonPaddingVertical: demoMetrics.padding.small, 190 | 191 | // Icon Container 192 | iconContainerSize: demoMetrics.iconSize.xlarge, 193 | iconContainerPadding: demoMetrics.padding.tiny, 194 | 195 | // Section 196 | sectionPadding: demoMetrics.padding.large, 197 | sectionMargin: demoMetrics.margin.medium, 198 | sectionBorderRadius: demoMetrics.borderRadius.large, 199 | 200 | // Container 201 | containerPadding: demoMetrics.padding.large, 202 | containerPaddingBottom: demoMetrics.padding.xlarge, 203 | 204 | // Stats 205 | statDividerWidth: 1, 206 | statDividerMargin: demoMetrics.spacing.normal, 207 | } 208 | 209 | export type DemoMetrics = typeof demoMetrics 210 | export type ComponentMetrics = typeof componentMetrics 211 | -------------------------------------------------------------------------------- /src/components/Progress/Progress.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, forwardRef, useCallback, useState} from 'react' 2 | import type {LayoutChangeEvent, View} from 'react-native' 3 | import {View as RNView, ViewProps as RNViewProperties} from 'react-native' 4 | import styled from 'styled-components/native' 5 | import Animated, { 6 | useSharedValue, 7 | useAnimatedStyle, 8 | withTiming, 9 | withRepeat, 10 | Easing, 11 | interpolate, 12 | } from 'react-native-reanimated' 13 | import {deviceWidth} from '../../helpers/metrics' 14 | import {useTheme} from '../../hooks' 15 | 16 | interface IProgressProps { 17 | /** 18 | * Current percent of the Progress bar 19 | * default 0, max 100 20 | */ 21 | value?: number 22 | /** 23 | * Defines height of Progress bar 24 | * default 16 25 | */ 26 | size?: number 27 | /** 28 | * Defines borderRadius of Progress bar 29 | * default 0 30 | */ 31 | borderRadius?: number 32 | /** 33 | * Defines color of Track Bar 34 | */ 35 | filledTrackColor?: string 36 | /** 37 | * Defines background color of Progress bar 38 | */ 39 | backgroundColor?: string 40 | /** 41 | * Defines full width of the progress bar 42 | */ 43 | width?: number 44 | /** 45 | * Defines progress mode 46 | */ 47 | isIndeterminateProgress?: boolean 48 | } 49 | 50 | type ProgressStyle = { 51 | width: number | undefined 52 | size: number | undefined 53 | backgroundColor: string 54 | borderRadius: number 55 | } 56 | 57 | const screenWidth = deviceWidth() 58 | const MAX_VALUE = 100 59 | 60 | const ProgressComponent = forwardRef( 61 | ( 62 | { 63 | width, 64 | value = 0, 65 | size, 66 | borderRadius, 67 | filledTrackColor, 68 | backgroundColor, 69 | isIndeterminateProgress = false, 70 | }, 71 | ref, 72 | ) => { 73 | const ProgressTheme = useTheme().components.Progress 74 | const [progressWidth, setProgressWidth] = useState(0) 75 | const translateX = useSharedValue(-screenWidth) 76 | const animation = useSharedValue(0) 77 | 78 | useEffect(() => { 79 | const progressValue = value >= MAX_VALUE ? MAX_VALUE : value 80 | const newToTranslateX = -progressWidth + (progressWidth * progressValue) / MAX_VALUE 81 | if (isIndeterminateProgress) { 82 | animation.value = withRepeat( 83 | withTiming(1, { 84 | duration: 2000, 85 | easing: Easing.linear, 86 | }), 87 | -1, 88 | ) 89 | } else { 90 | translateX.value = withTiming(newToTranslateX, { 91 | duration: 500, 92 | easing: Easing.linear, 93 | }) 94 | } 95 | }, [animation, isIndeterminateProgress, progressWidth, translateX, value]) 96 | 97 | const onLayout = useCallback((event: LayoutChangeEvent) => { 98 | const {layout} = event.nativeEvent 99 | setProgressWidth(layout.width) 100 | }, []) 101 | 102 | const progressStyle = useAnimatedStyle(() => { 103 | const translateXValue = isIndeterminateProgress 104 | ? interpolate(animation.value, [0, 1], [-progressWidth, 0.5 * progressWidth]) 105 | : translateX.value 106 | 107 | const scaleXValue = isIndeterminateProgress 108 | ? interpolate(animation.value, [0, 0.5, 1], [0.0001, 1, 0.001]) 109 | : 1 110 | 111 | return { 112 | transform: [{translateX: translateXValue}, {scaleX: scaleXValue}], 113 | } 114 | }, [progressWidth]) 115 | 116 | return ( 117 | 125 | 137 | 138 | ) 139 | }, 140 | ) 141 | 142 | const ForwardRefProgressWrapperComponent = forwardRef((props, ref) => ( 143 | 144 | )) 145 | 146 | const ProgressWrapper = styled(ForwardRefProgressWrapperComponent)(props => ({ 147 | overflow: 'hidden', 148 | width: props.width, 149 | height: props.size, 150 | backgroundColor: props.backgroundColor, 151 | borderRadius: props.borderRadius, 152 | })) 153 | 154 | export const Progress = React.memo(ProgressComponent) 155 | -------------------------------------------------------------------------------- /.cursor/rules/performance.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | alwaysApply: true 3 | --- 4 | # Performance Optimization Guidelines 5 | 6 | ## React Performance Best Practices 7 | 8 | ### Memoization 9 | - Use `React.memo()` for components that receive complex props 10 | - Use `useMemo()` for expensive calculations 11 | - Use `useCallback()` for event handlers passed to child components 12 | - Memoize theme-dependent style calculations 13 | 14 | ```typescript 15 | // ✅ Good - Memoized component 16 | const ExpensiveComponent = React.memo(({data, onPress}) => { 17 | const processedData = useMemo(() => 18 | data.map(item => complexTransformation(item)), [data] 19 | ) 20 | 21 | const handlePress = useCallback(() => { 22 | onPress(processedData) 23 | }, [onPress, processedData]) 24 | 25 | return {/* render */} 26 | }) 27 | 28 | // ✅ Good - Memoized theme styles 29 | const useButtonStyles = (variant: string) => { 30 | const {theme} = useTheme() 31 | 32 | return useMemo(() => ({ 33 | backgroundColor: theme.colors[variant], 34 | padding: theme.space.md, 35 | borderRadius: theme.radii.md, 36 | }), [theme, variant]) 37 | } 38 | ``` 39 | 40 | ### Styled Components Performance 41 | - Minimize styled component re-renders 42 | - Use stable references for theme props 43 | - Avoid creating styled components inside render functions 44 | - Use CSS helper for complex conditional styling 45 | 46 | ```typescript 47 | // ✅ Good - Stable styled component 48 | const StyledButton = styled.TouchableOpacity<{variant: string}>` 49 | ${({theme, variant}) => css` 50 | background-color: ${theme.colors[variant]}; 51 | /* other styles */ 52 | `} 53 | ` 54 | 55 | // ❌ Bad - Creating styled component in render 56 | const MyComponent = () => { 57 | const StyledView = styled.View`/* styles */` // Don't do this 58 | return 59 | } 60 | ``` 61 | 62 | ## Animation Performance 63 | - Use react-native-reanimated for smooth animations 64 | - Prefer transform animations over layout changes 65 | - Use native driver when possible 66 | - Avoid animating expensive properties 67 | 68 | ```typescript 69 | // ✅ Good - Performant animation 70 | const animatedStyle = useAnimatedStyle(() => { 71 | return { 72 | transform: [ 73 | {translateX: withSpring(translateX.value)}, 74 | {scale: withTiming(scale.value)}, 75 | ], 76 | } 77 | }) 78 | 79 | // ❌ Bad - Layout-thrashing animation 80 | const badStyle = { 81 | left: animatedValue, // Causes layout recalculation 82 | width: animatedWidth, // Expensive property to animate 83 | } 84 | ``` 85 | 86 | ## Bundle Size Optimization 87 | - Use tree-shaking friendly exports 88 | - Avoid importing entire libraries 89 | - Lazy load heavy components when appropriate 90 | - Keep component dependencies minimal 91 | 92 | ```typescript 93 | // ✅ Good - Specific imports 94 | import {debounce} from 'lodash/debounce' 95 | import {Platform} from 'react-native' 96 | 97 | // ❌ Bad - Full library import 98 | import _ from 'lodash' 99 | import * as RN from 'react-native' 100 | ``` 101 | 102 | ## Memory Management 103 | - Clean up subscriptions and listeners in useEffect cleanup 104 | - Avoid memory leaks in async operations 105 | - Use weak references where appropriate 106 | - Clean up timers and intervals 107 | 108 | ```typescript 109 | // ✅ Good - Proper cleanup 110 | useEffect(() => { 111 | const subscription = someListener.subscribe(callback) 112 | const timer = setTimeout(delayedAction, 1000) 113 | 114 | return () => { 115 | subscription.unsubscribe() 116 | clearTimeout(timer) 117 | } 118 | }, []) 119 | ``` 120 | 121 | ## Rendering Optimization 122 | - Use FlatList for large lists with proper keyExtractor 123 | - Implement getItemLayout when possible for FlatList 124 | - Use windowSize and initialNumToRender appropriately 125 | - Avoid nested ScrollViews 126 | 127 | ## Theme and Context Performance 128 | - Minimize theme context re-renders 129 | - Use selector patterns for complex theme objects 130 | - Memoize theme-dependent calculations 131 | - Avoid creating new objects in theme provider 132 | 133 | ```typescript 134 | // ✅ Good - Memoized theme provider 135 | const BaseProvider = ({children, theme}) => { 136 | const contextValue = useMemo(() => ({ 137 | theme, 138 | colorMode: colorModeValue, 139 | toggleColorMode, 140 | }), [theme, colorModeValue, toggleColorMode]) 141 | 142 | return ( 143 | 144 | {children} 145 | 146 | ) 147 | } 148 | ``` 149 | 150 | ## Image and Asset Optimization 151 | - Use appropriate image formats and sizes 152 | - Implement lazy loading for images 153 | - Use cached images for network resources 154 | - Optimize SVG assets and icons 155 | 156 | ## Development vs Production 157 | - Use React DevTools Profiler during development 158 | - Measure performance with appropriate tools 159 | - Test on lower-end devices 160 | - Use production builds for performance testing -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rn-base-component", 3 | "version": "0.6.0", 4 | "description": "Base component for React Native", 5 | "main": "lib/commonjs/index.js", 6 | "module": "lib/module/index.js", 7 | "types": "lib/typescript/src/index.d.ts", 8 | "react-native": "src/index.tsx", 9 | "source": "src/index.tsx", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "*.podspec" 14 | ], 15 | "scripts": { 16 | "test": "jest", 17 | "update-test": "jest -u", 18 | "typecheck": "tsc --noEmit", 19 | "lint": "npm-run-all --parallel lint:check:*", 20 | "lint:check:eslint": "eslint . --ext .js,.jsx,.ts,.tsx --cache --cache-strategy content --max-warnings=0", 21 | "lint:check:prettier": "prettier --check ./**/*.{ts,js,tsx,jsx,yaml,yml} --no-error-on-unmatched-pattern", 22 | "lint:check:tsc": "tsc --noEmit", 23 | "lint:fix": "npm-run-all --parallel lint:fix:*", 24 | "lint:fix:eslint": "eslint . --ext .js,.jsx,.ts,.tsx --fix --max-warnings=0", 25 | "lint:fix:prettier": "prettier --write ./**/*.{ts,js,tsx,jsx,yaml,yml} --no-error-on-unmatched-pattern", 26 | "prepack": "bob build", 27 | "example": "yarn --cwd example", 28 | "release": "release-it", 29 | "pretty": "prettier --write \"./**/*.{js,jsx,json}\"", 30 | "bootstrap": "yarn example && yarn install && yarn example pods", 31 | "dev": "cd example && npx expo start --clear", 32 | "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build", 33 | "prepare": "husky install", 34 | "format": "prettier --write .", 35 | "lint:staged": "lint-staged" 36 | }, 37 | "keywords": [ 38 | "react-native", 39 | "ios", 40 | "android" 41 | ], 42 | "repository": "https://github.com/saigon-technology/rn-base-component", 43 | "author": "React Native Team ", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/saigon-technology/rn-base-component/issues" 47 | }, 48 | "homepage": "https://saigontechnology.com", 49 | "devDependencies": { 50 | "@commitlint/config-conventional": "^17.0.2", 51 | "@react-native-community/eslint-config": "^3.0.2", 52 | "@release-it/conventional-changelog": "^5.0.0", 53 | "@testing-library/jest-native": "^5.4.3", 54 | "@testing-library/react-native": "^11.5.2", 55 | "@types/jest": "^29.5.0", 56 | "@types/lodash": "^4.14.192", 57 | "@types/react": "^18.0.26", 58 | "@types/react-native": "^0.70.0", 59 | "@types/react-test-renderer": "^18.0.0", 60 | "babel-jest": "^29.5.0", 61 | "commitlint": "^17.0.2", 62 | "del-cli": "^5.0.0", 63 | "eslint": "^8.4.1", 64 | "eslint-config-prettier": "^8.5.0", 65 | "eslint-plugin-import": "^2.27.5", 66 | "eslint-plugin-prettier": "^4.0.0", 67 | "eslint-plugin-unused-imports": "^2.0.0", 68 | "husky": "8", 69 | "jest": "^29.5.0", 70 | "lint-staged": "13", 71 | "lodash": "^4.17.21", 72 | "npm-run-all": "^4.1.5", 73 | "pod-install": "^0.1.0", 74 | "prettier": "^2.0.5", 75 | "react": "18.2.0", 76 | "react-addons-test-utils": "^15.6.2", 77 | "react-native": "0.71.4", 78 | "react-native-builder-bob": "^0.20.4", 79 | "react-native-gesture-handler": "^2.14.0", 80 | "react-native-reanimated": "^3.6.0", 81 | "react-test-renderer": "^18.2.0", 82 | "release-it": "^15.0.0", 83 | "styled-components": "^6.1.0", 84 | "ts-jest": "^29.0.5", 85 | "ts-node": "^10.9.1", 86 | "typescript": "^4.5.2" 87 | }, 88 | "resolutions": { 89 | "@types/react": "18.0.26" 90 | }, 91 | "peerDependencies": { 92 | "react": "*", 93 | "react-native": "*", 94 | "react-native-gesture-handler": "*", 95 | "react-native-reanimated": "*", 96 | "react-native-svg": "*", 97 | "styled-components": "*" 98 | }, 99 | "engines": { 100 | "node": ">= 16.0.0" 101 | }, 102 | "packageManager": "yarn@1.22.22", 103 | "commitlint": { 104 | "extends": [ 105 | "@commitlint/config-conventional" 106 | ] 107 | }, 108 | "release-it": { 109 | "git": { 110 | "commitMessage": "chore: release ${version}", 111 | "tagName": "v${version}" 112 | }, 113 | "npm": { 114 | "publish": true 115 | }, 116 | "github": { 117 | "release": true 118 | }, 119 | "plugins": { 120 | "@release-it/conventional-changelog": { 121 | "preset": "angular" 122 | } 123 | } 124 | }, 125 | "react-native-builder-bob": { 126 | "source": "src", 127 | "output": "lib", 128 | "targets": [ 129 | "commonjs", 130 | "module", 131 | [ 132 | "typescript", 133 | { 134 | "project": "tsconfig.build.json" 135 | } 136 | ] 137 | ] 138 | }, 139 | "dependencies": { 140 | "eslint-plugin-ft-flow": "^2.0.3" 141 | }, 142 | "publishConfig": { 143 | "registry": "https://registry.npmjs.org" 144 | }, 145 | "lint-staged": { 146 | "*.{js,jsx,ts,tsx}": [ 147 | "eslint --fix", 148 | "prettier --write" 149 | ], 150 | "*.{json,yaml,yml,md}": [ 151 | "prettier --write" 152 | ] 153 | } 154 | } 155 | --------------------------------------------------------------------------------