├── src
├── __tests__
│ └── index.test.tsx
├── utils.ts
├── types.ts
└── index.tsx
├── .gitattributes
├── tsconfig.build.json
├── babel.config.js
├── .yarnrc
├── assets
├── transpile-flow.png
└── styled-variants.jpeg
├── example
├── src
│ ├── App.tsx
│ ├── AnimeApp
│ │ ├── theme.ts
│ │ ├── components
│ │ │ └── Header.tsx
│ │ └── App.tsx
│ └── BenchmarkApp
│ │ ├── Benchmark.tsx
│ │ └── theme.ts
├── index.js
├── extension.d.ts
├── tsconfig.json
├── app.json
├── babel.config.js
├── webpack.config.js
├── metro.config.js
└── package.json
├── .editorconfig
├── README.md
├── babel-plugin
├── package.json
├── index.js
├── playground.js
├── utility-props.js
├── hash.js
├── utility-prop-v2.js
├── __test__
│ ├── index.test.jsx
│ └── __snapshots__
│ │ └── index.test.jsx.snap
├── utility-prop-visitor.js
├── utility-prop-v3.js
└── visitor.js
├── scripts
└── bootstrap.js
├── tsconfig.json
├── .gitignore
├── LICENSE
├── .circleci
└── config.yml
├── package.json
└── CONTRIBUTING.md
/src/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | it.todo('write a test');
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 | # specific for windows script files
3 | *.bat text eol=crlf
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "extends": "./tsconfig",
4 | "exclude": ["example"]
5 | }
6 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | # Override Yarn command so we can automatically setup the repo on running `yarn`
2 |
3 | yarn-path "scripts/bootstrap.js"
4 |
--------------------------------------------------------------------------------
/assets/transpile-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intergalacticspacehighway/react-native-styled-variants/HEAD/assets/transpile-flow.png
--------------------------------------------------------------------------------
/assets/styled-variants.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intergalacticspacehighway/react-native-styled-variants/HEAD/assets/styled-variants.jpeg
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import AnimeApp from './AnimeApp/App';
3 | // import BenchMarkApp from './BenchmarkApp/Benchmark';
4 |
5 | export default function App() {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import { registerRootComponent } from 'expo';
2 |
3 | import App from './src/App';
4 |
5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
6 | // It also ensures that whether you load the app in the Expo client or in a native build,
7 | // the environment is set up appropriately
8 | registerRootComponent(App);
9 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/example/extension.d.ts:
--------------------------------------------------------------------------------
1 | import 'react-native';
2 | import { RNStyles } from 'react-native-styled-variants';
3 | import type { ITheme, IBreakpoints } from './src/AnimeApp/theme';
4 |
5 | declare module 'react-native' {
6 | interface ViewProps {
7 | sx?: RNStyles;
8 | }
9 | interface ImageProps {
10 | sx?: RNStyles;
11 | }
12 | interface TextProps {
13 | sx?: RNStyles;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-native",
4 | "target": "esnext",
5 | "paths": {
6 | "react-native-styled-variants": ["../src/index"]
7 | },
8 | "lib": [
9 | "esnext",
10 | "DOM"
11 | ],
12 | "allowJs": true,
13 | "skipLibCheck": true,
14 | "noEmit": true,
15 | "allowSyntheticDefaultImports": true,
16 | "resolveJsonModule": true,
17 | "esModuleInterop": true,
18 | "moduleResolution": "node"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React native styled variants
2 |
3 | Compile time interactive, responsive and theming utilities for React Native.
4 |
5 | [Checkout documentation here](https://styled-variants.vercel.app/)
6 |
7 |
8 |
9 | # Run example
10 |
11 | ```
12 | cd repo/
13 | yarn
14 | cd repo/example
15 | yarn
16 | yarn web-clean
17 |
18 | ```
19 |
20 | ## License
21 |
22 | MIT
23 |
--------------------------------------------------------------------------------
/babel-plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "babel-plugin-styled-variants",
3 | "version": "0.2.0",
4 | "main": "index.js",
5 | "author": "nishan",
6 | "license": "MIT",
7 | "scripts": {
8 | "test": "jest"
9 | },
10 | "dependencies": {
11 | "@babel/helper-module-imports": "^7.15.4",
12 | "@babel/template": "^7.15.4",
13 | "@babel/types": "^7.15.6"
14 | },
15 | "devDependencies": {
16 | "@babel/generator": "^7.15.4",
17 | "@babel/parser": "^7.15.6",
18 | "@babel/traverse": "^7.15.4",
19 | "jest": "^27.1.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-styled-variants-example",
3 | "displayName": "StyledVariants Example",
4 | "expo": {
5 | "name": "react-native-styled-variants-example",
6 | "slug": "react-native-styled-variants-example",
7 | "description": "Example app for react-native-styled-variants",
8 | "privacy": "public",
9 | "version": "1.0.0",
10 | "platforms": [
11 | "ios",
12 | "android",
13 | "web"
14 | ],
15 | "ios": {
16 | "supportsTablet": true
17 | },
18 | "assetBundlePatterns": [
19 | "**/*"
20 | ]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/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 | path.join(__dirname, '..', 'babel-plugin', 'index.js'),
21 | ],
22 | };
23 | };
24 |
--------------------------------------------------------------------------------
/babel-plugin/index.js:
--------------------------------------------------------------------------------
1 | const visitor = require('./visitor');
2 | const utilityPropVisitor = require('./utility-prop-visitor');
3 |
4 | function transformer() {
5 | return {
6 | visitor: {
7 | 'Program'(path) {
8 | visitor.Program(path);
9 | utilityPropVisitor.Program(path);
10 | },
11 | 'FunctionDeclaration|ArrowFunctionExpression'(path, state) {
12 | utilityPropVisitor['FunctionDeclaration|ArrowFunctionExpression'](
13 | path,
14 | state
15 | );
16 | },
17 | 'CallExpression'(path, state) {
18 | visitor.CallExpression(path, state);
19 | },
20 | },
21 | };
22 | }
23 |
24 | module.exports = transformer;
25 |
--------------------------------------------------------------------------------
/scripts/bootstrap.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const child_process = require('child_process');
3 |
4 | const root = path.resolve(__dirname, '..');
5 | const args = process.argv.slice(2);
6 | const options = {
7 | cwd: process.cwd(),
8 | env: process.env,
9 | stdio: 'inherit',
10 | encoding: 'utf-8',
11 | };
12 |
13 | let result;
14 |
15 | if (process.cwd() !== root || args.length) {
16 | // We're not in the root of the project, or additional arguments were passed
17 | // In this case, forward the command to `yarn`
18 | result = child_process.spawnSync('yarn', args, options);
19 | } else {
20 | // If `yarn` is run without arguments, perform bootstrap
21 | result = child_process.spawnSync('yarn', ['bootstrap'], options);
22 | }
23 |
24 | process.exitCode = result.status;
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "react-native-styled-variants": ["./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 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "resolveJsonModule": true,
23 | "skipLibCheck": true,
24 | "strict": true,
25 | "target": "esnext"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.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 | .idea
35 | .gradle
36 | local.properties
37 | android.iml
38 |
39 | # Cocoapods
40 | #
41 | example/ios/Pods
42 |
43 | # node.js
44 | #
45 | node_modules/
46 | npm-debug.log
47 | yarn-debug.log
48 | yarn-error.log
49 |
50 | # BUCK
51 | buck-out/
52 | \.buckd/
53 | android/app/libs
54 | android/keystores/debug.keystore
55 |
56 | # Expo
57 | .expo/*
58 |
59 | # generated by bob
60 | lib/
61 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 intergalacticspacehighway
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/example/metro.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const blacklist = require('metro-config/src/defaults/blacklist');
3 | const escape = require('escape-string-regexp');
4 | const pak = require('../package.json');
5 |
6 | const root = path.resolve(__dirname, '..');
7 |
8 | const modules = Object.keys({
9 | ...pak.peerDependencies,
10 | });
11 |
12 | module.exports = {
13 | projectRoot: __dirname,
14 | watchFolders: [root],
15 |
16 | // We need to make sure that only one version is loaded for peerDependencies
17 | // So we blacklist them at the root, and alias them to the versions in example's node_modules
18 | resolver: {
19 | blacklistRE: blacklist(
20 | modules.map(
21 | (m) =>
22 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`)
23 | )
24 | ),
25 |
26 | extraNodeModules: modules.reduce((acc, name) => {
27 | acc[name] = path.join(__dirname, 'node_modules', name);
28 | return acc;
29 | }, {}),
30 | },
31 |
32 | transformer: {
33 | getTransformOptions: async () => ({
34 | transform: {
35 | experimentalImportSupport: false,
36 | inlineRequires: true,
37 | },
38 | }),
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/babel-plugin/playground.js:
--------------------------------------------------------------------------------
1 | const { parse } = require('@babel/parser');
2 | const traverse = require('@babel/traverse').default;
3 | const generate = require('@babel/generator').default;
4 | const visitor = require('./visitor');
5 | const utilityPropVisitor = require('./utility-prop-visitor');
6 |
7 | function transformToStyles(code) {
8 | const ast = parse(code, {
9 | sourceType: 'module',
10 | plugins: ['jsx', 'typescript'],
11 | });
12 |
13 | traverse(ast, {
14 | 'Program'(path) {
15 | visitor.Program(path);
16 | utilityPropVisitor.Program(path);
17 | },
18 | 'FunctionDeclaration|ArrowFunctionExpression'(path, state) {
19 | utilityPropVisitor['FunctionDeclaration|ArrowFunctionExpression'](
20 | path,
21 | state
22 | );
23 | },
24 | 'CallExpression'(path, state) {
25 | visitor.CallExpression(path, state);
26 | },
27 | });
28 |
29 | // generate code <- ast
30 | const output = generate(ast, code);
31 | console.log(output.code); // 'const x = 1;'
32 | }
33 |
34 | transformToStyles(`
35 | const App = () => {
36 | const pressed = false;
37 | return
38 | }
39 | `);
40 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-styled-variants-example",
3 | "description": "Example app for react-native-styled-variants",
4 | "version": "0.0.1",
5 | "private": true,
6 | "main": "index",
7 | "scripts": {
8 | "android": "expo start --android",
9 | "ios": "expo start --ios",
10 | "web": "expo start --web",
11 | "web-clean": "rm -rf .expo && expo start --web -c",
12 | "start": "expo start",
13 | "test": "jest"
14 | },
15 | "dependencies": {
16 | "babel-loader": "^8.2.2",
17 | "babel-plugin-styled-variants": "file:../babel-plugin",
18 | "expo": "^40.0.0",
19 | "expo-splash-screen": "~0.8.1",
20 | "react": "16.13.1",
21 | "react-dom": "16.13.1",
22 | "react-native": "0.63.4",
23 | "react-native-safe-area-context": "3.1.9",
24 | "react-native-unimodules": "~0.12.0",
25 | "react-native-web": "^0.17.1",
26 | "styled-components": "^5.3.1"
27 | },
28 | "devDependencies": {
29 | "@babel/core": "~7.12.10",
30 | "@babel/runtime": "^7.9.6",
31 | "@types/react-native": "0.63.4",
32 | "@types/styled-components": "^5.1.14",
33 | "babel-plugin-module-resolver": "^4.0.0",
34 | "babel-preset-expo": "8.3.0",
35 | "expo-cli": "^4.0.13"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/example/src/AnimeApp/theme.ts:
--------------------------------------------------------------------------------
1 | import { createTheme } from 'react-native-styled-variants';
2 |
3 | const theme = {
4 | colors: {
5 | white: '#fff',
6 | sky: '#9ab1d9',
7 | gray: {
8 | 0: '#504f63',
9 | 1: '#34304e',
10 | 2: '#393556',
11 | 3: '#0e0d20',
12 | },
13 | blue: {
14 | 0: '#a5f0f7',
15 | 1: '#53abe4',
16 | 2: '#2381dc',
17 | 3: '#001f45',
18 | },
19 | maroon: {
20 | 0: '#d66e84',
21 | 1: '#e27b8c',
22 | 2: '#ab4367',
23 | 3: '#631a4e',
24 | },
25 | red: {
26 | 0: '#f06d65',
27 | 1: '#f05d58',
28 | 2: '#e02a28',
29 | },
30 | },
31 | space: {
32 | 'px': '1px',
33 | '0': '0',
34 | '1': 4,
35 | '2': 8,
36 | '3': 12,
37 | '4': 16,
38 | '5': 20,
39 | '6': 24,
40 | '8': 32,
41 | '10': 40,
42 | '12': 48,
43 | '16': 64,
44 | '20': 80,
45 | '24': 96,
46 | '32': 128,
47 | },
48 | fontSize: {
49 | lg: 20,
50 | },
51 | };
52 |
53 | const breakpoints = {
54 | 'base': 0,
55 | 'sm': 480,
56 | 'md': 768,
57 | 'lg': 992,
58 | 'xl': 1280,
59 | '2xl': 1536,
60 | };
61 |
62 | export const { createVariant, ThemeProvider, useCurrentBreakpoint, useTheme } =
63 | createTheme({
64 | theme,
65 | breakpoints,
66 | });
67 |
68 | export type ITheme = typeof theme;
69 | export type IBreakpoints = typeof breakpoints;
70 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | //@ts-nocheck
2 | import React from 'react';
3 |
4 | export const useControlledState = (val: T) => {
5 | const [state, setState] = React.useState(val);
6 |
7 | // ToDo make it controlled state
8 |
9 | return [state, setState];
10 | };
11 |
12 | export function getClosestBreakpoint(breakpoints: any, windowWidth: number) {
13 | let dimValues = Object.values(breakpoints);
14 | let index = -1;
15 | for (let i = 0; i < dimValues.length; i++) {
16 | if (dimValues[i] === windowWidth) {
17 | index = i;
18 | break;
19 | } else if (dimValues[i] > windowWidth && i !== 0) {
20 | index = i - 1;
21 | break;
22 | }
23 | // If windowWidth is greater than last available breakpoint clamp it to last index
24 | else if (dimValues[i] < windowWidth && i === dimValues.length - 1) {
25 | index = i;
26 | break;
27 | }
28 | }
29 | return index;
30 | }
31 | export const getCurrentBreakpoint = (
32 | windowWidth,
33 | breakpointsSortedKeys,
34 | breakpoints
35 | ) => {
36 | const breakpoint = getClosestBreakpoint(
37 | breakpointsSortedKeys.map((key) => breakpoints[key]),
38 | windowWidth
39 | );
40 | return breakpointsSortedKeys[breakpoint];
41 | };
42 |
43 | export const getClosestResponsiveValue = (
44 | values,
45 | currentBreakpoint,
46 | breakpointsSortedKeys
47 | ) => {
48 | let val;
49 | if (currentBreakpoint in values) {
50 | val = currentBreakpoint;
51 | } else {
52 | let currentBreakpointIndex = breakpointsSortedKeys.findIndex(
53 | (v) => v === currentBreakpoint
54 | );
55 | for (let i = currentBreakpointIndex; i >= 0; i--) {
56 | if (breakpointsSortedKeys[i] in values) {
57 | val = breakpointsSortedKeys[i];
58 | break;
59 | }
60 | }
61 | }
62 | return values[val];
63 | };
64 |
--------------------------------------------------------------------------------
/example/src/AnimeApp/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Image } from 'react-native';
3 | import { createVariant } from '../theme';
4 | import { useSafeAreaInsets } from 'react-native-safe-area-context';
5 |
6 | const shadow = {
7 | shadowOffset: {
8 | width: 0,
9 | height: 2,
10 | },
11 | shadowOpacity: 0.25,
12 | shadowRadius: 3.84,
13 | elevation: 5,
14 | };
15 |
16 | export const Header = () => {
17 | const insets = useSafeAreaInsets();
18 |
19 | return (
20 |
31 |
38 |
47 |
48 |
58 |
59 | );
60 | };
61 |
62 | const Avatar = createVariant(Image, {
63 | borderRadius: 50,
64 | height: 80,
65 | width: 80,
66 | });
67 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | //@ts-nocheck
2 | import type { ReactNode } from 'react';
3 | import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
4 |
5 | type IStyle = {
6 | [key: string]: any;
7 | varians?: {
8 | [key: string]: any;
9 | };
10 | defaultVariants?: {
11 | [key: string]: any;
12 | };
13 | };
14 |
15 | export type GetVariantProps