├── 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 | loki variants 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