├── .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
9 | }
10 |
--------------------------------------------------------------------------------
/src/theme/components/Button/ButtonOutline.ts:
--------------------------------------------------------------------------------
1 | import base from '../../base'
2 | import {type ButtonThemeProps, ButtonTheme} from './Button'
3 |
4 | export const ButtonOutlineTheme: ButtonThemeProps = {
5 | ...ButtonTheme,
6 | backgroundColor: 'transparent',
7 | textColor: base.colors.primary,
8 | outlineWidth: base.borderWidths.tiny,
9 | outlineColor: base.colors.primary,
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/Button/ButtonOutline.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 ButtonOutline: React.FC = props => {
7 | const ButtonOutlineTheme = useTheme().components.ButtonOutline
8 | return
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Button/ButtonSecondary.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 ButtonSecondary: React.FC = props => {
7 | const ButtonSecondaryTheme = useTheme().components.ButtonSecondary
8 | return
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Button/ButtonTransparent.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 ButtonTransparent: React.FC = props => {
7 | const ButtonTransparentTheme = useTheme().components.ButtonTransparent
8 | return
9 | }
10 |
--------------------------------------------------------------------------------
/src/theme/base/opacity.ts:
--------------------------------------------------------------------------------
1 | const opacity = {
2 | transparent: 0,
3 | translucent: 0.05,
4 | hazy: 0.1,
5 | misty: 0.2,
6 | faint: 0.25,
7 | lightyOpaque: 0.3,
8 | semiOpaque: 0.4,
9 | partiallyOpaque: 0.5,
10 | clouded: 0.6,
11 | murky: 0.7,
12 | opaque: 0.75,
13 | solid: 0.8,
14 | dense: 0.9,
15 | darkened: 0.95,
16 | blackened: 1,
17 | } as const
18 |
19 | export default opacity
20 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Button'
2 | export * from './Progress'
3 | export * from './CodeInput'
4 | export * from './CountDown'
5 | export * from './Slider'
6 | export * from './Card'
7 | export * from './RadioButton'
8 | export * from './Accordion'
9 | export * from './TextInput'
10 | export * from './Checkbox'
11 | export * from './Text'
12 | export * from './Typography'
13 | export * from './Icon'
14 |
--------------------------------------------------------------------------------
/src/theme/base/index.ts:
--------------------------------------------------------------------------------
1 | import typography from './typography'
2 | import colors from './colors'
3 | import borderWidths from './borderWidths'
4 | import sizes from './sizes'
5 | import spacing from './spacing'
6 | import opacity from './opacity'
7 | import shadows from './shadows'
8 |
9 | const base = {
10 | colors,
11 | borderWidths,
12 | sizes,
13 | spacing,
14 | opacity,
15 | shadows,
16 | ...typography,
17 | }
18 |
19 | export default base
20 |
--------------------------------------------------------------------------------
/src/theme/base/sizes.ts:
--------------------------------------------------------------------------------
1 | const sizes = {
2 | tiny: 4,
3 | miniature: 8,
4 | petite: 12,
5 | small: 16,
6 | little: 20,
7 | compact: 24,
8 | narrow: 28,
9 | slim: 32,
10 | moderate: 36,
11 | average: 40,
12 | substantial: 48,
13 | large: 64,
14 | big: 80,
15 | grand: 96,
16 | huge: 128,
17 | giant: 160,
18 | colossal: 192,
19 | enormous: 224,
20 | mammoth: 256,
21 | titanic: 288,
22 | } as const
23 |
24 | export default sizes
25 |
--------------------------------------------------------------------------------
/src/theme/base/spacing.ts:
--------------------------------------------------------------------------------
1 | const spacing = {
2 | tiny: 2,
3 | miniature: 4,
4 | petite: 6,
5 | small: 8,
6 | little: 10,
7 | compact: 12,
8 | narrow: 14,
9 | slim: 16,
10 | moderate: 18,
11 | average: 20,
12 | substantial: 22,
13 | large: 24,
14 | big: 26,
15 | grand: 28,
16 | huge: 30,
17 | giant: 32,
18 | colossal: 34,
19 | enormous: 36,
20 | mammoth: 38,
21 | titanic: 40,
22 | gigantic: 48,
23 | } as const
24 |
25 | export default spacing
26 |
--------------------------------------------------------------------------------
/src/components/TextInput/constants.ts:
--------------------------------------------------------------------------------
1 | const DURATION = 200
2 | const FOCUSED = 1
3 | const OUT_OF_FOCUS = 0
4 | const BLURRED = 1
5 | const OUT_OF_BLUR = 0
6 | const DEFAULT_WIDTH = 0
7 | const DEFAULT_HEIGHT = 0
8 | const FOCUSED_FONTSIZE = 11
9 | const UNFOCUSED_FONTSIZE = 14
10 |
11 | export {
12 | DURATION,
13 | FOCUSED,
14 | OUT_OF_BLUR,
15 | OUT_OF_FOCUS,
16 | BLURRED,
17 | DEFAULT_WIDTH,
18 | DEFAULT_HEIGHT,
19 | FOCUSED_FONTSIZE,
20 | UNFOCUSED_FONTSIZE,
21 | }
22 |
--------------------------------------------------------------------------------
/src/core/color-mode/type.ts:
--------------------------------------------------------------------------------
1 | export type ColorMode = 'light' | 'dark' | null | undefined
2 |
3 | export interface StorageManager {
4 | get(init?: ColorMode): Promise
5 | set(value: ColorMode): void
6 | }
7 |
8 | export interface ColorModeOptions {
9 | initialColorMode?: ColorMode
10 | useSystemColorMode?: boolean
11 | }
12 |
13 | export interface IColorModeContextProps {
14 | colorMode: ColorMode
15 | toggleColorMode: () => void
16 | setColorMode: (value: ColorMode) => void
17 | }
18 |
--------------------------------------------------------------------------------
/src/theme/base/borderWidths.ts:
--------------------------------------------------------------------------------
1 | const borderWidths = {
2 | tiny: 1,
3 | miniature: 2,
4 | petite: 3,
5 | small: 4,
6 | little: 5,
7 | compact: 6,
8 | narrow: 7,
9 | slim: 8,
10 | moderate: 9,
11 | average: 10,
12 | substantial: 11,
13 | large: 12,
14 | big: 13,
15 | grand: 14,
16 | huge: 15,
17 | giant: 16,
18 | colossal: 17,
19 | enormous: 18,
20 | mammoth: 19,
21 | titanic: 20,
22 | } as const
23 |
24 | export type IBorderWidth = keyof typeof borderWidths
25 | export default borderWidths
26 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Summary
2 |
3 |
4 | ## Features
5 |
6 |
7 | ## Bugs
8 |
9 |
10 | ## Check List
11 | - [ ] Your code builds clean without any errors or warnings.
12 | - [ ] Check coding convention.
13 | - [ ] Updated Storybook components and stories to reflect any changes made to the UI.
14 | - [ ] Test covers all edge cases and coverage is greater than or equal to 80%.
15 |
16 | ## Proof of Completeness
17 |
18 | OR
19 | Video link
20 |
--------------------------------------------------------------------------------
/example/components/ThemedView.tsx:
--------------------------------------------------------------------------------
1 | import { View, type ViewProps } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedViewProps = ViewProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | };
9 |
10 | export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
11 | const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
12 |
13 | return ;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/TextInput/components/ErrorText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components/native'
3 | import type {TextProps} from 'react-native'
4 |
5 | interface ErrorTextProps {
6 | errorText?: string
7 | errorProps?: TextProps
8 | }
9 |
10 | export const Error: React.FC = ({errorText, errorProps}) => (
11 | {errorText}
12 | )
13 |
14 | const ErrorText = styled.Text(({theme}) => ({
15 | fontSize: theme?.fontSizes?.['2xs'],
16 | color: theme?.colors?.errorText,
17 | }))
18 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "jsx": "react",
5 | "strict": true,
6 | "baseUrl": ".",
7 | "paths": {
8 | "@/*": [
9 | "./*"
10 | ],
11 | "rn-base-component": [
12 | "../src"
13 | ],
14 | "rn-base-component/*": [
15 | "../src/*"
16 | ]
17 | }
18 | },
19 | "include": [
20 | "**/*.ts",
21 | "**/*.tsx",
22 | ".expo/types/**/*.ts",
23 | "expo-env.d.ts",
24 | "../src/**/*.ts",
25 | "../src/**/*.tsx"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/example/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useColorScheme as useRNColorScheme } from 'react-native';
3 |
4 | /**
5 | * To support static rendering, this value needs to be re-calculated on the client side for web
6 | */
7 | export function useColorScheme() {
8 | const [hasHydrated, setHasHydrated] = useState(false);
9 |
10 | useEffect(() => {
11 | setHasHydrated(true);
12 | }, []);
13 |
14 | const colorScheme = useRNColorScheme();
15 |
16 | if (hasHydrated) {
17 | return colorScheme;
18 | }
19 |
20 | return 'light';
21 | }
22 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 | expo-env.d.ts
11 |
12 | # Native
13 | .kotlin/
14 | *.orig.*
15 | *.jks
16 | *.p8
17 | *.p12
18 | *.key
19 | *.mobileprovision
20 |
21 | # Metro
22 | .metro-health-check*
23 |
24 | # debug
25 | npm-debug.*
26 | yarn-debug.*
27 | yarn-error.*
28 |
29 | # macOS
30 | .DS_Store
31 | *.pem
32 |
33 | # local env files
34 | .env*.local
35 |
36 | # typescript
37 | *.tsbuildinfo
38 |
39 | app-example
40 |
--------------------------------------------------------------------------------
/src/components/CodeInput/components/ErrorText.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type {TextProps} from 'react-native'
3 | import styled from 'styled-components/native'
4 |
5 | interface ErrorTextProps {
6 | errorText?: string
7 | errorProps?: TextProps
8 | }
9 |
10 | export const ErrorText: React.FC = ({errorText, errorProps}) => (
11 | {errorText}
12 | )
13 |
14 | const ErrorTextStyled = styled.Text(({theme}) => ({
15 | fontSize: theme?.fontSizes?.['2xs'],
16 | color: theme?.colors?.errorText,
17 | marginTop: theme?.spacing?.tiny,
18 | }))
19 |
--------------------------------------------------------------------------------
/src/theme/components/Button/Button.ts:
--------------------------------------------------------------------------------
1 | import base from '../../base'
2 | import {metrics} from '../../../helpers'
3 | import type {ButtonProps} from '../../../components/Button/Button'
4 |
5 | export type ButtonThemeProps = {
6 | height?: number
7 | } & Pick<
8 | ButtonProps,
9 | 'backgroundColor' | 'disabledColor' | 'borderRadius' | 'textColor' | 'outlineWidth' | 'outlineColor'
10 | >
11 |
12 | export const ButtonTheme: ButtonThemeProps = {
13 | height: metrics.xxl,
14 | backgroundColor: base.colors.primary,
15 | disabledColor: base.colors.muted,
16 | borderRadius: metrics.borderRadius,
17 | textColor: base.colors.white,
18 | }
19 |
--------------------------------------------------------------------------------
/example/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about light and dark modes:
3 | * https://docs.expo.dev/guides/color-schemes/
4 | */
5 |
6 | import { Colors } from '@/constants/Colors';
7 | import { useColorScheme } from '@/hooks/useColorScheme';
8 |
9 | export function useThemeColor(
10 | props: { light?: string; dark?: string },
11 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
12 | ) {
13 | const theme = useColorScheme() ?? 'light';
14 | const colorFromProps = props[theme];
15 |
16 | if (colorFromProps) {
17 | return colorFromProps;
18 | } else {
19 | return Colors[theme][colorName];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/theme/components/Checkbox.ts:
--------------------------------------------------------------------------------
1 | import base from '../base'
2 | import {metrics} from '../../helpers'
3 | import type {ICheckboxProps} from '../../components/Checkbox/Checkbox'
4 |
5 | export type CheckboxThemeProps = Pick<
6 | ICheckboxProps,
7 | 'size' | 'borderRadius' | 'fillColor' | 'unfillColor' | 'checkMarkColor' | 'borderWidth'
8 | >
9 |
10 | export const CheckboxTheme: CheckboxThemeProps = {
11 | size: base.sizes.narrow,
12 | borderRadius: metrics.borderRadius,
13 | fillColor: base.colors.primary,
14 | unfillColor: base.colors.transparent,
15 | checkMarkColor: base.colors.white,
16 | borderWidth: base.borderWidths.tiny,
17 | }
18 |
--------------------------------------------------------------------------------
/example/components/HapticTab.tsx:
--------------------------------------------------------------------------------
1 | import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
2 | import { PlatformPressable } from '@react-navigation/elements';
3 | import * as Haptics from 'expo-haptics';
4 |
5 | export function HapticTab(props: BottomTabBarButtonProps) {
6 | return (
7 | {
10 | if (process.env.EXPO_OS === 'ios') {
11 | // Add a soft haptic feedback when pressing down on the tabs.
12 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
13 | }
14 | props.onPressIn?.(ev);
15 | }}
16 | />
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/example/components/ui/TabBarBackground.ios.tsx:
--------------------------------------------------------------------------------
1 | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
2 | import { BlurView } from 'expo-blur';
3 | import { StyleSheet } from 'react-native';
4 |
5 | export default function BlurTabBarBackground() {
6 | return (
7 |
14 | );
15 | }
16 |
17 | export function useBottomTabOverflow() {
18 | return useBottomTabBarHeight();
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Slider/constants.ts:
--------------------------------------------------------------------------------
1 | const DEFAULT_STEP = 1
2 | const INIT_POINT = 0
3 | const MINIMUM_TRACK_WIDTH = 0
4 | const DEFAULT_MINIMUM_VALUE = 0
5 | const DEFAULT_MAXIMUM_VALUE = 1
6 | const INIT_VALUE = 0
7 | const VISIBLE = 1
8 | const INVISIBLE = 0
9 | const NEXT_STEP = 1
10 | const PREVIOUS_STEP = 1
11 | const FIRST_POINT = 1
12 | const DURATION = 200
13 |
14 | const THUMB_POSITION = {left: 'left', right: 'right'}
15 |
16 | export {
17 | DEFAULT_MINIMUM_VALUE,
18 | DEFAULT_MAXIMUM_VALUE,
19 | DEFAULT_STEP,
20 | INIT_POINT,
21 | FIRST_POINT,
22 | MINIMUM_TRACK_WIDTH,
23 | THUMB_POSITION,
24 | INIT_VALUE,
25 | VISIBLE,
26 | INVISIBLE,
27 | NEXT_STEP,
28 | PREVIOUS_STEP,
29 | DURATION,
30 | }
31 |
--------------------------------------------------------------------------------
/src/theme/components/Icon.ts:
--------------------------------------------------------------------------------
1 | import type {StyleProp, ImageStyle, ViewStyle} from 'react-native'
2 | import type {IconProps} from '../../components'
3 | import {metrics} from '../../helpers'
4 | import base from '../base'
5 |
6 | export type IconThemeProps = Pick & {
7 | /**
8 | * Default style for icon image
9 | */
10 | style?: StyleProp
11 | /**
12 | * Default style for icon button container
13 | */
14 | buttonStyle?: StyleProp
15 | }
16 |
17 | export const IconTheme: IconThemeProps = {
18 | size: metrics.medium,
19 | color: base.colors.black,
20 | disabled: false,
21 | resizeMode: 'contain',
22 | style: undefined,
23 | buttonStyle: undefined,
24 | }
25 |
--------------------------------------------------------------------------------
/src/theme/components/Card.ts:
--------------------------------------------------------------------------------
1 | import type {StyleProp, ViewStyle} from 'react-native'
2 | import {metrics} from '../../helpers'
3 | import base from '../base'
4 |
5 | export type CardThemeProps = {
6 | /**
7 | * Padding inside the card
8 | */
9 | padding: number
10 | /**
11 | * Border radius for the card
12 | */
13 | borderRadius: number
14 | /**
15 | * Background color of the card
16 | */
17 | backgroundColor: string
18 | /**
19 | * Custom style for the card container
20 | */
21 | style?: StyleProp
22 | }
23 |
24 | export const CardTheme: CardThemeProps = {
25 | padding: base.spacing.slim,
26 | borderRadius: metrics.borderRadius,
27 | backgroundColor: base.colors.cardBackground,
28 | style: undefined,
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Accordion/ToggleAnimation.ts:
--------------------------------------------------------------------------------
1 | import {LayoutAnimation} from 'react-native'
2 |
3 | export type AnimationType = 'easeInEaseOut' | 'easeIn' | 'easeOut' | 'keyboard' | 'linear' | 'spring'
4 |
5 | export const toggleAnimation = (
6 | openAnimation: AnimationType = 'easeInEaseOut',
7 | closeAnimation: AnimationType = 'easeInEaseOut',
8 | openDuration = 300,
9 | closeDuration = 300,
10 | ) => ({
11 | duration: 300,
12 | update: {
13 | duration: openDuration,
14 | property: LayoutAnimation.Properties.opacity,
15 | type: LayoutAnimation.Types[openAnimation],
16 | },
17 | delete: {
18 | duration: closeDuration,
19 | property: LayoutAnimation.Properties.opacity,
20 | type: LayoutAnimation.Types[closeAnimation],
21 | },
22 | })
23 |
--------------------------------------------------------------------------------
/src/theme/theme.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-interface */
2 | import type {ColorModeOptions} from '../core/color-mode/type'
3 | import base from './base'
4 | import components from './components'
5 |
6 | const config: ColorModeOptions = {
7 | useSystemColorMode: false,
8 | initialColorMode: 'light',
9 | }
10 |
11 | const darkColors = base.colors
12 |
13 | const theme = {
14 | ...base,
15 | components,
16 | config,
17 | darkColors,
18 | }
19 |
20 | type Theme = typeof theme
21 | type DarkColors = typeof darkColors
22 | type IColors = typeof theme.colors
23 | interface ICustomTheme {}
24 | interface ITheme extends ICustomTheme, Omit {}
25 |
26 | export {theme, Theme, ITheme, ICustomTheme, DarkColors, IColors}
27 |
--------------------------------------------------------------------------------
/example/components/ui/IconSymbol.ios.tsx:
--------------------------------------------------------------------------------
1 | import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
2 | import { StyleProp, ViewStyle } from 'react-native';
3 |
4 | export function IconSymbol({
5 | name,
6 | size = 24,
7 | color,
8 | style,
9 | weight = 'regular',
10 | }: {
11 | name: SymbolViewProps['name'];
12 | size?: number;
13 | color: string;
14 | style?: StyleProp;
15 | weight?: SymbolWeight;
16 | }) {
17 | return (
18 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/example/components/CodeInputDemo/DisabledState.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Text, View } from 'react-native'
3 | import { CodeInput } from 'rn-base-component'
4 | import { demoStyles } from './styles'
5 |
6 | export const DisabledState = () => {
7 | return (
8 |
9 | 🚫 Disabled State
10 |
11 |
12 | Pre-filled Disabled Input
13 |
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'react-native',
3 | setupFilesAfterEnv: [
4 | '/jest.setup.js',
5 | '/node_modules/react-native-gesture-handler/jestSetup.js',
6 | '/jest.setupFilesAfterEnv.ts',
7 | ],
8 | roots: [''],
9 | collectCoverage: true,
10 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
11 | modulePathIgnorePatterns: ['docs', 'lib', '.eslintrc'],
12 | transformIgnorePatterns: ['jest-runner'],
13 | transform: {
14 | '^.+\\.(js)$': '/node_modules/babel-jest',
15 | '\\.(ts|tsx)$': 'ts-jest',
16 | },
17 | testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$',
18 | testPathIgnorePatterns: ['\\.snap$', '/node_modules/'],
19 | cacheDirectory: '.jest/cache',
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/CodeInput/Cursor.tsx:
--------------------------------------------------------------------------------
1 | import React, {useEffect} from 'react'
2 | import type {StyleProp, TextStyle} from 'react-native'
3 | import Animated, {
4 | useAnimatedStyle,
5 | useSharedValue,
6 | withRepeat,
7 | withSequence,
8 | withTiming,
9 | } from 'react-native-reanimated'
10 |
11 | interface ICursorProps {
12 | style?: StyleProp
13 | }
14 |
15 | export const Cursor = ({style}: ICursorProps) => {
16 | const animatedValue = useSharedValue(0)
17 |
18 | useEffect(() => {
19 | animatedValue.value = withRepeat(withSequence(withTiming(1), withTiming(0)), -1)
20 | }, [animatedValue])
21 |
22 | const animatedStyle = useAnimatedStyle(() => ({opacity: animatedValue.value}), [])
23 |
24 | return |
25 | }
26 |
--------------------------------------------------------------------------------
/src/theme/components/Progress.ts:
--------------------------------------------------------------------------------
1 | // Define interface locally since IProgressProps is not exported
2 | import {metrics} from '../../helpers'
3 | import base from '../base'
4 | interface IProgressProps {
5 | value?: number
6 | size?: number
7 | borderRadius?: number
8 | filledTrackColor?: string
9 | backgroundColor?: string
10 | width?: number
11 | isIndeterminateProgress?: boolean
12 | }
13 |
14 | export type ProgressThemeProps = Pick<
15 | IProgressProps,
16 | 'size' | 'borderRadius' | 'filledTrackColor' | 'backgroundColor' | 'width'
17 | >
18 |
19 | export const ProgressTheme: ProgressThemeProps = {
20 | size: metrics.small,
21 | borderRadius: 0,
22 | filledTrackColor: base.colors.primary,
23 | backgroundColor: base.colors.gray,
24 | width: undefined, // Full width by default
25 | }
26 |
--------------------------------------------------------------------------------
/example/components/ExternalLink.tsx:
--------------------------------------------------------------------------------
1 | import { Href, Link } from 'expo-router';
2 | import { openBrowserAsync } from 'expo-web-browser';
3 | import { type ComponentProps } from 'react';
4 | import { Platform } from 'react-native';
5 |
6 | type Props = Omit, 'href'> & { href: Href & string };
7 |
8 | export function ExternalLink({ href, ...rest }: Props) {
9 | return (
10 | {
15 | if (Platform.OS !== 'web') {
16 | // Prevent the default behavior of linking to the default browser on native.
17 | event.preventDefault();
18 | // Open the link in an in-app browser.
19 | await openBrowserAsync(href);
20 | }
21 | }}
22 | />
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/example/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
4 | */
5 |
6 | const tintColorLight = '#0a7ea4';
7 | const tintColorDark = '#fff';
8 |
9 | export const Colors = {
10 | light: {
11 | text: '#11181C',
12 | background: '#fff',
13 | tint: tintColorLight,
14 | icon: '#687076',
15 | tabIconDefault: '#687076',
16 | tabIconSelected: tintColorLight,
17 | },
18 | dark: {
19 | text: '#ECEDEE',
20 | background: '#151718',
21 | tint: tintColorDark,
22 | icon: '#9BA1A6',
23 | tabIconDefault: '#9BA1A6',
24 | tabIconSelected: tintColorDark,
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/Slider/components/Track.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'
3 | import Animated from 'react-native-reanimated'
4 | import styled from 'styled-components/native'
5 | import type {ITheme} from '../../../theme'
6 |
7 | interface ITrack {
8 | style?: StyleProp
9 | onLayout?: (event: LayoutChangeEvent) => void
10 | }
11 |
12 | const Track: React.FC = ({style, onLayout}) => (
13 |
14 | )
15 |
16 | const TrackComponent = styled.View(({theme}: {theme: ITheme}) => ({
17 | flex: 1,
18 | borderRadius: theme.borderWidths?.huge,
19 | backgroundColor: theme?.colors?.backgroundPrimary,
20 | }))
21 |
22 | const TrackAnimatedComponent = Animated.createAnimatedComponent(TrackComponent)
23 |
24 | export {Track}
25 |
--------------------------------------------------------------------------------
/scripts/bootstrap.js:
--------------------------------------------------------------------------------
1 | const os = require('os')
2 | const path = require('path')
3 | const child_process = require('child_process')
4 |
5 | const root = path.resolve(__dirname, '..')
6 | const args = process.argv.slice(2)
7 | const options = {
8 | cwd: process.cwd(),
9 | env: process.env,
10 | stdio: 'inherit',
11 | encoding: 'utf-8',
12 | }
13 |
14 | if (os.type() === 'Windows_NT') {
15 | options.shell = true
16 | }
17 |
18 | let result
19 |
20 | if (process.cwd() !== root || args.length) {
21 | // We're not in the root of the project, or additional arguments were passed
22 | // In this case, forward the command to `yarn`
23 | result = child_process.spawnSync('yarn', args, options)
24 | } else {
25 | // If `yarn` is run without arguments, perform bootstrap
26 | result = child_process.spawnSync('yarn', ['bootstrap'], options)
27 | }
28 |
29 | process.exitCode = result.status
30 |
--------------------------------------------------------------------------------
/.github/workflows/on-pr-develop.yml:
--------------------------------------------------------------------------------
1 | name: Validate changes against develop
2 | on:
3 | pull_request:
4 | branches: ['develop']
5 |
6 | jobs:
7 | linting-typechecking:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v3
12 |
13 | - name: Setup Project and Cache
14 | uses: ./.github/actions/setup
15 |
16 | - name: Linting, Typechecking
17 | run: yarn lint
18 |
19 | unit-test:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Checkout
23 | uses: actions/checkout@v3
24 |
25 | - name: Setup Project and Cache
26 | uses: ./.github/actions/setup
27 |
28 | - name: Comment with Test Coverage
29 | uses: dkershner6/jest-coverage-commenter-action@v1
30 | with:
31 | github_token: '${{ secrets.GITHUB_TOKEN }}'
32 | comment_prefix: '## Test Result'
33 |
--------------------------------------------------------------------------------
/src/__tests__/TextBase.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render} from '@testing-library/react-native'
3 | import {Text, TextBold, TextItalic} from '../components'
4 | import {BaseProvider} from '../core'
5 |
6 | const renderWithProvider = (component: React.ReactElement) => render({component})
7 |
8 | describe('Text', () => {
9 | it('renders Text correctly', () => {
10 | const {getByText} = renderWithProvider(Hello, World!)
11 | expect(getByText('Hello, World!')).toBeDefined()
12 | })
13 |
14 | it('renders TextBold correctly', () => {
15 | const {getByText} = renderWithProvider(Hello, Bold Text!)
16 | expect(getByText('Hello, Bold Text!')).toBeDefined()
17 | })
18 |
19 | it('renders TextItalic correctly', () => {
20 | const {getByText} = renderWithProvider(Hello, Italic Text!)
21 | expect(getByText('Hello, Italic Text!')).toBeDefined()
22 | })
23 | })
24 |
--------------------------------------------------------------------------------
/docs/component-text.md:
--------------------------------------------------------------------------------
1 | # Text Component
2 |
3 | React Native Base Text Component
4 |
5 | This component extends from React Native Text
6 | with some variants
7 |
8 | ## Usage
9 |
10 | ```js
11 | import {Text, TextBold, TextItalic} from 'rn-base-component';
12 |
13 | Text font normal
14 | Text font bold
15 | Text font italic
16 | ```
17 |
18 | ## Default Props
19 |
20 | | Variant | Default Props |
21 | | :-----: | :-------------------------------------------------------------------------------------------: |
22 | | Text | fontSize: metrics.span
color: colors.black
fontFamily: typography.fonts.regular
|
23 | | Bold | fontFamily: typography.fonts.bold |
24 | | Italic | fontStyle: italic |
25 |
26 | ---
27 |
--------------------------------------------------------------------------------
/.github/actions/setup/action.yml:
--------------------------------------------------------------------------------
1 | name: Setup
2 | description: Setup Node.js and install dependencies
3 |
4 | runs:
5 | using: composite
6 | steps:
7 | - name: Enable Corepack before setting up Node
8 | run: corepack enable
9 | shell: bash
10 | - name: Setup Node.js
11 | uses: actions/setup-node@v3
12 | with:
13 | node-version-file: .nvmrc
14 | - name: Setup Yarn
15 | run: npm i -g yarn@1.22.22
16 | shell: bash
17 | - name: Cache dependencies
18 | id: yarn-cache
19 | uses: actions/cache@v3
20 | with:
21 | path: |
22 | **/node_modules
23 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
24 | restore-keys: |
25 | ${{ runner.os }}-yarn-
26 |
27 | - name: Install dependencies
28 | if: steps.yarn-cache.outputs.cache-hit != 'true'
29 | run: |
30 | yarn install --cwd example --frozen-lockfile
31 | yarn install --frozen-lockfile
32 | shell: bash
33 |
--------------------------------------------------------------------------------
/src/components/CodeInput/components/HelperText.tsx:
--------------------------------------------------------------------------------
1 | import React, {ReactNode} from 'react'
2 | import type {TextProps} from 'react-native'
3 | import styled from 'styled-components/native'
4 |
5 | interface HelperTextProps {
6 | helperText?: string
7 | helperComponent?: ReactNode
8 | helperTextProps?: TextProps
9 | }
10 |
11 | export const HelperText: React.FC = ({helperText, helperComponent, helperTextProps}) => {
12 | // Prioritize custom component over text
13 | if (helperComponent) {
14 | return {helperComponent}
15 | }
16 |
17 | // Fall back to text rendering
18 | return {helperText}
19 | }
20 |
21 | const HelperContainer = styled.View(({theme}) => ({
22 | marginTop: theme?.spacing?.tiny,
23 | }))
24 |
25 | const HelperTextStyled = styled.Text(({theme}) => ({
26 | fontSize: theme?.fontSizes?.['2xs'],
27 | color: theme?.colors?.gray,
28 | marginTop: theme?.spacing?.tiny,
29 | }))
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "rn-base-component": [
6 | "./src/index"
7 | ]
8 | },
9 | "allowUnreachableCode": false,
10 | "allowUnusedLabels": false,
11 | "esModuleInterop": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "jsx": "react",
14 | "lib": [
15 | "esnext"
16 | ],
17 | "module": "esnext",
18 | "moduleResolution": "node",
19 | "noFallthroughCasesInSwitch": true,
20 | "noImplicitReturns": true,
21 | "noImplicitUseStrict": false,
22 | "noStrictGenericChecks": false,
23 | "noUncheckedIndexedAccess": true,
24 | "noUnusedLocals": true,
25 | "noUnusedParameters": true,
26 | "resolveJsonModule": true,
27 | "skipLibCheck": true,
28 | "strict": true,
29 | "target": "esnext"
30 | },
31 | "include": [".eslintrc.js", "babel.config.js", "**/*.js", "**/*.ts", "**/*.tsx"],
32 | "exclude": ["example/**/*", "node_modules", "lib", "coverage"]
33 | }
34 |
--------------------------------------------------------------------------------
/example/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Stack } from 'expo-router'
2 | import { StatusBar } from 'expo-status-bar'
3 | import 'react-native-reanimated'
4 |
5 | import React from 'react'
6 | import { BaseProvider } from 'rn-base-component'
7 |
8 |
9 | export default function RootLayout() {
10 | return (
11 |
12 |
13 |
19 |
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-interface */
2 | import {requireNativeComponent, UIManager, Platform, ViewStyle} from 'react-native'
3 | import type {ITheme} from './theme'
4 |
5 | declare module 'styled-components/native' {
6 | export interface DefaultTheme extends ITheme {}
7 | }
8 |
9 | const LINKING_ERROR = `The package 'rn-base-component' doesn't seem to be linked. Make sure: \n\n ${Platform.select(
10 | {ios: "- You have run 'pod install'\n", default: ''},
11 | )} - You rebuilt the app after installing the package\n- You are not using Expo Go\n`
12 |
13 | type RnBaseComponentProps = {
14 | color: string
15 | style: ViewStyle
16 | }
17 |
18 | const ComponentName = 'RnBaseComponentView'
19 |
20 | export const RnBaseComponentView =
21 | UIManager.getViewManagerConfig(ComponentName) != null
22 | ? requireNativeComponent(ComponentName)
23 | : () => {
24 | throw new Error(LINKING_ERROR)
25 | }
26 |
27 | export * from './components'
28 | export * from './core'
29 | export * from './hooks'
30 | export * from './theme'
31 |
--------------------------------------------------------------------------------
/src/helpers/colors.ts:
--------------------------------------------------------------------------------
1 | import type {ColorValue} from 'react-native'
2 |
3 | export interface IColors {
4 | readonly primary: string
5 | readonly black: string
6 | readonly white: string
7 | readonly gray: string
8 | readonly red: string
9 | readonly textDisabled: string
10 | readonly placeHolderText: string
11 | readonly backgroundDisabled: string
12 | }
13 |
14 | const baseColor: IColors = {
15 | primary: '#7239E5',
16 | black: '#1F1F1F',
17 | white: '#ffffff',
18 | gray: '#454545',
19 | red: '#ff0009',
20 | textDisabled: '#666666',
21 | placeHolderText: '#929298',
22 | backgroundDisabled: '#e3e6e8',
23 | }
24 |
25 | const colors = {
26 | ...baseColor,
27 | }
28 |
29 | const getColorOpacity = (color: string, opacity: number): ColorValue | string | null | undefined => {
30 | if (opacity >= 0 && opacity <= 1 && color.includes('#')) {
31 | const hexValue = Math.round(opacity * 255).toString(16)
32 | return `${color.slice(0, 7)}${hexValue.padStart(2, '0').toUpperCase()}`
33 | }
34 | return color || null || undefined
35 | }
36 |
37 | export {colors, getColorOpacity}
38 |
--------------------------------------------------------------------------------
/src/core/extendTheme.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/ban-types */
2 | /* eslint-disable @typescript-eslint/no-explicit-any */
3 | import {theme as defaultTheme, Theme} from './../theme'
4 | import _ from 'lodash'
5 |
6 | function isFunction(value: any): boolean {
7 | return typeof value === 'function'
8 | }
9 |
10 | type ThemeUtil = Theme | (Record & {})
11 |
12 | export function extendTheme(overrides: T, ...restOverrides: T[]) {
13 | function customizer(source: any, override: any) {
14 | if (isFunction(source)) {
15 | return (...args: any[]) => {
16 | const sourceValue = source(...args)
17 | const overrideValue = isFunction(override) ? override(...args) : override
18 | return _.mergeWith({}, sourceValue, overrideValue, customizer)
19 | }
20 | }
21 | return undefined
22 | }
23 |
24 | const finalOverrides = [overrides, ...restOverrides].reduce(
25 | (prevValue, currentValue) => _.mergeWith({}, prevValue, currentValue, customizer),
26 | defaultTheme,
27 | )
28 |
29 | return finalOverrides as T & Theme
30 | }
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # XDE
6 | .expo/
7 |
8 | # VSCode
9 | .vscode/
10 | jsconfig.json
11 |
12 | # Xcode
13 | #
14 | build/
15 | *.pbxuser
16 | !default.pbxuser
17 | *.mode1v3
18 | !default.mode1v3
19 | *.mode2v3
20 | !default.mode2v3
21 | *.perspectivev3
22 | !default.perspectivev3
23 | xcuserdata
24 | *.xccheckout
25 | *.moved-aside
26 | DerivedData
27 | *.hmap
28 | *.ipa
29 | *.xcuserstate
30 | project.xcworkspace
31 |
32 | # Android/IJ
33 | #
34 | .classpath
35 | .cxx
36 | .gradle
37 | .idea
38 | .project
39 | .settings
40 | local.properties
41 | android.iml
42 |
43 | # Cocoapods
44 | #
45 | example/ios/Pods
46 |
47 | # Ruby
48 | example/vendor/
49 |
50 | # node.js
51 | #
52 | node_modules/
53 | npm-debug.log
54 | yarn-debug.log
55 | yarn-error.log
56 |
57 | # BUCK
58 | buck-out/
59 | \.buckd/
60 | android/app/libs
61 | android/keystores/debug.keystore
62 |
63 | # Expo
64 | .expo/
65 |
66 | # Turborepo
67 | .turbo/
68 |
69 | # generated by bob
70 | lib/
71 | # Jest
72 | #
73 | .jest/
74 | coverage/
75 |
76 | # Ruby / CocoaPods
77 | /ios/Pods/
78 | /vendor/bundle/
79 |
80 | .eslintcache
81 |
--------------------------------------------------------------------------------
/example/components/CodeInputDemo/AdditionalFeatures.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Text, View } from 'react-native'
3 | import { CodeInput } from 'rn-base-component'
4 | import { demoStyles } from './styles'
5 |
6 | export const AdditionalFeatures = () => {
7 | return (
8 |
9 | 💭 Additional Features
10 |
11 |
12 | With Cursor Animation
13 |
19 |
20 |
21 |
22 | Auto Focus Enabled
23 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "example",
4 | "slug": "example",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "example",
9 | "userInterfaceStyle": "automatic",
10 | "newArchEnabled": true,
11 | "ios": {
12 | "supportsTablet": true
13 | },
14 | "android": {
15 | "adaptiveIcon": {
16 | "foregroundImage": "./assets/images/adaptive-icon.png",
17 | "backgroundColor": "#ffffff"
18 | },
19 | "edgeToEdgeEnabled": true
20 | },
21 | "web": {
22 | "bundler": "metro",
23 | "output": "static",
24 | "favicon": "./assets/images/favicon.png"
25 | },
26 | "plugins": [
27 | "expo-router",
28 | [
29 | "expo-splash-screen",
30 | {
31 | "image": "./assets/images/splash-icon.png",
32 | "imageWidth": 200,
33 | "resizeMode": "contain",
34 | "backgroundColor": "#ffffff"
35 | }
36 | ]
37 | ],
38 | "experiments": {
39 | "typedRoutes": true
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: [
4 | '@react-native-community',
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react-hooks/recommended',
8 | 'plugin:@typescript-eslint/eslint-recommended',
9 | 'plugin:@typescript-eslint/recommended',
10 | ],
11 | parser: '@typescript-eslint/parser',
12 | plugins: ['@typescript-eslint', 'import', 'unused-imports'],
13 | overrides: [
14 | {
15 | files: ['*.ts', '*.tsx', '*.js'],
16 | rules: {
17 | semi: 'off',
18 | 'comma-dangle': 'off',
19 | 'no-shadow': 'off',
20 | 'no-undef': 'off',
21 | 'import/no-cycle': 'warn',
22 | 'import/first': 'error',
23 | 'import/no-duplicates': 'error',
24 | 'unused-imports/no-unused-imports': 'error',
25 | 'arrow-body-style': ['error', 'as-needed'],
26 | '@typescript-eslint/no-shadow': ['error'],
27 | '@typescript-eslint/no-empty-interface': 'warn',
28 | '@typescript-eslint/ban-types': 'warn',
29 | 'react/display-name': 'off',
30 | },
31 | },
32 | ],
33 | }
34 |
--------------------------------------------------------------------------------
/example/components/CodeInputDemo/ValidationStates.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Text, View } from 'react-native'
3 | import { CodeInput } from 'rn-base-component'
4 | import { demoStyles } from './styles'
5 |
6 | export const ValidationStates = () => {
7 | return (
8 |
9 | ✓ Validation States
10 |
11 |
12 | Success State
13 |
20 |
21 |
22 |
23 | Error State
24 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Text/Text.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components/native'
3 | import {Text as RNText, type TextProps as RNTextProps, type TextStyle} from 'react-native'
4 | import {useTheme} from '../../hooks'
5 |
6 | export type TextProps = RNTextProps & {
7 | /**
8 | * Custom font size
9 | */
10 | fontSize?: TextStyle['fontSize']
11 | /**
12 | * Text color
13 | */
14 | color?: string
15 | /**
16 | * Custom font family
17 | */
18 | fontFamily?: string
19 | }
20 |
21 | export const Text: React.FC = styled(RNText)(
22 | ({theme, fontSize, color, fontFamily}) => ({
23 | fontSize: fontSize ?? theme.components.Text.fontSize,
24 | color: color ?? theme.components.Text.color,
25 | fontFamily: fontFamily ?? theme.fonts.regular,
26 | }),
27 | )
28 |
29 | export const TextBold: React.FC = props =>
30 |
31 | export const TextItalic: React.FC = props =>
32 |
33 | const StyledTextItalic = styled(Text)`
34 | font-style: italic;
35 | `
36 |
--------------------------------------------------------------------------------
/example/components/HelloWave.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import Animated, {
4 | useAnimatedStyle,
5 | useSharedValue,
6 | withRepeat,
7 | withSequence,
8 | withTiming,
9 | } from 'react-native-reanimated';
10 |
11 | import { ThemedText } from '@/components/ThemedText';
12 |
13 | export function HelloWave() {
14 | const rotationAnimation = useSharedValue(0);
15 |
16 | useEffect(() => {
17 | rotationAnimation.value = withRepeat(
18 | withSequence(withTiming(25, { duration: 150 }), withTiming(0, { duration: 150 })),
19 | 4 // Run the animation 4 times
20 | );
21 | }, [rotationAnimation]);
22 |
23 | const animatedStyle = useAnimatedStyle(() => ({
24 | transform: [{ rotate: `${rotationAnimation.value}deg` }],
25 | }));
26 |
27 | return (
28 |
29 | 👋
30 |
31 | );
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | text: {
36 | fontSize: 28,
37 | lineHeight: 32,
38 | marginTop: -6,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/src/theme/components/Typography.ts:
--------------------------------------------------------------------------------
1 | import type {TextStyle} from 'react-native'
2 | import base from '../base'
3 | // Define types locally since TypographyProps is not exported
4 | export type TypographyVariant = 'h1' | 'h2' | 'regular' | 'bold'
5 | type TypographyProps = {
6 | color?: string
7 | variant?: TypographyVariant
8 | }
9 |
10 | export type TypographyThemeProps = Pick & {
11 | /**
12 | * Default typography variant styles
13 | */
14 | variantStyles: Record
15 | }
16 |
17 | export const TypographyTheme: TypographyThemeProps = {
18 | color: base.colors.darkText,
19 | variant: 'regular',
20 | variantStyles: {
21 | h1: {
22 | fontSize: 28,
23 | fontWeight: 'bold',
24 | lineHeight: 32,
25 | },
26 | h2: {
27 | fontSize: 24,
28 | fontWeight: 'bold',
29 | lineHeight: 28,
30 | },
31 | regular: {
32 | fontSize: 16,
33 | fontWeight: 'normal',
34 | lineHeight: 24,
35 | },
36 | bold: {
37 | fontWeight: 'bold',
38 | fontSize: 16,
39 | lineHeight: 24,
40 | },
41 | },
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 React Native Team
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20 | SOFTWARE.
21 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | // example/metro.config.js
2 | const path = require('path');
3 | const { getDefaultConfig } = require('expo/metro-config');
4 |
5 | const projectRoot = __dirname;
6 | const workspaceRoot = path.resolve(projectRoot, '..');
7 |
8 | const config = getDefaultConfig(projectRoot);
9 |
10 | // Watch the parent directory (the library source code)
11 | config.watchFolders = [workspaceRoot];
12 |
13 | // Add aliases to resolve to source code during development
14 | config.resolver.alias = {
15 | 'rn-base-component': path.resolve(workspaceRoot, 'src'),
16 | };
17 |
18 | // Node modules resolution order
19 | config.resolver.nodeModulesPaths = [
20 | path.resolve(projectRoot, 'node_modules'),
21 | path.resolve(workspaceRoot, 'node_modules'),
22 | ];
23 |
24 | // Include source extensions for better resolution
25 | config.resolver.sourceExts = [...config.resolver.sourceExts, 'ts', 'tsx'];
26 |
27 | // Disable hierarchical lookup for better performance
28 | config.resolver.disableHierarchicalLookup = true;
29 |
30 | // For better Metro performance with monorepo
31 | config.resolver.platforms = ['ios', 'android', 'native', 'web'];
32 |
33 | module.exports = config;
--------------------------------------------------------------------------------
/example/components/CodeInputDemo/utils.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StyleSheet, Text, View } from 'react-native'
3 | import { demoColors } from '../../theme/demoColors'
4 | import { demoMetrics } from '../../theme/demoMetrics'
5 |
6 | // Icon Components for demos
7 | export const LockIcon = () => (
8 |
9 | 🔒
10 |
11 | )
12 |
13 | export const CheckIcon = () => (
14 |
15 | ✓
16 |
17 | )
18 |
19 | export const ClearButton = () => (
20 |
21 | ✕
22 |
23 | )
24 |
25 | const styles = StyleSheet.create({
26 | iconContainer: {
27 | width: demoMetrics.iconSize.xlarge,
28 | height: demoMetrics.iconSize.xlarge,
29 | justifyContent: 'center',
30 | alignItems: 'center',
31 | backgroundColor: demoColors.iconBackground,
32 | borderRadius: demoMetrics.borderRadius.medium,
33 | marginHorizontal: demoMetrics.spacing.tiny,
34 | },
35 | icon: {
36 | fontSize: demoMetrics.fontSize.large,
37 | },
38 | })
39 |
--------------------------------------------------------------------------------
/example/components/CodeInputDemo/BasicExamples.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Text, View } from 'react-native'
3 | import { CodeInput } from 'rn-base-component'
4 | import { demoStyles } from './styles'
5 |
6 | export const BasicExamples = () => {
7 | return (
8 |
9 | 📝 Basic Examples
10 |
11 |
12 | Default with Label
13 |
14 |
15 |
16 |
17 | Required Field
18 |
24 |
25 |
26 |
27 | Different Length (5 digits)
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/TextInput/components/CustomIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type {ImageResizeMode, ImageSourcePropType, ImageStyle, StyleProp, ViewStyle} from 'react-native'
3 | import styled from 'styled-components/native'
4 | import {Icon} from '../../Icon/Icon'
5 |
6 | export interface CustomIconProps {
7 | source: ImageSourcePropType
8 | size?: number
9 | color?: string
10 | resizeMode?: ImageResizeMode
11 | iconContainerStyle?: StyleProp
12 | iconStyle?: StyleProp
13 | style?: StyleProp
14 | onPress?: () => void
15 | }
16 |
17 | export const CustomIcon: React.FC = ({
18 | source,
19 | size,
20 | color,
21 | resizeMode,
22 | iconContainerStyle,
23 | iconStyle,
24 | onPress,
25 | }) => (
26 |
27 |
36 |
37 | )
38 |
39 | const IconWrapper = styled.View(({theme}) => ({
40 | marginHorizontal: theme?.spacing?.small,
41 | }))
42 |
--------------------------------------------------------------------------------
/src/theme/components/CountDown.ts:
--------------------------------------------------------------------------------
1 | import {StyleProp, TextStyle} from 'react-native'
2 | import type {CountDownProps} from '../../components'
3 | import {metrics} from '../../helpers'
4 | import base from '../base'
5 |
6 | export type CountDownThemeProps = Pick<
7 | CountDownProps,
8 | 'fontSize' | 'textColor' | 'fontFamily' | 'textStyle'
9 | > & {
10 | /**
11 | * Font size for time unit labels (d, h, m, s)
12 | */
13 | labelFontSize: number
14 | /**
15 | * Color for time unit labels
16 | */
17 | labelColor: string
18 | /**
19 | * Font family for time unit labels
20 | */
21 | fontFamily: string
22 | /**
23 | * Custom text style for countdown text
24 | */
25 | textStyle?: StyleProp
26 | /**
27 | * Custom text style for time unit labels
28 | */
29 | unitTextStyle?: StyleProp
30 | }
31 |
32 | export const CountDownTheme: CountDownThemeProps = {
33 | fontSize: metrics.large,
34 | textColor: base.colors.black,
35 | labelFontSize: metrics.small,
36 | labelColor: base.colors.gray,
37 | fontFamily: base.fonts.regular as string,
38 | textStyle: undefined, // Optional custom text style
39 | unitTextStyle: undefined, // Optional custom unit text style
40 | }
41 |
--------------------------------------------------------------------------------
/src/theme/components/index.ts:
--------------------------------------------------------------------------------
1 | import {AccordionTheme} from './Accordion'
2 | import {
3 | ButtonTheme,
4 | ButtonOutlineTheme,
5 | ButtonPrimaryTheme,
6 | ButtonSecondaryTheme,
7 | ButtonTransparentTheme,
8 | } from './Button'
9 | import {CardTheme} from './Card'
10 | import {CheckboxTheme} from './Checkbox'
11 | import {CodeInputTheme} from './CodeInput'
12 | import {CountDownTheme} from './CountDown'
13 | import {IconTheme} from './Icon'
14 | import {ProgressTheme} from './Progress'
15 | import {RadioButtonTheme} from './RadioButton'
16 | import {SliderTheme} from './Slider'
17 | import {TextTheme} from './Text'
18 | import {TextInputTheme} from './TextInput'
19 | import {TypographyTheme} from './Typography'
20 |
21 | export default {
22 | Accordion: AccordionTheme,
23 | Button: ButtonTheme,
24 | ButtonOutline: ButtonOutlineTheme,
25 | ButtonPrimary: ButtonPrimaryTheme,
26 | ButtonSecondary: ButtonSecondaryTheme,
27 | ButtonTransparent: ButtonTransparentTheme,
28 | Card: CardTheme,
29 | Checkbox: CheckboxTheme,
30 | CodeInput: CodeInputTheme,
31 | CountDown: CountDownTheme,
32 | Icon: IconTheme,
33 | Progress: ProgressTheme,
34 | RadioButton: RadioButtonTheme,
35 | Slider: SliderTheme,
36 | Text: TextTheme,
37 | TextInput: TextInputTheme,
38 | Typography: TypographyTheme,
39 | }
40 |
--------------------------------------------------------------------------------
/example/components/CodeInputDemo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ScrollView, Text, View } from 'react-native'
3 | import { demoStyles } from './styles'
4 | import { InteractivePlayground } from './InteractivePlayground'
5 | import { BasicExamples } from './BasicExamples'
6 | import { ValidationStates } from './ValidationStates'
7 | import { IconsAndComponents } from './IconsAndComponents'
8 | import { SecurityAndStyling } from './SecurityAndStyling'
9 | import { AdvancedCellStyling } from './AdvancedCellStyling'
10 | import { DisabledState } from './DisabledState'
11 | import { AdditionalFeatures } from './AdditionalFeatures'
12 |
13 | export const CodeInputDemo = () => {
14 | return (
15 |
16 | CodeInput Component Demo
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 🎯 Explore all CodeInput features above!
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/src/theme/base/typography.ts:
--------------------------------------------------------------------------------
1 | import {Platform} from 'react-native'
2 |
3 | const typography = {
4 | fontWeights: {
5 | hairline: 100,
6 | thin: 200,
7 | light: 300,
8 | normal: 400,
9 | medium: 500,
10 | semibold: 600,
11 | bold: 700,
12 | extrabold: 800,
13 | black: 900,
14 | extraBlack: 950,
15 | },
16 | fonts: {
17 | heading: undefined,
18 | body: undefined,
19 | mono: undefined,
20 | regular: Platform.select({
21 | ios: 'ArialMT',
22 | android: 'sans-serif',
23 | }),
24 | bold: Platform.select({
25 | ios: 'Arial-BoldMT',
26 | android: 'sans-serif-medium',
27 | }),
28 | italic: Platform.select({
29 | ios: 'Arial-ItalicMT',
30 | android: 'sans-serif-medium',
31 | }),
32 | },
33 | fontSizes: {
34 | '2xs': 10,
35 | xs: 12,
36 | sm: 14,
37 | md: 16,
38 | lg: 18,
39 | xl: 20,
40 | '2xl': 24,
41 | '3xl': 30,
42 | '4xl': 36,
43 | '5xl': 48,
44 | '6xl': 60,
45 | '7xl': 72,
46 | '8xl': 96,
47 | '9xl': 128,
48 | },
49 | } as const
50 |
51 | export type ITypography = typeof typography
52 | export type IFontSize = keyof typeof typography.fontSizes
53 | export type IFontWeight = keyof typeof typography.fontWeights
54 | export type IFont = keyof typeof typography.fonts
55 |
56 | export default typography
57 |
--------------------------------------------------------------------------------
/example/components/CodeInputDemo/IconsAndComponents.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Text, View } from 'react-native'
3 | import { CodeInput } from 'rn-base-component'
4 | import { demoStyles } from './styles'
5 | import { LockIcon, CheckIcon, ClearButton } from './utils'
6 |
7 | export const IconsAndComponents = () => {
8 | return (
9 |
10 | 🎨 Icons & Components
11 |
12 |
13 | Left Icon Component
14 | }
18 | secureTextEntry
19 | />
20 |
21 |
22 |
23 | Right Icon Component
24 | }
29 | success
30 | />
31 |
32 |
33 |
34 | Both Left & Right Components
35 | }
39 | rightComponent={}
40 | helperText="Tap X to clear"
41 | />
42 |
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/src/theme/base/colors.ts:
--------------------------------------------------------------------------------
1 | const colors = {
2 | backgroundPrimary: '#F5F2F1',
3 | primary: '#0e7490',
4 | secondary: '#be185d',
5 | tertiary: '#047857',
6 | danger: '#be123c',
7 | error: '#b91c1c',
8 | success: '#15803d',
9 | warning: '#c2410c',
10 | muted: '#404040',
11 | info: '#0369a1',
12 | light: '#44403c',
13 | white: '#FFFFFF',
14 | black: '#000000',
15 | darkText: '#27272a',
16 | rose: '#be123c',
17 | pink: '#be185d',
18 | fuchsia: '#a21caf',
19 | purple: '#7e22ce',
20 | violet: '#6d28d9',
21 | indigo: '#4338ca',
22 | blue: '#1d4ed8',
23 | lightBlue: '#0369a1',
24 | darkBlue: '#004282',
25 | cyan: '#0e7490',
26 | teal: '#0f766e',
27 | emerald: '#047857',
28 | green: '#15803d',
29 | lime: '#4d7c0f',
30 | yellow: '#a16207',
31 | amber: '#b45309',
32 | orange: '#c2410c',
33 | red: '#b91c1c',
34 | warmGray: '#44403c',
35 | trueGray: '#404040',
36 | gray: '#3f3f46',
37 | coolGray: '#374151',
38 | blueGray: '#334155',
39 | dark: '#18181b',
40 | transparent: '#00000000',
41 | lightBackground: '#FFFFFF',
42 | backgroundSecondary: '#D1D1D1',
43 | mainBackground: '#F0F2F3',
44 | cardPrimaryBackground: '#0A906E',
45 |
46 | // border
47 | primaryBorder: '#454545',
48 |
49 | // text
50 | textColor: '#0B0B0B',
51 | cardBackground: '#ffffff',
52 | lightTextColor: '#FFFFFF',
53 | darkTextColor: '#333333',
54 | placeHolderText: '#929298',
55 | errorText: '#ff0009',
56 | }
57 |
58 | export type IColors = keyof typeof colors
59 | export default colors
60 |
--------------------------------------------------------------------------------
/example/components/Collapsible.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, useState } from 'react';
2 | import { StyleSheet, TouchableOpacity } from 'react-native';
3 |
4 | import { ThemedText } from '@/components/ThemedText';
5 | import { ThemedView } from '@/components/ThemedView';
6 | import { IconSymbol } from '@/components/ui/IconSymbol';
7 | import { Colors } from '@/constants/Colors';
8 | import { useColorScheme } from '@/hooks/useColorScheme';
9 |
10 | export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
11 | const [isOpen, setIsOpen] = useState(false);
12 | const theme = useColorScheme() ?? 'light';
13 |
14 | return (
15 |
16 | setIsOpen((value) => !value)}
19 | activeOpacity={0.8}>
20 |
27 |
28 | {title}
29 |
30 | {isOpen && {children}}
31 |
32 | );
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | heading: {
37 | flexDirection: 'row',
38 | alignItems: 'center',
39 | gap: 6,
40 | },
41 | content: {
42 | marginTop: 6,
43 | marginLeft: 24,
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/example/components/ThemedText.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Text, type TextProps } from 'react-native';
2 |
3 | import { useThemeColor } from '@/hooks/useThemeColor';
4 |
5 | export type ThemedTextProps = TextProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
9 | };
10 |
11 | export function ThemedText({
12 | style,
13 | lightColor,
14 | darkColor,
15 | type = 'default',
16 | ...rest
17 | }: ThemedTextProps) {
18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
19 |
20 | return (
21 |
33 | );
34 | }
35 |
36 | const styles = StyleSheet.create({
37 | default: {
38 | fontSize: 16,
39 | lineHeight: 24,
40 | },
41 | defaultSemiBold: {
42 | fontSize: 16,
43 | lineHeight: 24,
44 | fontWeight: '600',
45 | },
46 | title: {
47 | fontSize: 32,
48 | fontWeight: 'bold',
49 | lineHeight: 32,
50 | },
51 | subtitle: {
52 | fontSize: 20,
53 | fontWeight: 'bold',
54 | },
55 | link: {
56 | lineHeight: 30,
57 | fontSize: 16,
58 | color: '#0a7ea4',
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/example/components/ui/IconSymbol.tsx:
--------------------------------------------------------------------------------
1 | // Fallback for using MaterialIcons on Android and web.
2 |
3 | import MaterialIcons from '@expo/vector-icons/MaterialIcons';
4 | import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
5 | import { ComponentProps } from 'react';
6 | import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
7 |
8 | type IconMapping = Record['name']>;
9 | type IconSymbolName = keyof typeof MAPPING;
10 |
11 | /**
12 | * Add your SF Symbols to Material Icons mappings here.
13 | * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
14 | * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
15 | */
16 | const MAPPING = {
17 | 'house.fill': 'home',
18 | 'paperplane.fill': 'send',
19 | 'chevron.left.forwardslash.chevron.right': 'code',
20 | 'chevron.right': 'chevron-right',
21 | } as IconMapping;
22 |
23 | /**
24 | * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
25 | * This ensures a consistent look across platforms, and optimal resource usage.
26 | * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
27 | */
28 | export function IconSymbol({
29 | name,
30 | size = 24,
31 | color,
32 | style,
33 | }: {
34 | name: IconSymbolName;
35 | size?: number;
36 | color: string | OpaqueColorValue;
37 | style?: StyleProp;
38 | weight?: SymbolWeight;
39 | }) {
40 | return ;
41 | }
42 |
--------------------------------------------------------------------------------
/src/core/BaseProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, {createContext, useCallback, useMemo, useState} from 'react'
2 | import {ThemeProvider} from 'styled-components/native'
3 | import {ITheme, theme as defaultTheme} from '../theme'
4 | import type {ColorMode, IColorModeContextProps} from './color-mode/type'
5 |
6 | export type IBaseContext = {theme: ITheme} | IColorModeContextProps
7 | export const BaseContext = createContext(null)
8 |
9 | export interface BaseProviderProps {
10 | children?: React.ReactNode
11 | theme?: ITheme
12 | }
13 |
14 | export const BaseProvider = ({children, theme = defaultTheme}: BaseProviderProps) => {
15 | const [colorModeValue, setColorModeValue] = useState(theme?.config.initialColorMode)
16 |
17 | const isLight = useMemo(() => colorModeValue === 'light', [colorModeValue])
18 |
19 | const darkTheme = useMemo(() => ({...theme, colors: {...theme.colors, ...theme.darkColors}}), [theme])
20 |
21 | const newTheme = useMemo(() => (isLight ? theme : darkTheme), [darkTheme, isLight, theme])
22 |
23 | const toggleColorMode = useCallback(() => {
24 | isLight ? setColorModeValue('dark') : setColorModeValue('light')
25 | }, [isLight])
26 |
27 | const setColorMode = useCallback((value: ColorMode) => {
28 | setColorModeValue(value)
29 | }, [])
30 |
31 | return (
32 |
39 | {children}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/theme/components/Accordion.ts:
--------------------------------------------------------------------------------
1 | import base from '../base'
2 | import type {ViewStyle} from 'react-native'
3 | import type {IFontWeight} from '../base/typography'
4 |
5 | export interface AccordionThemeProps {
6 | // Container styles
7 | container: {
8 | paddingBottom: number
9 | overflow: ViewStyle['overflow']
10 | }
11 |
12 | // Header styles
13 | header: {
14 | padding: number
15 | }
16 |
17 | // Title/Text styles
18 | title: {
19 | fontSize: number
20 | textAlign: 'center' | 'left' | 'right' | 'justify'
21 | color: string
22 | fontWeight: IFontWeight
23 | }
24 |
25 | // Content/Body styles
26 | body: {
27 | padding: number
28 | justifyContent: ViewStyle['justifyContent']
29 | alignItems: ViewStyle['alignItems']
30 | }
31 |
32 | // Animation settings
33 | animation: {
34 | openDuration: number
35 | closeDuration: number
36 | }
37 |
38 | // Interactive styles
39 | interactive: {
40 | activeOpacity: number
41 | }
42 | }
43 |
44 | export const AccordionTheme: AccordionThemeProps = {
45 | container: {
46 | paddingBottom: base.spacing.petite,
47 | overflow: 'hidden',
48 | },
49 |
50 | header: {
51 | padding: base.spacing.compact,
52 | },
53 |
54 | title: {
55 | fontSize: base.fontSizes.xl,
56 | textAlign: 'center',
57 | color: base.colors.amber,
58 | fontWeight: 'bold',
59 | },
60 |
61 | body: {
62 | padding: base.spacing.compact,
63 | justifyContent: 'center',
64 | alignItems: 'center',
65 | },
66 |
67 | animation: {
68 | openDuration: 300,
69 | closeDuration: 300,
70 | },
71 |
72 | interactive: {
73 | activeOpacity: 0.7,
74 | },
75 | }
76 |
--------------------------------------------------------------------------------
/src/theme/components/RadioButton.ts:
--------------------------------------------------------------------------------
1 | import type {StyleProp, ViewStyle, TextStyle} from 'react-native'
2 | import type {IRadioButtonProps} from '../../components'
3 | // responsiveHeight is available but not used in theme defaults
4 | import base from '../base'
5 |
6 | export type RadioButtonThemeProps = Pick<
7 | IRadioButtonProps,
8 | 'outerSize' | 'innerSize' | 'ringColor' | 'innerBackgroundColor' | 'disabled' | 'disableOpacity' | 'initial'
9 | > & {
10 | /**
11 | * Default style for radio button wrapper
12 | */
13 | wrapperStyle?: StyleProp
14 | /**
15 | * Default style for radio button ring
16 | */
17 | style?: StyleProp
18 | /**
19 | * Default style for inner container
20 | */
21 | innerContainerStyle?: StyleProp
22 | /**
23 | * Default style for text container
24 | */
25 | textContainerStyle?: StyleProp
26 | /**
27 | * Default style for text label
28 | */
29 | textStyle?: StyleProp
30 | /**
31 | * Border width for radio button ring
32 | */
33 | borderWidth: number
34 | }
35 |
36 | const OUTER_SIZE_DEFAULT = 45
37 | const INNER_SIZE_DEFAULT = 25
38 | const OPACITY_DEFAULT = 0.5
39 |
40 | export const RadioButtonTheme: RadioButtonThemeProps = {
41 | outerSize: OUTER_SIZE_DEFAULT,
42 | innerSize: INNER_SIZE_DEFAULT,
43 | ringColor: base.colors.darkBlue,
44 | innerBackgroundColor: base.colors.darkBlue,
45 | disabled: false,
46 | disableOpacity: OPACITY_DEFAULT,
47 | initial: false,
48 | borderWidth: base.borderWidths.little,
49 | wrapperStyle: undefined,
50 | style: undefined,
51 | innerContainerStyle: undefined,
52 | textContainerStyle: undefined,
53 | textStyle: undefined,
54 | }
55 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo start --android",
9 | "ios": "expo start --ios",
10 | "web": "expo start --web",
11 | "lint": "expo lint"
12 | },
13 | "dependencies": {
14 | "@expo/vector-icons": "^14.1.0",
15 | "@react-navigation/bottom-tabs": "^7.3.10",
16 | "@react-navigation/elements": "^2.3.8",
17 | "@react-navigation/native": "^7.1.6",
18 | "expo": "~53.0.20",
19 | "expo-blur": "~14.1.5",
20 | "expo-constants": "~17.1.7",
21 | "expo-font": "~13.3.2",
22 | "expo-haptics": "~14.1.4",
23 | "expo-image": "~2.4.0",
24 | "expo-linking": "~7.1.7",
25 | "expo-router": "~5.1.4",
26 | "expo-splash-screen": "~0.30.10",
27 | "expo-status-bar": "~2.2.3",
28 | "expo-symbols": "~0.4.5",
29 | "expo-system-ui": "~5.0.10",
30 | "expo-web-browser": "~14.2.0",
31 | "react": "19.0.0",
32 | "react-dom": "19.0.0",
33 | "react-native": "0.79.5",
34 | "react-native-gesture-handler": "~2.24.0",
35 | "react-native-reanimated": "~3.17.4",
36 | "react-native-safe-area-context": "5.4.0",
37 | "react-native-screens": "~4.11.1",
38 | "react-native-web": "~0.20.0",
39 | "react-native-webview": "13.13.5",
40 | "rn-base-component": "file:../",
41 | "styled-components": "^6.1.19"
42 | },
43 | "devDependencies": {
44 | "@babel/core": "^7.25.2",
45 | "@types/react": "~19.0.10",
46 | "eslint": "^9.25.0",
47 | "eslint-config-expo": "~9.2.0",
48 | "typescript": "~5.8.3"
49 | },
50 | "private": true
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Card/Card.tsx:
--------------------------------------------------------------------------------
1 | import React, {PropsWithChildren} from 'react'
2 | import type {StyleProp, ViewStyle} from 'react-native'
3 | import styled from 'styled-components/native'
4 | import {activeOpacity} from '../../helpers'
5 | import {useTheme} from '../../hooks'
6 |
7 | export interface CardProps extends PropsWithChildren {
8 | /**
9 | * handle press on card
10 | */
11 | onPress?: () => void
12 |
13 | /**
14 | * style for card
15 | */
16 | style?: StyleProp | Array>
17 |
18 | /**
19 | * padding inside the card
20 | */
21 | padding?: number
22 |
23 | /**
24 | * border radius for the card
25 | */
26 | borderRadius?: number
27 |
28 | /**
29 | * background color of the card
30 | */
31 | backgroundColor?: string
32 | }
33 |
34 | export const Card: React.FC = ({
35 | onPress,
36 | style,
37 | children,
38 | padding,
39 | borderRadius,
40 | backgroundColor,
41 | }) => {
42 | const CardTheme = useTheme().components.Card
43 |
44 | return (
45 |
53 | {children}
54 |
55 | )
56 | }
57 |
58 | const CardWrapper = styled.TouchableOpacity<{
59 | padding: number
60 | borderRadius: number
61 | backgroundColor: string
62 | }>(({padding, borderRadius, backgroundColor}) => ({
63 | padding,
64 | borderRadius,
65 | backgroundColor,
66 | }))
67 |
--------------------------------------------------------------------------------
/example/components/CodeInputDemo/SecurityAndStyling.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Text, View } from 'react-native'
3 | import { CodeInput } from 'rn-base-component'
4 | import { demoStyles, customCellStyles } from './styles'
5 |
6 | export const SecurityAndStyling = () => {
7 | return (
8 |
9 | 🔐 Security & Styling
10 |
11 |
12 | Secure Text Entry
13 |
19 |
20 |
21 |
22 | Placeholder as Dots
23 |
29 |
30 |
31 |
32 | Custom Placeholder
33 |
39 |
40 |
41 |
42 | Custom Cell Styling
43 |
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type {
3 | ImageStyle,
4 | ImageSourcePropType,
5 | ImageResizeMode,
6 | StyleProp,
7 | ViewStyle,
8 | Insets,
9 | } from 'react-native'
10 | import {Image, StyleSheet, TouchableOpacity} from 'react-native'
11 | import {hitSlop as defaultHitSlop} from '../../helpers/metrics'
12 | import {useTheme} from '../../hooks'
13 |
14 | export type IconProps = {
15 | source: ImageSourcePropType
16 | size?: number
17 | disabled?: boolean
18 | color?: string
19 | hitSlop?: Insets
20 | style?: StyleProp
21 | resizeMode?: ImageResizeMode
22 | testID?: string
23 | onPress?: () => void
24 | onLongPress?: () => void
25 | buttonStyle?: StyleProp
26 | }
27 |
28 | export const Icon: React.FunctionComponent = ({
29 | source,
30 | size,
31 | disabled,
32 | color,
33 | hitSlop = defaultHitSlop,
34 | style,
35 | resizeMode,
36 | testID,
37 | onPress,
38 | onLongPress,
39 | buttonStyle,
40 | }) => {
41 | const IconTheme = useTheme().components.Icon
42 |
43 | return (
44 |
51 |
64 |
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/.cursor/rules/project-structure.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | alwaysApply: true
3 | ---
4 |
5 | # RN Base Component Library - Project Structure
6 |
7 | This is a React Native base component library with the following key architecture:
8 |
9 | ## Core Entry Points
10 | - Main entry: [src/index.tsx](mdc:src/index.tsx) - exports all components, core, hooks, and theme
11 | - Components index: [src/components/index.ts](mdc:src/components/index.ts) - exports all available components
12 | - Core functionality: [src/core/index.ts](mdc:src/core/index.ts) - BaseProvider and theme extension utilities
13 | - Theme system: [src/theme/index.ts](mdc:src/theme/index.ts) - theme definitions and configuration
14 | - Hooks: [src/hooks/index.ts](mdc:src/hooks/index.ts) - useTheme and useBase hooks
15 |
16 | ## Package Configuration
17 | - Package definition: [package.json](mdc:package.json) - uses react-native-builder-bob for multi-format builds
18 | - TypeScript config: [tsconfig.json](mdc:tsconfig.json) - strict TypeScript settings with path mapping
19 | - Build output: `lib/` directory with commonjs, module, and typescript formats
20 |
21 | ## Component Architecture
22 | Each component follows a consistent structure:
23 | - Main component file (e.g., `Button.tsx`)
24 | - Variant components (e.g., `ButtonPrimary.tsx`, `ButtonOutline.tsx`)
25 | - Barrel export in `index.ts`
26 | - Documentation in `README.md`
27 | - Type definitions in separate files when complex
28 |
29 | ## Theme System
30 | - Uses styled-components with custom theme interface
31 | - [src/core/BaseProvider.tsx](mdc:src/core/BaseProvider.tsx) - provides theme context and color mode switching
32 | - Supports light/dark mode with automatic theme switching
33 | - Centralized color, spacing, and component styling definitions
34 |
35 | ## Example App
36 | - Located in `example/` directory using Expo
37 | - Demonstrates component usage and integration patterns
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your Expo app 👋
2 |
3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
4 |
5 | ## Get started
6 |
7 | 1. Install dependencies
8 |
9 | ```bash
10 | npm install
11 | ```
12 |
13 | 2. Start the app
14 |
15 | ```bash
16 | npx expo start
17 | ```
18 |
19 | In the output, you'll find options to open the app in a
20 |
21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
25 |
26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
27 |
28 | ## Get a fresh project
29 |
30 | When you're ready, run:
31 |
32 | ```bash
33 | npm run reset-project
34 | ```
35 |
36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
37 |
38 | ## Learn more
39 |
40 | To learn more about developing your project with Expo, look at the following resources:
41 |
42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
44 |
45 | ## Join the community
46 |
47 | Join our community of developers creating universal apps.
48 |
49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
51 |
--------------------------------------------------------------------------------
/src/__tests__/Typography.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render} from '@testing-library/react-native'
3 | import {Typography} from '../components'
4 | import {typographyVariantStyles} from '../components/Typography/Typography'
5 | import {BaseProvider} from '../core'
6 |
7 | const renderWithProvider = (component: React.ReactElement) => render({component})
8 |
9 | describe('Typography Component', () => {
10 | it('renders text with default style and variant', () => {
11 | const {getByText} = renderWithProvider(Test Text)
12 | const textElement = getByText('Test Text')
13 |
14 | // Verify default variant is 'regular'
15 | const regularStyle = typographyVariantStyles.regular
16 | expect(textElement).toHaveStyle(regularStyle)
17 | })
18 |
19 | it('applies the specified typography variant style', () => {
20 | const {getByText} = renderWithProvider(Test Text)
21 | const textElement = getByText('Test Text')
22 |
23 | // Verify variant style is applied
24 | const h1Style = typographyVariantStyles.h1
25 | expect(textElement).toHaveStyle(h1Style)
26 | })
27 |
28 | it('applies custom style', () => {
29 | const customStyle = {fontSize: 20, color: 'red'}
30 | const {getByText} = renderWithProvider(Test Text)
31 | const textElement = getByText('Test Text')
32 |
33 | // Verify custom style is applied
34 | expect(textElement).toHaveStyle(customStyle)
35 | })
36 |
37 | it('applies specified color', () => {
38 | const customColor = 'blue'
39 | const {getByText} = renderWithProvider(Test Text)
40 | const textElement = getByText('Test Text')
41 |
42 | // Verify color is applied
43 | expect(textElement).toHaveStyle({color: customColor})
44 | })
45 |
46 | it('renders text correctly', () => {
47 | const {getByText} = renderWithProvider(Test Text)
48 | const textElement = getByText('Test Text')
49 |
50 | // Verify text is rendered correctly
51 | expect(textElement).toBeTruthy()
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/src/components/Slider/components/TrackPoint.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import type {Insets, StyleProp, ViewStyle} from 'react-native'
3 | import styled from 'styled-components/native'
4 | import type {FlexDirection, Position, TrackPointStyle} from '../Slider'
5 | import {FIRST_POINT} from '../constants'
6 |
7 | interface TrackPointProps {
8 | sliderWidth: number
9 | totalPoint: number
10 | hitSlopPoint?: Insets | number
11 | activeOpacity?: number
12 | trackPointStyle?: StyleProp
13 | onPressPoint?: (point: number) => void
14 | }
15 |
16 | const TrackPoint: React.FunctionComponent = React.memo(
17 | ({
18 | sliderWidth,
19 | totalPoint,
20 | trackPointStyle,
21 | hitSlopPoint,
22 | activeOpacity,
23 | onPressPoint,
24 | }: TrackPointProps) => {
25 | // We don't need to display the first point on the track, so we removed it using totalPoint - 1
26 | const range = sliderWidth / totalPoint - FIRST_POINT
27 |
28 | // Render the track points based on the range
29 | return (
30 |
31 | {/**
32 | * Loop through the range of the slider track and render a point for each value
33 | */}
34 | {Array(totalPoint - FIRST_POINT)
35 | .fill(0)
36 | .map((_, i) => (
37 | onPressPoint?.(i)}
42 | activeOpacity={activeOpacity}
43 | style={[trackPointStyle, {left: range * (i + FIRST_POINT)}]}
44 | />
45 | ))}
46 |
47 | )
48 | },
49 | )
50 |
51 | const TrackPointComponent = styled.View(props => ({
52 | width: props.width,
53 | flexDirection: 'row' as FlexDirection,
54 | height: '100%',
55 | position: 'absolute' as Position,
56 | overflow: 'hidden',
57 | }))
58 |
59 | const Point = styled.TouchableOpacity(({theme}) => ({
60 | height: '100%',
61 | width: 1,
62 | backgroundColor: theme.colors.primary,
63 | }))
64 |
65 | TrackPoint.displayName = 'TrackPoint'
66 |
67 | export {TrackPoint}
68 |
--------------------------------------------------------------------------------
/.cursor/rules/development-workflow.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | alwaysApply: true
3 | ---
4 | # Development Workflow
5 |
6 | ## Development Scripts
7 | Available scripts from [package.json](mdc:package.json):
8 |
9 | ### Code Quality
10 | - `yarn lint` - Run all linting checks (ESLint, Prettier, TypeScript)
11 | - `yarn lint:fix` - Auto-fix linting issues
12 | - `yarn typecheck` - TypeScript type checking only
13 | - `yarn format` - Format code with Prettier
14 |
15 | ### Testing
16 | - `yarn test` - Run Jest tests
17 | - `yarn update-test` - Update Jest snapshots
18 | - Coverage reports generated in `coverage/` directory
19 |
20 | ### Building
21 | - `yarn prepack` - Build library for distribution using react-native-builder-bob
22 | - Outputs: `lib/commonjs/`, `lib/module/`, `lib/typescript/`
23 | - Build config in `react-native-builder-bob` section of package.json
24 |
25 | ### Example App
26 | - `yarn example` - Navigate to example directory
27 | - `yarn bootstrap` - Set up development environment
28 | - Example app uses Expo for testing components
29 |
30 | ## Git Workflow
31 | - Uses Husky for git hooks
32 | - Lint-staged for pre-commit formatting
33 | - Conventional commits with commitlint
34 | - Release process with release-it
35 |
36 | ## Development Environment Setup
37 | 1. `yarn install` - Install dependencies
38 | 2. `yarn bootstrap` - Set up example app
39 | 3. `yarn example ios` or `yarn example android` - Run example app
40 |
41 | ## Code Quality Standards
42 | - ESLint with React Native configuration
43 | - Prettier for code formatting
44 | - Strict TypeScript configuration
45 | - 100% import/export consistency
46 |
47 | ## Build Targets
48 | - CommonJS (`lib/commonjs/`) - for Node.js environments
49 | - ES Modules (`lib/module/`) - for modern bundlers
50 | - TypeScript definitions (`lib/typescript/`) - for type checking
51 |
52 | ## Release Process
53 | - Uses release-it with conventional changelog
54 | - Automatic version bumping based on commit messages
55 | - GitHub releases with generated changelogs
56 | - NPM publishing with proper registry configuration
57 |
58 | ## File Watching and Development
59 | - Use `yarn example start` for live development
60 | - Metro bundler for React Native hot reloading
61 | - TypeScript compilation in watch mode during development
--------------------------------------------------------------------------------
/.cursor/rules/component-development.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | alwaysApply: true
3 | ---
4 | # Component Development Standards
5 |
6 | ## Component Structure Guidelines
7 |
8 | ### File Organization
9 |
10 | - Each component should have its own directory under `src/components/`
11 | - Use PascalCase for component names and file names
12 | - Include these files in each component directory:
13 | - Main component file (e.g., `Button.tsx`)
14 | - Variant components (e.g., `ButtonPrimary.tsx`, `ButtonOutline.tsx`)
15 | - `index.ts` for barrel exports
16 | - `README.md` for component documentation
17 | - Type definitions file if complex (e.g., `types.ts`)
18 |
19 | ### Component Implementation
20 |
21 | - Use functional components with TypeScript
22 | - Export component props interface with descriptive naming (e.g., `ButtonProps`)
23 | - Use styled-components for styling with theme integration
24 | - Import theme types: `import type {ITheme} from '../../theme'`
25 | - Use theme-aware styling: `${({theme}) => theme.colors.primary}`
26 |
27 | ### Prop Design
28 |
29 | - Use clear, descriptive prop names
30 | - Provide sensible defaults
31 | - Support common React Native props (style, testID, etc.)
32 | - Use discriminated unions for variant props when appropriate
33 | - Document complex props with JSDoc comments
34 |
35 | ### Export Pattern
36 |
37 | ```typescript
38 | // In component directory index.ts
39 | export * from './ComponentName'
40 | export * from './ComponentVariant'
41 |
42 | // In main components/index.ts
43 | export * from './ComponentName'
44 | ```
45 |
46 | ### Styling Best Practices
47 |
48 | - Use theme values consistently
49 | - Support responsive design through theme breakpoints
50 | - Implement proper focus states and accessibility
51 | - Use semantic color names from theme
52 | - Support both light and dark themes
53 |
54 | ### Example Component Structure
55 |
56 | ```typescript
57 | interface ButtonProps {
58 | variant?: 'primary' | 'secondary' | 'outline'
59 | size?: 'sm' | 'md' | 'lg'
60 | disabled?: boolean
61 | children: React.ReactNode
62 | onPress?: () => void
63 | // ... other props
64 | }
65 |
66 | export const Button: React.FC = ({variant = 'primary', size = 'md', ...props}) => {
67 | // Implementation
68 | }
69 | ```
70 |
--------------------------------------------------------------------------------
/docs/jest-config.md:
--------------------------------------------------------------------------------
1 | # Jest Configuration
2 |
3 | ## Installation
4 |
5 | ```
6 | npm install --save-dev jest
7 | ```
8 |
9 | or
10 |
11 | ```
12 | yarn add --dev jest
13 | ```
14 |
15 | ## Setup
16 |
17 | Create file **jest.config.json** then adding these lines
18 |
19 | ```
20 | {
21 | "preset": "react-native",
22 | "roots": ["/src"],
23 | "collectCoverage": true,
24 | "moduleFileExtensions": ["ts", "tsx", "js"],
25 | "transform": {
26 | "^.+\\.(js)$": "/node_modules/babel-jest",
27 | "\\.(ts|tsx)$": "ts-jest"
28 | },
29 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
30 | "testPathIgnorePatterns": ["\\.snap$", "/node_modules/"],
31 | "cacheDirectory": ".jest/cache"
32 | }
33 |
34 | ```
35 |
36 | `preset`: "react-native" specifies that Jest should use the preset configuration for React Native projects.
37 |
38 | `roots`: ["/src"] tells Jest where to look for the source code files to test. In this case, it is set to look in the /src directory of the project's root directory.
39 |
40 | `collectCoverage`: true indicates that Jest should collect code coverage information.
41 |
42 | `moduleFileExtensions`: ["ts", "tsx", "js"] lists the file extensions that Jest should consider as modules.
43 |
44 | `transform`: {...} specifies the transformation process that Jest should apply to the files it is testing. The code transforms JavaScript files using Babel and TypeScript files using ts-jest.
45 |
46 | `testRegex`: "(/**tests**/.\*|\\.(test|spec))\\.(ts|tsx|js)$" specifies the regular expression pattern for test files. In this case, Jest will look for files with a .test.ts, .test.tsx, or .test.js extension in the /**tests** directory, or with a .spec.ts, .spec.tsx, or .spec.js extension.
47 |
48 | `testPathIgnorePatterns`: ["\\.snap$", "/node_modules/"] lists the file paths that Jest should ignore when running tests. In this case, it is ignoring .snap files and files in the node_modules directory.
49 |
50 | `cacheDirectory`: ".jest/cache" specifies the directory where Jest should store its cache files.
51 |
52 | ## Feedback
53 |
54 | If you have any feedback, please reach out to us at ...
55 |
56 | ## License
57 |
58 | [MIT](https://choosealicense.com/licenses/mit/)
59 |
60 | ## Support
61 |
62 | For support, email ... or join our Slack channel.
63 |
--------------------------------------------------------------------------------
/src/theme/base/shadows.ts:
--------------------------------------------------------------------------------
1 | const shadows = {
2 | tiny: {
3 | shadowColor: '#000',
4 | shadowOffset: {
5 | width: 0,
6 | height: 1,
7 | },
8 | shadowOpacity: 0.18,
9 | shadowRadius: 1.0,
10 | elevation: 1,
11 | },
12 | miniature: {
13 | shadowColor: '#000',
14 | shadowOffset: {
15 | width: 0,
16 | height: 1,
17 | },
18 | shadowOpacity: 0.2,
19 | shadowRadius: 1.41,
20 | elevation: 2,
21 | },
22 | petite: {
23 | shadowColor: '#000',
24 | shadowOffset: {
25 | width: 0,
26 | height: 1,
27 | },
28 | shadowOpacity: 0.22,
29 | shadowRadius: 2.22,
30 | elevation: 3,
31 | },
32 | small: {
33 | shadowColor: '#000',
34 | shadowOffset: {
35 | width: 0,
36 | height: 2,
37 | },
38 | shadowOpacity: 0.23,
39 | shadowRadius: 2.62,
40 | elevation: 4,
41 | },
42 | little: {
43 | shadowColor: '#000',
44 | shadowOffset: {
45 | width: 0,
46 | height: 2,
47 | },
48 | shadowOpacity: 0.25,
49 | shadowRadius: 3.84,
50 | elevation: 5,
51 | },
52 | compact: {
53 | shadowColor: '#000',
54 | shadowOffset: {
55 | width: 0,
56 | height: 3,
57 | },
58 | shadowOpacity: 0.27,
59 | shadowRadius: 4.65,
60 | elevation: 6,
61 | },
62 | narrow: {
63 | shadowColor: '#000',
64 | shadowOffset: {
65 | width: 0,
66 | height: 3,
67 | },
68 | shadowOpacity: 0.29,
69 | shadowRadius: 4.65,
70 | elevation: 7,
71 | },
72 | slim: {
73 | shadowColor: '#000',
74 | shadowOffset: {
75 | width: 0,
76 | height: 4,
77 | },
78 | shadowOpacity: 0.3,
79 | shadowRadius: 4.65,
80 | elevation: 8,
81 | },
82 | moderate: {
83 | shadowColor: '#000',
84 | shadowOffset: {
85 | width: 0,
86 | height: 4,
87 | },
88 | shadowOpacity: 0.32,
89 | shadowRadius: 5.46,
90 | elevation: 9,
91 | },
92 | average: {
93 | shadowColor: '#000',
94 | shadowOffset: {
95 | width: 0,
96 | height: 5,
97 | },
98 | shadowOpacity: 0.34,
99 | shadowRadius: 6.27,
100 | elevation: 10,
101 | },
102 | }
103 |
104 | export type IShadows = keyof typeof shadows
105 | export default shadows
106 |
--------------------------------------------------------------------------------
/example/components/ParallaxScrollView.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren, ReactElement } from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import Animated, {
4 | interpolate,
5 | useAnimatedRef,
6 | useAnimatedStyle,
7 | useScrollViewOffset,
8 | } from 'react-native-reanimated';
9 |
10 | import { ThemedView } from '@/components/ThemedView';
11 | import { useBottomTabOverflow } from '@/components/ui/TabBarBackground';
12 | import { useColorScheme } from '@/hooks/useColorScheme';
13 |
14 | const HEADER_HEIGHT = 250;
15 |
16 | type Props = PropsWithChildren<{
17 | headerImage: ReactElement;
18 | headerBackgroundColor: { dark: string; light: string };
19 | }>;
20 |
21 | export default function ParallaxScrollView({
22 | children,
23 | headerImage,
24 | headerBackgroundColor,
25 | }: Props) {
26 | const colorScheme = useColorScheme() ?? 'light';
27 | const scrollRef = useAnimatedRef();
28 | const scrollOffset = useScrollViewOffset(scrollRef);
29 | const bottom = useBottomTabOverflow();
30 | const headerAnimatedStyle = useAnimatedStyle(() => {
31 | return {
32 | transform: [
33 | {
34 | translateY: interpolate(
35 | scrollOffset.value,
36 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
37 | [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
38 | ),
39 | },
40 | {
41 | scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
42 | },
43 | ],
44 | };
45 | });
46 |
47 | return (
48 |
49 |
54 |
60 | {headerImage}
61 |
62 | {children}
63 |
64 |
65 | );
66 | }
67 |
68 | const styles = StyleSheet.create({
69 | container: {
70 | flex: 1,
71 | },
72 | header: {
73 | height: HEADER_HEIGHT,
74 | overflow: 'hidden',
75 | },
76 | content: {
77 | flex: 1,
78 | padding: 32,
79 | gap: 16,
80 | overflow: 'hidden',
81 | },
82 | });
83 |
--------------------------------------------------------------------------------
/src/__tests__/RadioButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, fireEvent} from '@testing-library/react-native'
3 | import {RadioButton} from '../components'
4 | import {StyleSheet} from 'react-native'
5 | import {BaseProvider} from '../core/BaseProvider'
6 |
7 | const renderWithProvider = (component: React.ReactElement) => render({component})
8 |
9 | describe('RadioButton test', () => {
10 | const onPressMock = jest.fn()
11 |
12 | beforeEach(() => {
13 | jest.clearAllMocks()
14 | })
15 |
16 | it('should render correctly', () => {
17 | const {getByTestId} = renderWithProvider()
18 | expect(getByTestId('container')).toBeDefined()
19 | })
20 |
21 | it('should call on press', () => {
22 | const {getByTestId} = renderWithProvider()
23 | const radioButton = getByTestId('bounceable')
24 |
25 | fireEvent.press(radioButton)
26 | expect(onPressMock).toHaveBeenCalled()
27 | })
28 |
29 | it('should change state when pressed', () => {
30 | const {getByTestId} = renderWithProvider()
31 | const radionButton = getByTestId('bounceable')
32 | const circle = getByTestId('circle')
33 |
34 | fireEvent.press(radionButton)
35 |
36 | const styles = StyleSheet.flatten(circle.props.style)
37 |
38 | expect(styles.backgroundColor).toBe('#004282')
39 |
40 | fireEvent.press(radionButton)
41 | const stylesAfterPress = StyleSheet.flatten(circle.props.style)
42 | expect(stylesAfterPress.backgroundColor).toBe('transparent')
43 | })
44 |
45 | it('should not change state when disabled', () => {
46 | const {getByTestId} = renderWithProvider()
47 | const radioButton = getByTestId('bounceable')
48 |
49 | fireEvent.press(radioButton)
50 | expect(onPressMock).not.toHaveBeenCalled()
51 | })
52 |
53 | it('should be active state when initial is set to true', () => {
54 | const {getByTestId} = renderWithProvider()
55 | const circle = getByTestId('circle')
56 |
57 | expect(circle.props.style.backgroundColor).toBe('#004282')
58 | })
59 |
60 | it('should be remain state', () => {
61 | const {getByTestId} = renderWithProvider()
62 | const radionButton = getByTestId('bounceable')
63 | const circle = getByTestId('circle')
64 |
65 | fireEvent.press(radionButton)
66 | expect(onPressMock).not.toHaveBeenCalled()
67 |
68 | expect(circle.props.style.backgroundColor).toBe('transparent')
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/.cursor/rules/typescript-standards.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | alwaysApply: true
3 | ---
4 | # TypeScript Coding Standards
5 |
6 | ## Type Safety Requirements
7 | - Always use strict TypeScript settings as defined in [tsconfig.json](mdc:tsconfig.json)
8 | - Enable `noUncheckedIndexedAccess: true` for safer array/object access
9 | - Use `noUnusedLocals: true` and `noUnusedParameters: true` to prevent dead code
10 | - No `any` types - use proper typing or `unknown` with type guards
11 |
12 | ## Import/Export Standards
13 | - Use explicit imports and exports
14 | - Prefer named exports over default exports for components
15 | - Use barrel exports in `index.ts` files for clean imports
16 | - Import types with `import type` syntax for type-only imports
17 |
18 | ## Interface and Type Definitions
19 | - Use PascalCase for interface names
20 | - Prefix interfaces with `I` when defining core types (e.g., `ITheme`)
21 | - Use descriptive names for props interfaces (e.g., `ButtonProps`, `TextInputProps`)
22 | - Define props interfaces in the same file as the component
23 | - Use discriminated unions for variant props
24 |
25 | ## Component Typing
26 | - Always type component props explicitly
27 | - Use `React.FC` or direct function typing
28 | - Type event handlers properly (e.g., `onPress?: () => void`)
29 | - Use proper React Native types (e.g., `ViewStyle`, `TextStyle`)
30 |
31 | ## Theme Integration
32 | - Import theme types: `import type {ITheme} from '../theme'`
33 | - Use theme interface for styled-components: `DefaultTheme extends ITheme`
34 | - Type theme props in styled components: `${({theme}: {theme: ITheme}) => ...}`
35 |
36 | ## Generic Types
37 | - Use descriptive generic type names (not just `T`)
38 | - Constrain generics when appropriate
39 | - Use conditional types for complex prop relationships
40 |
41 | ## Error Handling
42 | - Use proper error types, not just `Error`
43 | - Type async functions with proper return types
44 | - Use Result types or proper error boundaries for component error handling
45 |
46 | ## Example Patterns
47 | ```typescript
48 | // Proper component props interface
49 | interface ButtonProps {
50 | variant: 'primary' | 'secondary' | 'outline'
51 | size: 'sm' | 'md' | 'lg'
52 | disabled?: boolean
53 | onPress?: () => void
54 | children: React.ReactNode
55 | style?: ViewStyle
56 | testID?: string
57 | }
58 |
59 | // Theme-aware styled component
60 | const StyledButton = styled.TouchableOpacity<{variant: string}>`
61 | ${({theme, variant}: {theme: ITheme; variant: string}) => css`
62 | background-color: ${theme.colors[variant]};
63 | padding: ${theme.space.md}px;
64 | `}
65 | `
66 |
67 | // Proper type imports
68 | import type {ITheme} from '../theme'
69 | import type {ViewStyle} from 'react-native'
70 | ```
--------------------------------------------------------------------------------
/src/theme/components/TextInput.ts:
--------------------------------------------------------------------------------
1 | import type {StyleProp, ViewStyle, TextStyle} from 'react-native'
2 | import type {TextInputProps} from '../../components'
3 | import {metrics} from '../../helpers'
4 | import base from '../base'
5 |
6 | export type TextInputThemeProps = Pick & {
7 | /**
8 | * Style for container
9 | */
10 | containerStyle?: StyleProp
11 | /**
12 | * Styling for Input Component Container
13 | */
14 | inputContainerStyle?: StyleProp
15 | /**
16 | * Style for Input Component
17 | */
18 | inputStyle?: StyleProp
19 | /**
20 | * Styling for the label
21 | */
22 | labelStyle?: StyleProp
23 | /**
24 | * Placeholder text color
25 | */
26 | placeholderTextColor: string
27 | /**
28 | * Border color for input
29 | */
30 | borderColor: string
31 | /**
32 | * Border color when input is focused
33 | */
34 | focusedBorderColor: string
35 | /**
36 | * Background color for input
37 | */
38 | backgroundColor: string
39 | /**
40 | * Text color for input
41 | */
42 | textColor: string
43 | /**
44 | * Label text color
45 | */
46 | labelColor: string
47 | /**
48 | * Error text color
49 | */
50 | errorColor: string
51 | /**
52 | * Border radius for input
53 | */
54 | borderRadius: number
55 | /**
56 | * Border width for input
57 | */
58 | borderWidth: number
59 | /**
60 | * Padding for input
61 | */
62 | padding: number
63 | /**
64 | * Font size for input text
65 | */
66 | fontSize: number
67 | /**
68 | * Font size for label
69 | */
70 | labelFontSize: number
71 | /**
72 | * Font size for error text
73 | */
74 | errorFontSize: number
75 | }
76 |
77 | export const TextInputTheme: TextInputThemeProps = {
78 | editable: true,
79 | numberOfLines: 1,
80 | multiline: false,
81 | containerStyle: undefined,
82 | inputContainerStyle: {
83 | borderWidth: 1,
84 | borderRadius: metrics.borderRadius,
85 | paddingHorizontal: base.spacing.slim,
86 | paddingVertical: base.spacing.small,
87 | },
88 | inputStyle: {
89 | fontSize: 16,
90 | color: base.colors.black,
91 | },
92 | labelStyle: {
93 | fontSize: 14,
94 | marginBottom: base.spacing.tiny,
95 | color: base.colors.darkText,
96 | },
97 | placeholderTextColor: base.colors.gray,
98 | borderColor: base.colors.primaryBorder,
99 | focusedBorderColor: base.colors.primary,
100 | backgroundColor: base.colors.white,
101 | textColor: base.colors.black,
102 | labelColor: base.colors.darkText,
103 | errorColor: base.colors.error,
104 | borderRadius: metrics.borderRadius,
105 | borderWidth: 1,
106 | padding: base.spacing.slim,
107 | fontSize: 16,
108 | labelFontSize: 14,
109 | errorFontSize: 12,
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/Typography/Typography.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components/native'
3 | import type {TextProps, TextStyle} from 'react-native'
4 | import {useTheme} from '../../hooks'
5 |
6 | const StyledText = styled.Text>(({theme, color}) => ({
7 | flexShrink: 1,
8 | color: color ?? theme.colors?.darkText,
9 | }))
10 | /**
11 | * Enumeration of typography variants.
12 | *
13 | * @typedef {('h1' | 'h2' | 'regular' | 'bold')} TypographyVariant
14 | */
15 | export type TypographyVariant = 'h1' | 'h2' | 'regular' | 'bold'
16 | type TypographyVariantStyles = {
17 | // If you wish to add fontFamily, please do it here.
18 | // Ex: fontFamily: 'Roboto-Bold' | 'Roboto-Regular' | 'Roboto-SemiBold'
19 | } & TextStyle
20 |
21 | type TypographyProps = {
22 | color?: string
23 | variant?: TypographyVariant
24 | } & TextProps
25 |
26 | /**
27 | * Mapping of typography variant styles to TextStyle objects.
28 | *
29 | * @constant
30 | * @type {Record}
31 | */
32 | // Default variant styles - can be overridden by theme
33 | export const typographyVariantStyles: Record = {
34 | h1: {
35 | fontSize: 28,
36 | fontWeight: 'bold',
37 | lineHeight: 32,
38 | },
39 | h2: {
40 | fontSize: 24,
41 | fontWeight: 'bold',
42 | lineHeight: 28,
43 | },
44 | regular: {
45 | fontSize: 16,
46 | fontWeight: 'normal',
47 | lineHeight: 24,
48 | },
49 | bold: {
50 | fontWeight: 'bold',
51 | fontSize: 16,
52 | lineHeight: 24,
53 | },
54 | }
55 |
56 | /**
57 | * Typography component that renders styled text elements based on the provided typography variant.
58 | *
59 | * @component
60 | * @param {TypographyProps} props - The properties of the Typography component.
61 | * @param {TypographyVariant} [props.variant='regular'] - The typography variant that defines the text style.
62 | * @param {string} [props.color] - The color of the text.
63 | * @param {...TextProps} [props.rest] - Additional text properties that can be passed down to the underlying `StyledText` component.
64 | * @returns {JSX.Element} The styled text element.
65 | */
66 | export const Typography: React.FC = ({
67 | variant,
68 | color,
69 | style,
70 | ...rest
71 | }: TypographyProps): JSX.Element => {
72 | const TypographyTheme = useTheme().components.Typography
73 | const actualVariant = variant ?? TypographyTheme.variant ?? 'regular'
74 | const actualColor = color ?? TypographyTheme.color
75 |
76 | // Get variant styles from theme or fallback to default
77 | const variantStyles = TypographyTheme.variantStyles[actualVariant] ?? typographyVariantStyles[actualVariant]
78 |
79 | return (
80 |
90 | )
91 | }
92 |
--------------------------------------------------------------------------------
/.cursor/rules/theme-styling.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | alwaysApply: true
3 | ---
4 | # Theme and Styling Guidelines
5 |
6 | ## Theme Architecture
7 | - Core theme definition: [src/theme/theme.ts](mdc:src/theme/theme.ts)
8 | - Theme provider: [src/core/BaseProvider.tsx](mdc:src/core/BaseProvider.tsx)
9 | - Theme hooks: [src/hooks/useTheme.tsx](mdc:src/hooks/useTheme.tsx) and [src/hooks/useBase.tsx](mdc:src/hooks/useBase.tsx)
10 | - Extended theme utility: [src/core/extendTheme.tsx](mdc:src/core/extendTheme.tsx)
11 |
12 | ## Styled Components Integration
13 | - Use styled-components/native for all component styling
14 | - Extend DefaultTheme interface: `export interface DefaultTheme extends ITheme {}`
15 | - Access theme in styled components: `${({theme}) => theme.colors.primary}`
16 | - Use theme-aware CSS helper for complex styling
17 |
18 | ## Color System
19 | - Support both light and dark color modes
20 | - Define colors in theme with semantic names (primary, secondary, background, etc.)
21 | - Use `theme.colors` for all color references
22 | - Implement color mode switching through BaseProvider
23 | - Dark theme colors should be defined in `theme.darkColors`
24 |
25 | ## Spacing and Layout
26 | - Use consistent spacing scale from `theme.space`
27 | - Define breakpoints for responsive design in `theme.breakpoints`
28 | - Use theme metrics for consistent sizing: `theme.fontSizes`, `theme.lineHeights`
29 |
30 | ## Component Theming
31 | - Store component-specific theme overrides in `src/theme/components/`
32 | - Use theme variants for component styling variations
33 | - Support size variants through theme configuration
34 | - Enable theme customization through extendTheme utility
35 |
36 | ## Styling Best Practices
37 | ```typescript
38 | // ✅ Good - Theme-aware styled component
39 | const StyledButton = styled.TouchableOpacity<{variant: string}>`
40 | ${({theme, variant}) => css`
41 | background-color: ${theme.colors[variant]};
42 | padding: ${theme.space.md}px ${theme.space.lg}px;
43 | border-radius: ${theme.radii.md}px;
44 |
45 | ${variant === 'outline' && css`
46 | border: 1px solid ${theme.colors.border};
47 | background-color: transparent;
48 | `}
49 | `}
50 | `
51 |
52 | // ✅ Good - Using theme hook
53 | const MyComponent = () => {
54 | const {theme} = useTheme()
55 | return (
56 |
57 | {/* content */}
58 |
59 | )
60 | }
61 |
62 | // ❌ Bad - Hardcoded values
63 | const BadButton = styled.TouchableOpacity`
64 | background-color: #007AFF;
65 | padding: 12px 16px;
66 | border-radius: 8px;
67 | `
68 | ```
69 |
70 | ## Theme Customization
71 | - Use extendTheme to customize default theme
72 | - Support partial theme overrides
73 | - Maintain backward compatibility when extending theme
74 | - Document theme customization in component README files
75 |
76 | ## Color Mode Support
77 | - Test components in both light and dark modes
78 | - Use semantic color names that work in both modes
79 | - Provide color mode toggle functionality
80 | - Store color mode preference properly
81 |
82 | ## Performance Considerations
83 | - Memoize theme-dependent calculations
84 | - Use theme context efficiently
85 | - Avoid recreating theme objects unnecessarily
86 | - Consider theme switching performance impact
--------------------------------------------------------------------------------
/src/components/RadioButton/Bounceable.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Pressable, StyleSheet, View} from 'react-native'
3 | import type {PressableProps, ViewStyle, StyleProp} from 'react-native'
4 | import Animated, {withSpring, useSharedValue, useAnimatedStyle} from 'react-native-reanimated'
5 |
6 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
7 |
8 | type CustomStyleProp = StyleProp | Array>
9 |
10 | export interface IBounceableProps extends PressableProps {
11 | /**
12 | * The target value when onPressIn
13 | * default: 0.93
14 | */
15 | bounceEffectIn?: number
16 | /**
17 | * The target value when onPressOut
18 | * default: 1
19 | */
20 | bounceEffectOut?: number
21 | /**
22 | * The initial velocity of the animation in units per second when onPressIn
23 | * default: 0.1
24 | */
25 | bounceVelocityIn?: number
26 | /**
27 | * The initial velocity of the animation in units per second when onPressOut
28 | * default: 0.4
29 | */
30 | bounceVelocityOut?: number
31 | /**
32 | * The bounciness parameter determines how bouncy the animation should be when onPress
33 | * default: 0
34 | */
35 | bouncinessValue?: number
36 | /**
37 | * Custom style for Bounceable
38 | */
39 | style?: CustomStyleProp
40 | }
41 |
42 | const BOUNCE_EFFECT_IN_DEFAULT = 0.93
43 | const BOUNCE_EFFECT_OUT_DEFAULT = 1
44 | const BOUNCE_VELOCITY_IN_DEFAULT = 0.1
45 | const BOUNCE_VELOCITY_OUT_DEFAULT = 0.4
46 | const BOUNCINESS_VALUE_DEFAULT = 80
47 | const SCALE_DEFAULT = 1
48 |
49 | export const Bounceable = React.forwardRef(
50 | (
51 | {
52 | bounceEffectIn = BOUNCE_EFFECT_IN_DEFAULT,
53 | bounceEffectOut = BOUNCE_EFFECT_OUT_DEFAULT,
54 | bounceVelocityIn = BOUNCE_VELOCITY_IN_DEFAULT,
55 | bounceVelocityOut = BOUNCE_VELOCITY_OUT_DEFAULT,
56 | bouncinessValue = BOUNCINESS_VALUE_DEFAULT,
57 | children,
58 | style,
59 | onPress,
60 | ...rest
61 | },
62 | ref,
63 | ) => {
64 | const scale = useSharedValue(SCALE_DEFAULT)
65 | const velocity = useSharedValue(bounceVelocityIn)
66 | const bounciness = useSharedValue(bouncinessValue)
67 |
68 | const animatedStyle = useAnimatedStyle(
69 | () => ({
70 | transform: [
71 | {
72 | scale: withSpring(scale.value, {
73 | stiffness: bounciness.value,
74 | velocity: velocity.value,
75 | }),
76 | },
77 | ],
78 | }),
79 | [scale.value, bounciness.value, velocity.value],
80 | )
81 |
82 | const handlePressIn = () => {
83 | scale.value = bounceEffectIn
84 | velocity.value = bounceVelocityIn
85 | }
86 |
87 | const handlePressOut = () => {
88 | scale.value = bounceEffectOut
89 | velocity.value = bounceVelocityOut
90 | }
91 |
92 | return (
93 |
100 | {children}
101 |
102 | )
103 | },
104 | )
105 |
--------------------------------------------------------------------------------
/src/__tests__/Checkbox.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {render, fireEvent} from '@testing-library/react-native'
3 | import {Checkbox} from '../components'
4 | import {BaseProvider} from '../core/BaseProvider'
5 |
6 | jest.mock('../hooks/useTheme', () => ({
7 | useTheme: jest.fn(() => ({
8 | components: {
9 | Checkbox: {
10 | fillColor: '#0B0B0B',
11 | unfillColor: '#00000000',
12 | },
13 | },
14 | })),
15 | }))
16 |
17 | const renderWithProvider = (component: React.ReactElement) => render({component})
18 |
19 | describe('Checkbox test', () => {
20 | const onPressMock = jest.fn()
21 |
22 | beforeEach(() => {
23 | jest.clearAllMocks()
24 | })
25 |
26 | // TODO: Fix tests later
27 | // it('should trigger bounceEffect on press', async () => {
28 | // const {getByTestId} = renderComponent()
29 | // const container = getByTestId('container')
30 | // const icon = getByTestId('icon-container')
31 |
32 | // fireEvent(container, 'pressIn')
33 |
34 | // expect(icon.props.style[1].transform[0].scale).toEqual({value: BOUNCE_EFFECT_IN})
35 | // })
36 |
37 | // it('should trigger bounceOutEffect on press', () => {
38 | // const {getByTestId} = renderComponent()
39 | // const container = getByTestId('container')
40 | // const icon = getByTestId('icon-container')
41 |
42 | // fireEvent(container, 'pressOut')
43 |
44 | // expect(icon.props.style[1].transform[0].scale).toEqual({value: BOUNCE_EFFECT_OUT})
45 | // })
46 |
47 | it('should render correctly', () => {
48 | const {getByTestId} = renderWithProvider()
49 | expect(getByTestId('container')).toBeDefined()
50 | })
51 |
52 | it('should call on press', () => {
53 | const {getByTestId} = renderWithProvider()
54 | const checkbox = getByTestId('container')
55 |
56 | fireEvent.press(checkbox)
57 | expect(onPressMock).toHaveBeenCalled()
58 | })
59 |
60 | it('should change state when pressed', () => {
61 | const {getByTestId} = renderWithProvider()
62 | const checkbox = getByTestId('container')
63 | const icon = getByTestId('icon-container')
64 |
65 | fireEvent.press(checkbox)
66 | expect(icon.props.style[0].backgroundColor).toEqual('#0B0B0B')
67 |
68 | fireEvent.press(checkbox)
69 | expect(icon.props.style[0].backgroundColor).toEqual('#00000000')
70 | })
71 |
72 | it('should not change state when disabled', () => {
73 | const {getByTestId} = renderWithProvider()
74 | const checkbox = getByTestId('container')
75 | const icon = getByTestId('icon-container')
76 |
77 | fireEvent.press(checkbox)
78 | expect(icon.props.style[0].backgroundColor).toEqual('#00000000')
79 | })
80 |
81 | it('label should be null', () => {
82 | const {queryByTestId} = renderWithProvider()
83 | const label = queryByTestId('label')
84 | expect(label).toBeNull()
85 | })
86 |
87 | it('label should be set', () => {
88 | const {getByTestId} = renderWithProvider()
89 | const label = getByTestId('label')
90 |
91 | expect(label.props.children).toEqual('checkbox text')
92 | })
93 | })
94 |
--------------------------------------------------------------------------------
/.cursor/rules/testing-guidelines.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | alwaysApply: true
3 | ---
4 | # Testing Guidelines
5 |
6 | ## Testing Framework Setup
7 | - Use Jest with React Native Testing Library as configured in [jest.config.js](mdc:jest.config.js)
8 | - Setup file: [jest.setup.js](mdc:jest.setup.js) and [jest.setupFilesAfterEnv.ts](mdc:jest.setupFilesAfterEnv.ts)
9 | - Mock definitions in [src/__mocks__/](mdc:src/__mocks__/) directory
10 | - Test files located in [src/__tests__/](mdc:src/__tests__/) directory
11 |
12 | ## Test File Naming
13 | - Use `.test.tsx` for component tests
14 | - Use `.test.ts` for utility/hook tests
15 | - Name tests after the component/module being tested (e.g., `Button.test.tsx`)
16 | - Store test files in `src/__tests__/` directory
17 |
18 | ## Component Testing Standards
19 | - Test component rendering with default props
20 | - Test all prop variations and combinations
21 | - Test user interactions (press, change, etc.)
22 | - Test accessibility features (testID, accessibility labels)
23 | - Test theme integration and styling variants
24 |
25 | ## Testing Patterns
26 | ```typescript
27 | import React from 'react'
28 | import {render, fireEvent} from '@testing-library/react-native'
29 | import {Button} from '../components/Button'
30 | import {BaseProvider} from '../core/BaseProvider'
31 |
32 | const renderWithProvider = (component: React.ReactElement) => {
33 | return render(
34 |
35 | {component}
36 |
37 | )
38 | }
39 |
40 | describe('Button Component', () => {
41 | it('renders correctly with default props', () => {
42 | const {getByText} = renderWithProvider()
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 |
49 |
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 |
--------------------------------------------------------------------------------