├── .editorconfig
├── .gitattributes
├── .github
├── actions
│ └── setup
│ │ └── action.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .watchmanconfig
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── babel.config.js
├── example
├── App.js
├── app.json
├── assets
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── icon.png
│ └── splash.png
├── babel.config.js
├── metro.config.js
├── package.json
├── src
│ ├── App.tsx
│ ├── HooksExample.tsx
│ ├── ReStyleSheetExample.tsx
│ ├── responsiveBox.tsx
│ └── theme.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
├── lefthook.yml
├── package.json
├── scripts
└── bootstrap.js
├── src
├── ReStyleSheet.ts
├── __tests__
│ └── index.test.tsx
├── createStore.ts
├── createStyleSheet.ts
├── hooks
│ └── useMediaQuery.ts
├── index.tsx
├── mediaQuery.ts
├── multiKeyStore.ts
├── types.ts
├── utill
│ ├── addDynamicValue.ts
│ ├── getDeviceType.ts
│ ├── initializeBreakpoints.ts
│ ├── isDynamicValue.ts
│ └── shallowEqual.ts
└── validations
│ └── checkProviderError.ts
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.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 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 | # specific for windows script files
3 | *.bat text eol=crlf
--------------------------------------------------------------------------------
/.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: Setup Node.js
8 | uses: actions/setup-node@v3
9 | with:
10 | node-version-file: .nvmrc
11 |
12 | - name: Cache dependencies
13 | id: yarn-cache
14 | uses: actions/cache@v3
15 | with:
16 | path: |
17 | **/node_modules
18 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
19 | restore-keys: |
20 | ${{ runner.os }}-yarn-
21 |
22 | - name: Install dependencies
23 | if: steps.yarn-cache.outputs.cache-hit != 'true'
24 | run: |
25 | yarn install --cwd example --frozen-lockfile
26 | yarn install --frozen-lockfile
27 | shell: bash
28 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | lint:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 |
17 | - name: Setup
18 | uses: ./.github/actions/setup
19 |
20 | - name: Lint files
21 | run: yarn lint
22 |
23 | - name: Typecheck files
24 | run: yarn typecheck
25 |
26 | test:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - name: Checkout
30 | uses: actions/checkout@v3
31 |
32 | - name: Setup
33 | uses: ./.github/actions/setup
34 |
35 | - name: Run unit tests
36 | run: yarn test --maxWorkers=2 --coverage
37 |
38 | build:
39 | runs-on: ubuntu-latest
40 | steps:
41 | - name: Checkout
42 | uses: actions/checkout@v3
43 |
44 | - name: Setup
45 | uses: ./.github/actions/setup
46 |
47 | - name: Build package
48 | run: yarn prepack
49 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our
7 | community a harassment-free experience for everyone, regardless of age, body
8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
9 | identity and expression, level of experience, education, socio-economic status,
10 | nationality, personal appearance, race, caste, color, religion, or sexual
11 | identity and orientation.
12 |
13 | We pledge to act and interact in ways that contribute to an open, welcoming,
14 | diverse, inclusive, and healthy community.
15 |
16 | ## Our Standards
17 |
18 | Examples of behavior that contributes to a positive environment for our
19 | community include:
20 |
21 | * Demonstrating empathy and kindness toward other people
22 | * Being respectful of differing opinions, viewpoints, and experiences
23 | * Giving and gracefully accepting constructive feedback
24 | * Accepting responsibility and apologizing to those affected by our mistakes,
25 | and learning from the experience
26 | * Focusing on what is best not just for us as individuals, but for the overall
27 | community
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | * The use of sexualized language or imagery, and sexual attention or advances of
32 | any kind
33 | * Trolling, insulting or derogatory comments, and personal or political attacks
34 | * Public or private harassment
35 | * Publishing others' private information, such as a physical or email address,
36 | without their explicit permission
37 | * Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions that are
49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
50 | decisions when appropriate.
51 |
52 | ## Scope
53 |
54 | This Code of Conduct applies within all community spaces, and also applies when
55 | an individual is officially representing the community in public spaces.
56 | Examples of representing our community include using an official e-mail address,
57 | posting via an official social media account, or acting as an appointed
58 | representative at an online or offline event.
59 |
60 | ## Enforcement
61 |
62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
63 | reported to the community leaders responsible for enforcement at
64 | [INSERT CONTACT METHOD].
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series of
87 | actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or permanent
94 | ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.1, available at
120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121 |
122 | Community Impact Guidelines were inspired by
123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127 | [https://www.contributor-covenant.org/translations][translations].
128 |
129 | [homepage]: https://www.contributor-covenant.org
130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131 | [Mozilla CoC]: https://github.com/mozilla/diversity
132 | [FAQ]: https://www.contributor-covenant.org/faq
133 | [translations]: https://www.contributor-covenant.org/translations
134 |
--------------------------------------------------------------------------------
/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 | To run the example app on Web:
38 |
39 | ```sh
40 | yarn example web
41 | ```
42 |
43 | Make sure your code passes TypeScript and ESLint. Run the following to verify:
44 |
45 | ```sh
46 | yarn typecheck
47 | yarn lint
48 | ```
49 |
50 | To fix formatting errors, run the following:
51 |
52 | ```sh
53 | yarn lint --fix
54 | ```
55 |
56 | Remember to add tests for your change if possible. Run the unit tests by:
57 |
58 | ```sh
59 | yarn test
60 | ```
61 |
62 |
63 | ### Commit message convention
64 |
65 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages:
66 |
67 | - `fix`: bug fixes, e.g. fix crash due to deprecated method.
68 | - `feat`: new features, e.g. add new method to the module.
69 | - `refactor`: code refactor, e.g. migrate from class components to hooks.
70 | - `docs`: changes into documentation, e.g. add usage example for the module..
71 | - `test`: adding or updating tests, e.g. add integration tests using detox.
72 | - `chore`: tooling changes, e.g. change CI config.
73 |
74 | Our pre-commit hooks verify that your commit message matches this format when committing.
75 |
76 | ### Linting and tests
77 |
78 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/)
79 |
80 | 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.
81 |
82 | Our pre-commit hooks verify that the linter and tests pass when committing.
83 |
84 | ### Publishing to npm
85 |
86 | 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.
87 |
88 | To publish new versions, run the following:
89 |
90 | ```sh
91 | yarn release
92 | ```
93 |
94 | ### Scripts
95 |
96 | The `package.json` file contains various scripts for common tasks:
97 |
98 | - `yarn bootstrap`: setup project by installing all dependencies and pods.
99 | - `yarn typecheck`: type-check files with TypeScript.
100 | - `yarn lint`: lint files with ESLint.
101 | - `yarn test`: run unit tests with Jest.
102 | - `yarn example start`: start the Metro server for the example app.
103 | - `yarn example android`: run the example app on Android.
104 | - `yarn example ios`: run the example app on iOS.
105 |
106 | ### Sending a pull request
107 |
108 | > **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).
109 |
110 | When you're sending a pull request:
111 |
112 | - Prefer small pull requests focused on one change.
113 | - Verify that linters and tests are passing.
114 | - Review the documentation to make sure it looks good.
115 | - Follow the pull request template when opening a pull request.
116 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.
117 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 redwanul islam
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # react-native-restyle-sheet
4 |
5 | [](https://www.npmjs.com/package/react-native-restyle-sheet)
6 | 
7 | 
8 |
9 | restyle-sheet provides flexible way to define Theming, Dynamic styles & Media Query support for React Native
10 |
11 | ## Features
12 |
13 | - Media Query Support
14 | - Easy way to define Dynamic Style & Theming (No inline style)
15 | - useMediaQuery hooks
16 | - Fully typed with TypeScript
17 |
18 | https://user-images.githubusercontent.com/22383818/226711571-9a310e32-7e3d-499c-a04a-898028be49ec.mp4
19 |
20 | ## PlayGround
21 |
22 | Check out the codeSandbox playGround [link](https://codesandbox.io/s/react-native-restyle-sheet-example-vh19ce)
23 |
24 | ## Install
25 |
26 | ```sh
27 | yarn add react-native-restyle-sheet
28 | # or
29 | npm install --save react-native-restyle-sheet
30 | ```
31 |
32 | ## Usage
33 |
34 | 1. First We need to initialize Style Sheet
35 |
36 | ```js
37 | // theme.js
38 |
39 | import { createStyleSheet } from 'react-native-restyle-sheet';
40 |
41 | export const lightTheme = {
42 | themeId: 'light',
43 | colors: {
44 | main: 'green',
45 | primary: '#00235B',
46 | secondary: '#E21818',
47 | tertiary: '#FFDD83',
48 | quatenary: '#98DFD6',
49 | },
50 | };
51 |
52 | const breakpoints = {
53 | small: 0,
54 | medium: 500,
55 | large: 800,
56 | // you can define custom device size also
57 | };
58 |
59 | export const { ReStyleSheet, changeTheme } = createStyleSheet({
60 | theme: lightTheme,
61 | breakpoints,
62 | });
63 | ```
64 |
65 | 2. Then use ReStyleSheet like this anywhere in your app with theme & breakpoints:
66 |
67 | ```js
68 | import React from 'react';
69 | import { View, Text } from 'react-native';
70 | import { ReStyleSheet } from './theme';
71 |
72 | const useStyle = ReStyleSheet(({ theme, breakpoints }) => ({
73 | header: {
74 | fontWeight: 'bold',
75 | fontSize: 20,
76 | color: theme.colors.tertiary,
77 | [breakpoints.only('medium')]: {
78 | color: theme.colors.primary,
79 | },
80 | },
81 | }));
82 |
83 | const Demo = () => {
84 | const { styles } = useStyle();
85 | return (
86 | <>
87 |
88 | Hello World
89 |
90 | >
91 | );
92 | };
93 | ```
94 |
95 | ## Define dynamic style
96 |
97 | we can pass any dynamic values in useStyle hooks returned by ReStyleSheet
98 |
99 | ```js
100 | const useStyle = ReStyleSheet(() => ({
101 | header: {
102 | fontSize: 20,
103 | color: (props) => props?.activeColor,
104 | },
105 | }));
106 |
107 | const Demo = () => {
108 | const [color, setColor] = React.useState('red');
109 | const { styles } = useStyle({ activeColor: color });
110 |
111 | const toggleColor = () => {
112 | setColor(color === 'red' ? 'green' : 'red');
113 | };
114 |
115 | return (
116 |
117 | Hello World
118 |
119 | Toggle Color
120 |
121 |
122 | );
123 | };
124 | ```
125 |
126 | ## Change Theme
127 |
128 | 1. To change the Theme we can use **`changeTheme()`** method anywhere in our app
129 |
130 | ```js
131 | // theme.js
132 |
133 | import { createStyleSheet } from 'react-native-restyle-sheet';
134 |
135 | const lightTheme = {
136 | themeId: 'lightTheme',
137 | colors: {
138 | ...
139 | },
140 | };
141 |
142 | const darkTheme = {
143 | themeId: 'darkTheme',
144 | colors: {
145 | ...
146 | },
147 | };
148 |
149 | const breakpoints = {
150 | ...
151 | };
152 |
153 | export const { ReStyleSheet, changeTheme } = createStyleSheet({
154 | theme: lightTheme,
155 | breakpoints,
156 | });
157 |
158 | export const toggleTheme = () => {
159 | changeTheme((themId) => (themId === 'darkTheme' ? lightTheme : darkTheme));
160 | };
161 |
162 | ```
163 |
164 |
165 | ## Override Media Query
166 |
167 | If multiple breakpoints are applicable similar to css rule the bottom breakpoint will get priority. \
168 | For example we are in a **"small"** device so here **breakpoints.only('small')** & **breakpoints.down('medium')** both are applicable
169 |
170 | ```js
171 | // Example 1
172 | const useStyle = ReStyleSheet(({ breakpoints }) => ({
173 | header: {
174 | backgroundColor: 'black',
175 | [breakpoints.only('large')]: {
176 | backgroundColor: 'red',
177 | },
178 | [breakpoints.only('small')]: {
179 | backgroundColor: 'green',
180 | },
181 | // in "small" device header background will be "blue"
182 | [breakpoints.down('medium')]: {
183 | backgroundColor: 'blue',
184 | },
185 | },
186 | }));
187 |
188 | // Example 2
189 | const useStyle = ReStyleSheet(({ breakpoints }) => ({
190 | header: {
191 | backgroundColor: 'black',
192 | [breakpoints.only('large')]: {
193 | backgroundColor: 'red',
194 | },
195 | [breakpoints.down('medium')]: {
196 | backgroundColor: 'blue',
197 | },
198 | // in "small" device header background will be "green"
199 | [breakpoints.only('small')]: {
200 | backgroundColor: 'green',
201 | },
202 | },
203 | }));
204 | ```
205 |
206 | ## breakpoints API
207 |
208 |
209 | | Methods | Arguments |Required | Returns |
210 | | ------------- |:-------------:| -------------| -------------|
211 | | **`breakpoints.up(size)`** | size (string) |true |MediaQuery key (string) |
212 | | **`breakpoints.down(size)`** | size (string) |true | MediaQuery key (string) |
213 | | **`breakpoints.only(size)`** | size (string) |true | MediaQuery key (string) |
214 |
215 |
216 |
217 | ## useMediaQuery Hooks
218 |
219 | It returns the device size based on breakpoints
220 |
221 | ```js
222 | import { useMediaQuery } from 'react-native-restyle-sheet';
223 |
224 | const Demo = () => {
225 | const devicetype = useMediaQuery()
226 | console.log(devicetype) // For Example: small
227 |
228 | ....
229 | };
230 | ```
231 |
232 | It also accepts custom breakpoints and returns boolean value
233 |
234 | ```js
235 | const isTablet = useMediaQuery({ min: 400, max: 800 });
236 | const isExtraLarge = useMediaQuery({ min: 1200 });
237 | const isExtraSmall = useMediaQuery({ Max: 576 });
238 |
239 | console.log(isTablet); //false
240 | ```
241 |
271 |
272 | ## License
273 |
274 | MIT
275 |
276 | ---
277 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/example/App.js:
--------------------------------------------------------------------------------
1 | export { default } from './src/App';
2 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "example",
4 | "slug": "example",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "updates": {
15 | "fallbackToCacheTimeout": 0
16 | },
17 | "assetBundlePatterns": [
18 | "**/*"
19 | ],
20 | "ios": {
21 | "supportsTablet": true
22 | },
23 | "android": {
24 | "adaptiveIcon": {
25 | "foregroundImage": "./assets/adaptive-icon.png",
26 | "backgroundColor": "#FFFFFF"
27 | }
28 | },
29 | "web": {
30 | "favicon": "./assets/favicon.png"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/example/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redwanul10/react-native-restyle-sheet/6ebae0fae0096e9ba517b5defa82d11f949efc89/example/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/example/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redwanul10/react-native-restyle-sheet/6ebae0fae0096e9ba517b5defa82d11f949efc89/example/assets/favicon.png
--------------------------------------------------------------------------------
/example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redwanul10/react-native-restyle-sheet/6ebae0fae0096e9ba517b5defa82d11f949efc89/example/assets/icon.png
--------------------------------------------------------------------------------
/example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redwanul10/react-native-restyle-sheet/6ebae0fae0096e9ba517b5defa82d11f949efc89/example/assets/splash.png
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const pak = require('../package.json');
3 |
4 | module.exports = function (api) {
5 | api.cache(true);
6 |
7 | return {
8 | presets: ['babel-preset-expo'],
9 | plugins: [
10 | [
11 | 'module-resolver',
12 | {
13 | extensions: ['.tsx', '.ts', '.js', '.json'],
14 | alias: {
15 | // For development, we want to alias the library to the source
16 | [pak.name]: path.join(__dirname, '..', pak.source),
17 | },
18 | },
19 | ],
20 | ],
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const escape = require('escape-string-regexp');
3 | const { getDefaultConfig } = require('@expo/metro-config');
4 | const exclusionList = require('metro-config/src/defaults/exclusionList');
5 | const pak = require('../package.json');
6 |
7 | const root = path.resolve(__dirname, '..');
8 |
9 | const modules = Object.keys({
10 | ...pak.peerDependencies,
11 | });
12 |
13 | const defaultConfig = getDefaultConfig(__dirname);
14 |
15 | module.exports = {
16 | ...defaultConfig,
17 |
18 | projectRoot: __dirname,
19 | watchFolders: [root],
20 |
21 | // We need to make sure that only one version is loaded for peerDependencies
22 | // So we block them at the root, and alias them to the versions in example's node_modules
23 | resolver: {
24 | ...defaultConfig.resolver,
25 |
26 | blacklistRE: exclusionList(
27 | modules.map(
28 | (m) =>
29 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`)
30 | )
31 | ),
32 |
33 | extraNodeModules: modules.reduce((acc, name) => {
34 | acc[name] = path.join(__dirname, 'node_modules', name);
35 | return acc;
36 | }, {}),
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@types/react": "~18.0.24",
13 | "@types/react-native": "~0.70.6",
14 | "expo": "~47.0.12",
15 | "expo-status-bar": "~1.4.2",
16 | "react": "18.1.0",
17 | "react-dom": "18.1.0",
18 | "react-native": "0.70.5",
19 | "react-native-web": "~0.18.9",
20 | "typescript": "^4.6.3"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "^7.12.9",
24 | "@expo/webpack-config": "^0.17.2",
25 | "babel-loader": "^8.1.0",
26 | "babel-plugin-module-resolver": "^4.1.0"
27 | },
28 | "private": true
29 | }
30 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { SafeAreaView } from 'react-native';
4 | import HooksExample from './HooksExample';
5 | import ResponsiveBox from './responsiveBox';
6 | import ReStyleSheetExample from './ReStyleSheetExample';
7 |
8 | export default function App() {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/example/src/HooksExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 | import { useMediaQuery } from 'react-native-restyle-sheet';
4 | import { ReStyleSheet } from './theme';
5 |
6 | const useStyle = ReStyleSheet(() => ({
7 | container: {
8 | backgroundColor: 'green',
9 | width: 250,
10 | height: 100,
11 | marginTop: 30,
12 | justifyContent: 'center',
13 | alignItems: 'center',
14 | alignSelf: 'center',
15 | borderRadius: 20,
16 | },
17 | paragraph: {
18 | fontWeight: 'bold',
19 | fontSize: 20,
20 | color: 'white',
21 | },
22 | }));
23 |
24 | export default function HooksExample() {
25 | const { styles } = useStyle();
26 | const size = useMediaQuery();
27 | return (
28 |
29 | from Hooks {size}
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/example/src/ReStyleSheetExample.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, Text } from 'react-native';
3 | import { ReStyleSheet, toggleTheme } from './theme';
4 |
5 | const useStyle = ReStyleSheet(({ theme, breakpoints }) => ({
6 | title: {
7 | fontWeight: 'bold',
8 | fontSize: 25,
9 | marginVertical: 16,
10 | backgroundColor: theme.colors.primary,
11 | color: 'white',
12 | textAlign: 'center',
13 | [breakpoints.only('large')]: {
14 | backgroundColor: 'green',
15 | },
16 | },
17 | subtitle: {
18 | fontWeight: 'bold',
19 | fontSize: 25,
20 | textAlign: 'center',
21 | color: (props) => props.activeColor,
22 | },
23 | }));
24 |
25 | export default function ReStyleSheetExample() {
26 | const [color, setColor] = React.useState('#DF2E38');
27 | const { styles, deviceType } = useStyle({ activeColor: color }, true);
28 |
29 | const isLargeDevice = deviceType === 'large';
30 |
31 | const toggleColor = () => {
32 | setColor(color === '#DF2E38' ? 'black' : '#DF2E38');
33 | };
34 |
35 | return (
36 | <>
37 | React Native
38 | re-size codesandbox window
39 |
40 | Device size {deviceType}
41 | {isLargeDevice && (
42 | only visible for large device
43 | )}
44 |
45 |
46 |
47 | >
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/example/src/responsiveBox.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 | import { ReStyleSheet } from './theme';
4 |
5 | export default function ResponsiveBox() {
6 | const { styles } = useStyles();
7 | return (
8 | <>
9 |
10 |
11 | COL 1
12 |
13 |
14 | COL 2
15 |
16 |
17 | COL 3
18 |
19 |
20 | COL 4
21 |
22 |
23 | >
24 | );
25 | }
26 |
27 | const useStyles = ReStyleSheet(({ breakpoints, theme }) => ({
28 | col: {
29 | width: '100%',
30 | backgroundColor: 'red',
31 | paddingVertical: 20,
32 | marginBottom: 5,
33 | [breakpoints.only('medium')]: {
34 | width: '50%',
35 | },
36 | [breakpoints.only('large')]: {
37 | width: '25%',
38 | },
39 | },
40 |
41 | container: {
42 | flexDirection: 'row',
43 | flexWrap: 'wrap',
44 | marginTop: 10,
45 | },
46 |
47 | boxText: {
48 | color: 'white',
49 | fontWeight: 'bold',
50 | textAlign: 'center',
51 | },
52 | col1: {
53 | backgroundColor: theme.colors.primary,
54 | },
55 | col2: {
56 | backgroundColor: theme.colors.secondary,
57 | },
58 | col3: {
59 | backgroundColor: theme.colors.tertiary,
60 | },
61 | col4: {
62 | backgroundColor: theme.colors.quatenary,
63 | },
64 | }));
65 |
--------------------------------------------------------------------------------
/example/src/theme.ts:
--------------------------------------------------------------------------------
1 | import { createStyleSheet } from 'react-native-restyle-sheet';
2 |
3 | export const lightTheme = {
4 | themeId: 'light',
5 | colors: {
6 | main: 'green',
7 | primary: '#00235B',
8 | secondary: '#E21818',
9 | tertiary: '#FFDD83',
10 | quatenary: '#98DFD6',
11 | },
12 | };
13 |
14 | const breakpoints = {
15 | small: 0,
16 | medium: 500,
17 | large: 800,
18 | };
19 |
20 | export const { ReStyleSheet, changeTheme } = createStyleSheet({
21 | theme: lightTheme,
22 | breakpoints,
23 | });
24 |
25 | const darkTheme = {
26 | themeId: 'dark',
27 | colors: {
28 | main: 'red',
29 | primary: '#19A7CE',
30 | secondary: '#F6F1F1',
31 | tertiary: '#146C94',
32 | quatenary: '#000000',
33 | },
34 | };
35 | export const toggleTheme = () => {
36 | changeTheme((themId) => (themId === 'dark' ? lightTheme : darkTheme));
37 | };
38 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig",
3 | "compilerOptions": {
4 | // Avoid expo-cli auto-generating a tsconfig
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config');
3 | const { resolver } = require('./metro.config');
4 |
5 | const root = path.resolve(__dirname, '..');
6 | const node_modules = path.join(__dirname, 'node_modules');
7 |
8 | module.exports = async function (env, argv) {
9 | const config = await createExpoWebpackConfigAsync(env, argv);
10 |
11 | config.module.rules.push({
12 | test: /\.(js|jsx|ts|tsx)$/,
13 | include: path.resolve(root, 'src'),
14 | use: 'babel-loader',
15 | });
16 |
17 | // We need to make sure that only one version is loaded for peerDependencies
18 | // So we alias them to the versions in example's node_modules
19 | Object.assign(config.resolve.alias, {
20 | ...resolver.extraNodeModules,
21 | 'react-native-web': path.join(node_modules, 'react-native-web'),
22 | });
23 |
24 | return config;
25 | };
26 |
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | pre-commit:
2 | parallel: true
3 | commands:
4 | lint:
5 | files: git diff --name-only @{push}
6 | glob: '*.{js,ts,jsx,tsx}'
7 | run: npx eslint {files}
8 | types:
9 | files: git diff --name-only @{push}
10 | glob: '*.{js,ts, jsx, tsx}'
11 | run: npx tsc --noEmit
12 | commit-msg:
13 | parallel: true
14 | commands:
15 | commitlint:
16 | run: npx commitlint --edit
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-restyle-sheet",
3 | "version": "0.3.0",
4 | "description": "restyle-sheet provides flexible way to define Theming, Dynamic styles & Media Query support 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",
9 | "source": "src/index",
10 | "files": [
11 | "src",
12 | "lib",
13 | "!**/__tests__",
14 | "!**/__fixtures__",
15 | "!**/__mocks__",
16 | "android",
17 | "ios",
18 | "cpp",
19 | "*.podspec",
20 | "!lib/typescript/example",
21 | "!ios/build",
22 | "!android/build",
23 | "!android/gradle",
24 | "!android/gradlew",
25 | "!android/gradlew.bat",
26 | "!android/local.properties",
27 | "!**/.*"
28 | ],
29 | "scripts": {
30 | "test": "jest",
31 | "typecheck": "tsc --noEmit",
32 | "lint": "eslint \"**/*.{js,ts,tsx}\"",
33 | "prepack": "bob build",
34 | "release": "release-it",
35 | "example": "yarn --cwd example",
36 | "bootstrap": "yarn example && yarn install"
37 | },
38 | "keywords": [
39 | "react-native",
40 | "react-native-media-query",
41 | "react-native-responsive",
42 | "react-native-restyle-sheet",
43 | "restyle-sheet"
44 | ],
45 | "repository": "https://github.com/redwanul10/restyle-sheet",
46 | "author": "redwanul islam (https://github.com/redwanul10)",
47 | "license": "MIT",
48 | "bugs": {
49 | "url": "https://github.com/redwanul10/restyle-sheet/issues"
50 | },
51 | "homepage": "https://github.com/redwanul10/restyle-sheet#readme",
52 | "publishConfig": {
53 | "registry": "https://registry.npmjs.org/"
54 | },
55 | "devDependencies": {
56 | "@commitlint/config-conventional": "^17.0.2",
57 | "@evilmartians/lefthook": "^1.2.2",
58 | "@react-native-community/eslint-config": "^3.0.2",
59 | "@release-it/conventional-changelog": "^5.0.0",
60 | "@types/jest": "^28.1.2",
61 | "@types/react": "~17.0.21",
62 | "@types/react-native": "0.70.0",
63 | "@types/use-sync-external-store": "^0.0.3",
64 | "commitlint": "^17.0.2",
65 | "del-cli": "^5.0.0",
66 | "eslint": "^8.4.1",
67 | "eslint-config-prettier": "^8.5.0",
68 | "eslint-plugin-prettier": "^4.0.0",
69 | "jest": "^28.1.1",
70 | "pod-install": "^0.1.0",
71 | "prettier": "^2.0.5",
72 | "react": "18.1.0",
73 | "react-native": "0.70.5",
74 | "react-native-builder-bob": "^0.20.3",
75 | "release-it": "^15.0.0",
76 | "typescript": "^4.9.5"
77 | },
78 | "resolutions": {
79 | "@types/react": "17.0.21"
80 | },
81 | "peerDependencies": {
82 | "react": "*",
83 | "react-native": "*"
84 | },
85 | "dependencies": {
86 | "use-sync-external-store": "^1.0.0"
87 | },
88 | "engines": {
89 | "node": ">= 16.0.0"
90 | },
91 | "packageManager": "^yarn@1.22.15",
92 | "jest": {
93 | "preset": "react-native",
94 | "modulePathIgnorePatterns": [
95 | "/example/node_modules",
96 | "/lib/"
97 | ]
98 | },
99 | "commitlint": {
100 | "extends": [
101 | "@commitlint/config-conventional"
102 | ]
103 | },
104 | "release-it": {
105 | "git": {
106 | "commitMessage": "chore: release ${version}",
107 | "tagName": "v${version}"
108 | },
109 | "npm": {
110 | "publish": true
111 | },
112 | "github": {
113 | "release": true
114 | },
115 | "plugins": {
116 | "@release-it/conventional-changelog": {
117 | "preset": "angular"
118 | }
119 | }
120 | },
121 | "eslintConfig": {
122 | "root": true,
123 | "extends": [
124 | "@react-native-community",
125 | "prettier"
126 | ],
127 | "rules": {
128 | "prettier/prettier": [
129 | "error",
130 | {
131 | "quoteProps": "consistent",
132 | "singleQuote": true,
133 | "tabWidth": 2,
134 | "trailingComma": "es5",
135 | "useTabs": false
136 | }
137 | ]
138 | }
139 | },
140 | "eslintIgnore": [
141 | "node_modules/",
142 | "lib/"
143 | ],
144 | "prettier": {
145 | "quoteProps": "consistent",
146 | "singleQuote": true,
147 | "tabWidth": 2,
148 | "trailingComma": "es5",
149 | "useTabs": false
150 | },
151 | "react-native-builder-bob": {
152 | "source": "src",
153 | "output": "lib",
154 | "targets": [
155 | "commonjs",
156 | "module",
157 | "typescript"
158 | ]
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/ReStyleSheet.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import store from './createStore';
4 | import multiKeyStore from './multiKeyStore';
5 | import {
6 | isMediaQuery as detectMediaQuery,
7 | processMediaQuery,
8 | } from './mediaQuery';
9 | import type {
10 | DynamicValues,
11 | BreakPoints,
12 | Config,
13 | breakpoint,
14 | StyleProps,
15 | indexType,
16 | BreakPointMethods,
17 | StyleObject,
18 | } from './types';
19 | import shallowEqual from './utill/shallowEqual';
20 | import { isDynamicValue } from './utill/isDynamicValue';
21 | import { addDynamicValue } from './utill/addDynamicValue';
22 |
23 | const processStyles = (
24 | styles: StyleObject,
25 | width: number,
26 | breakpoints: BreakPoints
27 | ): { dynamicValues: DynamicValues[]; rawStyle: StyleObject } => {
28 | let dynamicValues = {}; //[];
29 | let rawStyle: StyleObject = {};
30 |
31 | Object.keys(styles).forEach((cssID) => {
32 | const subStyle = styles[cssID];
33 | let foundMediaQuery = false;
34 |
35 | Object.keys(subStyle).forEach((cssKey) => {
36 | const cssValue = subStyle[cssKey];
37 | const isMediaQuery = detectMediaQuery(cssKey);
38 |
39 | if (isDynamicValue(cssValue)) {
40 | dynamicValues = addDynamicValue(dynamicValues, cssID, cssKey, cssValue);
41 | }
42 |
43 | if (!isMediaQuery && typeof cssValue === 'object') {
44 | throw new Error(`Invalid style found Cant't use object in '${cssKey}'`);
45 | }
46 |
47 | if (!isMediaQuery && foundMediaQuery) {
48 | // throw new Error(`invalid shape`);
49 |
50 | console.warn(
51 | `Please define "${cssKey}" above media-query styles because that can override your Media-Query styles`
52 | );
53 | }
54 |
55 | if (isMediaQuery) {
56 | const [updatedDynamicValues, updatedRawStyle] = processMediaQuery(
57 | cssKey,
58 | width,
59 | breakpoints,
60 | cssValue,
61 | dynamicValues,
62 | cssID,
63 | rawStyle
64 | );
65 |
66 | rawStyle = updatedRawStyle;
67 | dynamicValues = updatedDynamicValues;
68 |
69 | if (!foundMediaQuery) foundMediaQuery = true;
70 | }
71 |
72 | if (!isMediaQuery) {
73 | if (!rawStyle[cssID]) {
74 | rawStyle[cssID] = {
75 | [cssKey]: subStyle[cssKey],
76 | };
77 | } else {
78 | rawStyle[cssID] = {
79 | ...rawStyle[cssID],
80 | [cssKey]: subStyle[cssKey],
81 | };
82 | }
83 | }
84 | });
85 | });
86 |
87 | // dynamicValues = Object.values(dynamicValues);
88 |
89 | return { dynamicValues: Object.values(dynamicValues), rawStyle };
90 | };
91 |
92 | const applyDynamicValues = (styleConfig: Config, props: object) => {
93 | const { dynamicValues, styles } = styleConfig;
94 | const copyStyle: StyleObject = { ...styles };
95 |
96 | for (let index = 0; index < dynamicValues.length; index++) {
97 | const element = dynamicValues[index] as DynamicValues;
98 | const [selector, cssKey] = element?.key.split('.');
99 |
100 | copyStyle[selector as indexType] = [
101 | copyStyle[selector as indexType],
102 | { [cssKey as indexType]: element?.dynamic(props) },
103 | ];
104 | }
105 |
106 | return copyStyle;
107 | };
108 |
109 | const createObserable = (
110 | data: { theme: object; breakPointMethods: BreakPointMethods },
111 | onAccess: (property: string) => void
112 | ) => {
113 | let options = {};
114 |
115 | Object.defineProperty(options, 'theme', {
116 | get() {
117 | onAccess('theme');
118 | return data.theme;
119 | },
120 | enumerable: true,
121 | configurable: true,
122 | });
123 |
124 | Object.defineProperty(options, 'breakpoints', {
125 | get() {
126 | onAccess('breakpoints');
127 | return data.breakPointMethods;
128 | },
129 | enumerable: true,
130 | configurable: true,
131 | });
132 | return options;
133 | };
134 |
135 | const init = (
136 | styleFunc: (options: any) => any,
137 | data: any,
138 | width: number,
139 | accessed: any
140 | ): Config => {
141 | let options = createObserable(data, (property) => {
142 | if (!accessed.current[property]) accessed.current[property] = true;
143 | });
144 |
145 | const styles = styleFunc(options);
146 |
147 | // ======= STEP 1 =========
148 | // GET ALL THE APPLICABLE RAW STYLE & DYNAMIC VALUE
149 | // FROM STYLES BASED ON MEDIA QUERIES
150 | const { rawStyle, dynamicValues } = processStyles(
151 | styles,
152 | width,
153 | data.breakpoints
154 | );
155 |
156 | // ======= STEP 2 =========
157 | // CREATE STYLE SHEET WITH RAW STYLE
158 | var cloneRawstyle = StyleSheet.create(rawStyle);
159 |
160 | const config = {
161 | dynamicValues,
162 | styles: cloneRawstyle,
163 | };
164 |
165 | return config;
166 | };
167 |
168 | const customShallowEqual = (
169 | prev: any,
170 | current: any,
171 | accessed: any,
172 | size: boolean | undefined
173 | ) => {
174 | let theme = accessed.current.theme;
175 | let breakpoint = accessed.current.breakpoints || size;
176 |
177 | if (!theme && !breakpoint) {
178 | return true;
179 | }
180 |
181 | if (theme && !breakpoint) {
182 | let oldProps = {
183 | theme: prev.theme,
184 | breakpoints: prev.defaultBreakPoints,
185 | };
186 |
187 | let newProps = {
188 | theme: current.theme,
189 | breakpoints: current.defaultBreakPoints,
190 | };
191 |
192 | return shallowEqual(oldProps, newProps);
193 | }
194 |
195 | if (!theme && breakpoint) {
196 | let oldProps = {
197 | device: prev.device,
198 | breakpoints: prev.defaultBreakPoints,
199 | };
200 |
201 | let newProps = {
202 | device: current.device,
203 | breakpoints: current.defaultBreakPoints,
204 | };
205 | return shallowEqual(oldProps, newProps);
206 | }
207 | return shallowEqual(prev, current);
208 | };
209 |
210 | const ReStyleSheet = ) => StyleProps>(
211 | styleFunc: T
212 | ): ((
213 | props?: any,
214 | size?: boolean | undefined
215 | ) => {
216 | styles: { [key in keyof ReturnType]?: Object };
217 | deviceType: String;
218 | }) => {
219 | let isHooksReady = false;
220 | const useStyle = (props: any = {}, size: boolean | undefined) => {
221 | let accessed = useRef(
222 | 'pending'
223 | );
224 |
225 | const data = store.useStore(
226 | (state: object) => state,
227 | (prev: object, current: object) =>
228 | customShallowEqual(prev, current, accessed, size)
229 | ) as any;
230 |
231 | let activeMediaQuery = useRef('');
232 | let styleConfig = useRef({ dynamicValues: [], styles: {} });
233 | let CACHE = useRef(new multiKeyStore());
234 |
235 | let prevProps = useRef('');
236 | let prevThemeId = useRef('');
237 | let finalStyle = useRef({});
238 | const { width, device } = data;
239 |
240 | useEffect(() => {
241 | isHooksReady = true;
242 | }, []);
243 |
244 | const cacheStyleConfig = CACHE.current.get(data.theme.themeId, device);
245 |
246 | if (accessed.current === 'pending') accessed.current = {};
247 |
248 | if (!isHooksReady || !cacheStyleConfig) {
249 | const config = init(styleFunc, data, width, accessed);
250 | CACHE.current.set(data.theme.themeId, device, config);
251 | styleConfig.current = config;
252 | } else {
253 | styleConfig.current = cacheStyleConfig;
254 | }
255 |
256 | const isPropsOrThemeOrDeviceTypeChanged =
257 | prevProps.current !== JSON.stringify(props) ||
258 | prevThemeId.current !== data.theme.themeId ||
259 | activeMediaQuery.current !== device;
260 |
261 | if (!isHooksReady || isPropsOrThemeOrDeviceTypeChanged) {
262 | finalStyle.current = applyDynamicValues(styleConfig.current, props);
263 | prevProps.current = JSON.stringify(props);
264 | prevThemeId.current = data.theme.themeId;
265 | activeMediaQuery.current = device;
266 | }
267 |
268 | return { styles: finalStyle.current, deviceType: device };
269 | };
270 |
271 | return useStyle;
272 | };
273 |
274 | export default ReStyleSheet;
275 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | it.todo('write a test');
2 |
--------------------------------------------------------------------------------
/src/createStore.ts:
--------------------------------------------------------------------------------
1 | import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector';
2 | import shallowEqual from './utill/shallowEqual';
3 |
4 | const initStore = (initialState: object) => {
5 | let state = initialState;
6 | const listeners = new Set();
7 |
8 | const getState = () => {
9 | return state;
10 | };
11 |
12 | const setState = (stateOrCb: any) => {
13 | state = typeof stateOrCb === 'function' ? stateOrCb(state) : stateOrCb;
14 | listeners.forEach((listener: any) => listener());
15 | };
16 |
17 | const subscribe = (listener: () => void) => {
18 | listeners.add(listener);
19 | return () => listeners.delete(listener);
20 | };
21 |
22 | return { state, setState, getState, subscribe };
23 | };
24 |
25 | const createStore = (initialState: any) => {
26 | const store = initStore(initialState);
27 | const useStore = (selector?: any, shEqueal?: any) => {
28 | return useSyncExternalStoreWithSelector(
29 | store.subscribe,
30 | store.getState,
31 | store.getState,
32 | selector,
33 | shEqueal || shallowEqual
34 | );
35 | };
36 | return { useStore, store };
37 | };
38 |
39 | const store = createStore(null);
40 |
41 | export default store;
42 |
--------------------------------------------------------------------------------
/src/createStyleSheet.ts:
--------------------------------------------------------------------------------
1 | import type { breakpoint, indexType, StyleProps } from './types';
2 | import ReStyleSheet from './ReStyleSheet';
3 | import { Dimensions } from 'react-native';
4 | import { getDevicetype } from './utill/getDeviceType';
5 | import store from './createStore';
6 | import initializeBreakpoints from './utill/initializeBreakpoints';
7 | import { breakPointMethods } from './mediaQuery';
8 |
9 | const ScreenWidth = Dimensions.get('window').width;
10 |
11 | type ReStyleSheet = <
12 | T extends (theme: breakpoint) => StyleProps
13 | >(
14 | styleFunc: T
15 | ) => (
16 | props?: any,
17 | size?: boolean | undefined
18 | ) => {
19 | styles: { [key in keyof ReturnType]?: Object };
20 | deviceType: String;
21 | };
22 |
23 | let activeMediaQuery = '';
24 |
25 | export const createStyleSheet = <
26 | T extends {
27 | breakpoints: { [key: indexType]: number };
28 | theme: { [key: indexType]: any };
29 | }
30 | >({
31 | breakpoints,
32 | theme,
33 | }: T): {
34 | changeTheme: (cb: (id: string) => object) => void;
35 | ReStyleSheet: ReStyleSheet;
36 | } => {
37 | if (__DEV__) {
38 | validateArg(theme, breakpoints);
39 | }
40 |
41 | /**
42 | * Event Listener
43 | */
44 | Dimensions.addEventListener('change', ({ window }) => {
45 | let device = getDevicetype(window.width, breakpoints);
46 | if (device !== activeMediaQuery) {
47 | //
48 | activeMediaQuery = device;
49 | store.store.setState((state: object) => ({
50 | ...state,
51 | width: window.width,
52 | device,
53 | }));
54 | }
55 | });
56 |
57 | // Set Initial State
58 | store.store.setState({
59 | device: getDevicetype(ScreenWidth, breakpoints),
60 | width: ScreenWidth,
61 | defaultBreakPoints: breakpoints,
62 | breakpoints: {
63 | ...initializeBreakpoints(breakpoints),
64 | },
65 | breakPointMethods: {
66 | ...breakPointMethods,
67 | },
68 | theme: {
69 | // default Theme ID
70 | themeId: theme?.themeId || '@random',
71 | ...theme,
72 | },
73 | });
74 |
75 | const changeTheme = (themeCallback: (id: string) => object): void => {
76 | const currentStore = store.store.getState() as any;
77 | const newTheme = themeCallback(currentStore.theme.themeId);
78 | store.store.setState((state: object) => ({
79 | ...state,
80 | theme: {
81 | ...newTheme,
82 | },
83 | }));
84 | };
85 |
86 | return {
87 | ReStyleSheet,
88 | changeTheme,
89 | };
90 | };
91 |
92 | const validateArg = (theme: any, breakpoint: any) => {
93 | if (!theme && !breakpoint) {
94 | throw new Error(
95 | "'theme' & 'breakpoints' both are required for creating style sheet"
96 | );
97 | }
98 | if (!theme) {
99 | throw new Error("'theme' is required for creating style sheet");
100 | }
101 |
102 | if (!breakpoint) {
103 | throw new Error("'breakpoint' is required for creating style sheet");
104 | }
105 |
106 | if (Object.prototype.toString.call(theme) !== '[object Object]') {
107 | throw new Error("Invalid 'theme' type found ! 'theme' should be an object");
108 | }
109 |
110 | if (Object.prototype.toString.call(theme) !== '[object Object]') {
111 | throw new Error(
112 | "Invalid 'breakpoint' type found ! 'breakpoint' should be an object"
113 | );
114 | }
115 | };
116 |
--------------------------------------------------------------------------------
/src/hooks/useMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useEffect, useRef, useState } from 'react';
2 | import { Dimensions } from 'react-native';
3 | import checkProviderError from '../validations/checkProviderError';
4 |
5 | import store from '../createStore';
6 |
7 | import shallowEqual from '../utill/shallowEqual';
8 |
9 | export const ProviderContext = createContext({});
10 |
11 | const detectCustomMediaQuery = (
12 | width: number,
13 | min: number,
14 | max: number
15 | ): boolean => {
16 | if (width > min && width < max) {
17 | return true;
18 | }
19 | if (width > min && !max) {
20 | return true;
21 | }
22 | if (width < max && !min) {
23 | return true;
24 | }
25 | return false;
26 | };
27 |
28 | // type Props = ;
29 |
30 | const useMediaQuery = (props?: {
31 | min?: number;
32 | max?: number;
33 | }): string | boolean => {
34 | // const { device } = useContext(ProviderContext);
35 | const device: any = store.useStore(
36 | (state: any) => state?.device,
37 | (prev: any, current: any) => {
38 | if (props) return true;
39 | return shallowEqual(prev, current);
40 | }
41 | );
42 |
43 | if (__DEV__) {
44 | checkProviderError(device);
45 | }
46 |
47 | const listenerRef = useRef();
48 | const [isActive, setIsActive] = useState(false);
49 | const isActiveRef = useRef(false);
50 |
51 | useEffect(() => {
52 | if (props?.min || props?.max) {
53 | const width = Dimensions.get('window').width;
54 | let result = detectCustomMediaQuery(
55 | width,
56 | props?.min || 0,
57 | props.max || 0
58 | );
59 | setIsActive(result);
60 |
61 | if (listenerRef?.current?.remove) listenerRef.current?.remove();
62 | listenerRef.current = Dimensions.addEventListener(
63 | 'change',
64 | ({ window }) => {
65 | // eslint-disable-next-line @typescript-eslint/no-shadow
66 | let result = detectCustomMediaQuery(
67 | window.width,
68 | props.min || 0,
69 | props.max || 0
70 | );
71 | if (result !== isActiveRef.current) {
72 | setIsActive(result);
73 | isActiveRef.current = result;
74 | }
75 | }
76 | );
77 | }
78 | return () => listenerRef?.current?.remove();
79 | }, [props?.min, props?.max]);
80 |
81 | return props ? isActive : device;
82 | };
83 |
84 | export default useMediaQuery;
85 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import useMediaQuery from './hooks/useMediaQuery';
2 | import { createStyleSheet } from './createStyleSheet';
3 |
4 | export { useMediaQuery, createStyleSheet };
5 |
--------------------------------------------------------------------------------
/src/mediaQuery.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Operators,
3 | BreakPointMethods,
4 | BreakPoints,
5 | DynamicValues,
6 | Operator,
7 | StyleObject,
8 | } from './types';
9 | import { addDynamicValue } from './utill/addDynamicValue';
10 | import { isDynamicValue } from './utill/isDynamicValue';
11 |
12 | const generateMediaQueryMethod = (operator: Operator) => {
13 | return (deviceType: string): string => {
14 | return `@ ${deviceType} - ${operator}`;
15 | };
16 | };
17 |
18 | export const breakPointMethods: BreakPointMethods<{}> = {
19 | up: generateMediaQueryMethod('>'),
20 | down: generateMediaQueryMethod('<'),
21 | only: generateMediaQueryMethod('='),
22 | };
23 |
24 | export const dynamicOperator: Operators = {
25 | '>': (width, breakpoint) => {
26 | if (width >= breakpoint.start && breakpoint.start === breakpoint.end) {
27 | return true;
28 | }
29 | return width >= breakpoint.start;
30 | },
31 | '<': (width, breakpoint) => {
32 | if (width <= breakpoint.end && breakpoint.start === breakpoint.end) {
33 | return true;
34 | }
35 | return width <= breakpoint.end;
36 | },
37 | '=': (width, breakpoint) => {
38 | if (breakpoint.start === breakpoint.end) {
39 | return width >= breakpoint.start;
40 | }
41 | return width >= breakpoint.start && width <= breakpoint.end;
42 | },
43 | };
44 |
45 | export const isMediaQuery = (cssKey: string): boolean => cssKey.includes('@');
46 |
47 | export const isApplicableMediaQuery = (
48 | cssKey: string,
49 | width: number,
50 | breakpoints: BreakPoints
51 | ): boolean => {
52 | const [, deviceType, , operator] = cssKey.split(' ') as [
53 | string,
54 | keyof Operators,
55 | string,
56 | keyof Operators
57 | ];
58 |
59 | const currentBreakPoint = breakpoints[deviceType as keyof BreakPoints];
60 | if (!currentBreakPoint) {
61 | throw new Error(`"${deviceType}" is not a valid breakpoint`);
62 | }
63 | if (!dynamicOperator[operator]) {
64 | throw new Error(
65 | '@@ Invalid Media Query. Please use BREAKPOINTS utility methods for Media Queries'
66 | );
67 | }
68 | return dynamicOperator[operator]!(width, currentBreakPoint);
69 | };
70 |
71 | export const processMediaQuery = (
72 | cssKeyy: string,
73 | width: number,
74 | breakpoints: BreakPoints,
75 | cssValue: object,
76 | dynamicValues: DynamicValues[] | any,
77 | cssID: string,
78 | rawStyle: object
79 | ): [DynamicValues[], object] => {
80 | if (typeof cssValue !== 'object') {
81 | throw new Error(
82 | 'Invalid media query value found, Media query should e an Object'
83 | );
84 | }
85 |
86 | let newDynamicValues = { ...dynamicValues }; //[...dynamicValues];
87 | let newRawStyles = { ...rawStyle } as StyleObject;
88 | let mediaQueryRawStyle = {} as StyleObject;
89 |
90 | const isApplicable = isApplicableMediaQuery(cssKeyy, width, breakpoints);
91 |
92 | if (isApplicable) {
93 | const mediaQueryStyles = cssValue as StyleObject;
94 | for (const cssKey in mediaQueryStyles) {
95 | const element = mediaQueryStyles[cssKey];
96 |
97 | if (isDynamicValue(element)) {
98 | newDynamicValues = addDynamicValue(
99 | newDynamicValues,
100 | cssID,
101 | cssKey,
102 | element
103 | );
104 | }
105 |
106 | if (!isDynamicValue(element)) {
107 | if (newDynamicValues[cssKey]) delete newDynamicValues[cssKey];
108 | mediaQueryRawStyle[cssKey] = element;
109 | }
110 | }
111 |
112 | newRawStyles = {
113 | ...newRawStyles,
114 | [cssID]: {
115 | ...newRawStyles[cssID],
116 | ...mediaQueryRawStyle,
117 | },
118 | };
119 | }
120 | return [newDynamicValues, newRawStyles];
121 | };
122 |
--------------------------------------------------------------------------------
/src/multiKeyStore.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from './types';
2 |
3 | class multiKeyStore {
4 | cache: any;
5 |
6 | constructor() {
7 | this.cache = new Map();
8 | }
9 |
10 | set(key1: any, key2: any, value: Config) {
11 | let subCache = this.cache.get(key1);
12 |
13 | if (!subCache) {
14 | subCache = new Map();
15 | this.cache.set(key1, subCache);
16 | }
17 |
18 | subCache.set(key2, value);
19 | }
20 | get(key1: any, key2: any) {
21 | const subCache = this.cache.get(key1);
22 | return subCache ? subCache.get(key2) : undefined;
23 | }
24 | delete(key1: any, key2: any) {
25 | const subCache = this.cache.get(key1);
26 | subCache.delete(key2);
27 | }
28 |
29 | clear() {
30 | this.cache.clear();
31 | }
32 | }
33 |
34 | export default multiKeyStore;
35 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { ViewStyle, TextStyle, ImageStyle } from 'react-native';
2 |
3 | export type StyleObject = {
4 | [key: indexType]: any;
5 | };
6 |
7 | export type DynamicType = {
8 | [key in keyof Type as key]: ((props: object) => any) | Type[key];
9 | };
10 |
11 | export type indexType = string | number | symbol;
12 |
13 | export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
14 | export type Operator = '>' | '<' | '=';
15 | export type Media = `@ ${Size} - ${Operator}`;
16 |
17 | export interface DynamicValues {
18 | key: string;
19 | dynamic: (props: object) => any;
20 | }
21 |
22 | type Breakpoint = {
23 | start: number;
24 | end: number;
25 | };
26 |
27 | export type BreakPoints = {
28 | [key in Size]: Breakpoint;
29 | };
30 |
31 | export type Operators = {
32 | [key in Operator]?: (width: number, breakpoint: Breakpoint) => boolean;
33 | };
34 |
35 | type BrMethodTypes = 'up' | 'down' | 'only';
36 | export type BreakPointMethods = {
37 | [key in BrMethodTypes]: (deviceType: keyof T) => string;
38 | };
39 |
40 | export type Config = {
41 | dynamicValues: DynamicValues[];
42 | styles: object;
43 | };
44 |
45 | export type Query = {
46 | [key: indexType]:
47 | | RNStyle
48 | | string
49 | | number
50 | | ((options: { [key: indexType]: any }) => any);
51 | };
52 |
53 | // type theme = {
54 | // [key: indexType]: any;
55 | // };
56 | export type breakpoint = {
57 | breakpoints: BreakPointMethods;
58 | theme: T['theme'];
59 | };
60 |
61 | type RNStyle = ViewStyle & TextStyle & ImageStyle;
62 | type mergeRnstyle = Query & RNStyle;
63 |
64 | export type StyleProps = {
65 | [key: indexType]: {
66 | [key in keyof mergeRnstyle]?: mergeRnstyle[key] extends RNStyle
67 | ? RNStyle | ((options: { [key: indexType]: any }) => any)
68 | : mergeRnstyle[key] | ((options: { [key: indexType]: any }) => any);
69 | };
70 | };
71 |
--------------------------------------------------------------------------------
/src/utill/addDynamicValue.ts:
--------------------------------------------------------------------------------
1 | import type { DynamicValues } from 'src/types';
2 |
3 | export const addDynamicValue = (
4 | dynamicValues: DynamicValues[] | any,
5 | key: string,
6 | cssKey: string,
7 | cssValue: () => any
8 | ): DynamicValues[] | any => {
9 | dynamicValues[cssKey] = {
10 | key: `${key}.${cssKey}`,
11 | dynamic: cssValue,
12 | };
13 | return dynamicValues;
14 | };
15 |
--------------------------------------------------------------------------------
/src/utill/getDeviceType.ts:
--------------------------------------------------------------------------------
1 | export const getDevicetype = (width: number, breakpoints: any): string => {
2 | let br = '';
3 |
4 | for (const key in breakpoints) {
5 | if (breakpoints.hasOwnProperty(key)) {
6 | if (width >= breakpoints[key]) {
7 | br = key;
8 | }
9 | }
10 | }
11 |
12 | return br;
13 | };
14 |
--------------------------------------------------------------------------------
/src/utill/initializeBreakpoints.ts:
--------------------------------------------------------------------------------
1 | import type { indexType } from '../types';
2 |
3 | export type BreakpointDetail = {
4 | [key: indexType]: { start?: number; end?: number };
5 | };
6 | let defaultBreakPoints = {};
7 |
8 | const initializeBreakpoints = (breakpoints: {
9 | [key: indexType]: number;
10 | }): BreakpointDetail => {
11 | const configuredBreakpoints: BreakpointDetail = {};
12 |
13 | Object.keys({ ...defaultBreakPoints, ...breakpoints }).map(
14 | (key, index, arr) => {
15 | const nextBreakPointKey = arr[index + 1];
16 | const start = index === 0 ? 0 : breakpoints[key];
17 | const end = (breakpoints[nextBreakPointKey as any] as any) - 1 || start;
18 |
19 | configuredBreakpoints[key] = { start, end };
20 | }
21 | );
22 |
23 | return configuredBreakpoints;
24 | };
25 |
26 | export default initializeBreakpoints;
27 |
--------------------------------------------------------------------------------
/src/utill/isDynamicValue.ts:
--------------------------------------------------------------------------------
1 | export const isDynamicValue = (styleKey: string): boolean =>
2 | typeof styleKey === 'function';
3 |
--------------------------------------------------------------------------------
/src/utill/shallowEqual.ts:
--------------------------------------------------------------------------------
1 | function is(x: unknown, y: unknown) {
2 | if (x === y) {
3 | return x !== 0 || y !== 0 || 1 / x === 1 / y;
4 | } else {
5 | // eslint-disable-next-line no-self-compare
6 | return x !== x && y !== y;
7 | }
8 | }
9 |
10 | export default function shallowEqual(objA: any, objB: any) {
11 | if (is(objA, objB)) return true;
12 |
13 | if (
14 | typeof objA !== 'object' ||
15 | objA === null ||
16 | typeof objB !== 'object' ||
17 | objB === null
18 | ) {
19 | return false;
20 | }
21 |
22 | const keysA = Object.keys(objA);
23 | const keysB = Object.keys(objB);
24 |
25 | if (keysA.length !== keysB.length) return false;
26 |
27 | for (let i = 0; i < keysA.length; i++) {
28 | if (
29 | !Object.prototype.hasOwnProperty.call(objB, keysA[i] as any) ||
30 | !is(objA[keysA[i] as any], objB[keysA[i] as any])
31 | ) {
32 | return false;
33 | }
34 | }
35 |
36 | return true;
37 | }
38 |
--------------------------------------------------------------------------------
/src/validations/checkProviderError.ts:
--------------------------------------------------------------------------------
1 | const checkProviderError = (data: any) => {
2 | if (!data) {
3 | throw new Error(
4 | 'Must need to Create Style Sheet First or Please Follow the Documentation'
5 | );
6 | }
7 | };
8 |
9 | export default checkProviderError;
10 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "extends": "./tsconfig",
4 | "exclude": ["example"]
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "react-native-restyle-sheet": ["./src/index"]
6 | },
7 | "allowUnreachableCode": false,
8 | "allowUnusedLabels": false,
9 | "esModuleInterop": true,
10 | "importsNotUsedAsValues": "error",
11 | "forceConsistentCasingInFileNames": true,
12 | "jsx": "react",
13 | "lib": ["esnext"],
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "noFallthroughCasesInSwitch": true,
17 | "noImplicitReturns": true,
18 | "noImplicitUseStrict": false,
19 | "noStrictGenericChecks": false,
20 | "noUncheckedIndexedAccess": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "resolveJsonModule": true,
24 | "skipLibCheck": true,
25 | "strict": true,
26 | "target": "esnext"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------