├── .gitignore ├── .babelrc ├── .watchmanconfig ├── lib ├── matchMediaMock.js ├── utils │ ├── isWeb.js │ ├── makeClassName.js │ └── expandCSS.js ├── plugins │ ├── index.js │ ├── mergeArray.js │ ├── copyStyles.js │ └── mediaQueries.js ├── resolveStyles.js ├── animate.js └── index.js ├── src ├── matchMediaMock.js ├── utils │ ├── isWeb.js │ ├── makeClassName.js │ └── expandCSS.js ├── plugins │ ├── mergeArray.js │ ├── index.js │ ├── copyStyles.js │ └── mediaQueries.js ├── index.js ├── resolveStyles.js └── animate.js ├── .editorconfig ├── .eslintrc ├── LICENSE.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": [ 3 | ".git", 4 | "node_modules" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /lib/matchMediaMock.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports,"__esModule",{value:true});exports.default= 2 | 3 | 4 | function(){return{ 5 | addListener:function addListener(){}, 6 | removeListener:function removeListener(){}};}; -------------------------------------------------------------------------------- /src/matchMediaMock.js: -------------------------------------------------------------------------------- 1 | // matchMedia mock for clients that can't install react-native-match-media 2 | // Mostly to support exponent 3 | 4 | export default () => ({ 5 | addListener() {}, 6 | removeListener() {}, 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /lib/utils/isWeb.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports,"__esModule",{value:true});exports. 2 | 3 | isWebVoidElement=isWebVoidElement;exports.default= 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | isWeb;var WEB_VOID_ELEMENTS=['input','TextInput'];function isWebVoidElement(element){return WEB_VOID_ELEMENTS.indexOf(element.type)!==-1||WEB_VOID_ELEMENTS.indexOf(element.type.displayName)!==-1;}function isWeb(){return!global.__BUNDLE_START_TIME__;} -------------------------------------------------------------------------------- /src/utils/isWeb.js: -------------------------------------------------------------------------------- 1 | const WEB_VOID_ELEMENTS = ['input', 'TextInput'] 2 | 3 | export function isWebVoidElement(element) { 4 | return ( 5 | WEB_VOID_ELEMENTS.indexOf(element.type) !== -1 || 6 | WEB_VOID_ELEMENTS.indexOf(element.type.displayName) !== -1 7 | ) 8 | } 9 | 10 | // HACK global.__BUNDLE_START_TIME__ is only present in React Native 11 | export default function isWeb() { return !global.__BUNDLE_START_TIME__ } 12 | -------------------------------------------------------------------------------- /lib/plugins/index.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports,"__esModule",{value:true});var _mergeArray=require('./mergeArray');var _mergeArray2=_interopRequireDefault(_mergeArray); 2 | var _copyStyles=require('./copyStyles');var _copyStyles2=_interopRequireDefault(_copyStyles); 3 | var _mediaQueries=require('./mediaQueries');var _mediaQueries2=_interopRequireDefault(_mediaQueries);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj};}exports.default= 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | [_mergeArray2.default,_copyStyles2.default,_mediaQueries2.default]; -------------------------------------------------------------------------------- /src/plugins/mergeArray.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import merge from 'lodash/merge' 3 | 4 | function reduceStyle(style) { 5 | if (typeof style !== 'object') return style 6 | if (!Array.isArray(style)) return style 7 | return style.reduce((mergedStyles, currentStyle) => 8 | merge({}, mergedStyles, currentStyle), 9 | {}) 10 | } 11 | 12 | export default element => { 13 | const { props } = element 14 | const { css, style } = props 15 | 16 | const newCSS = reduceStyle(css) 17 | const newStyle = reduceStyle(style) 18 | 19 | return React.cloneElement(element, { ...props, css: newCSS, style: newStyle }) 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | 4 | "parser": "babel-eslint", 5 | 6 | "plugins": [ 7 | "babel" 8 | ], 9 | 10 | "rules": { 11 | "semi": [2, "never"], 12 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 13 | "default-case": 0, 14 | "no-unused-expressions": [2, { 15 | "allowShortCircuit": true, 16 | "allowTernary": true 17 | }], 18 | "no-shadow": 0, 19 | "new-cap": 0, 20 | "no-loop-func": 0, 21 | "no-underscore-dangle": 0, 22 | 23 | "react/jsx-closing-bracket-location": [1, "after-props"], 24 | }, 25 | 26 | "ecmaFeatures": { 27 | "experimentalObjectRestSpread": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/utils/makeClassName.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports,"__esModule",{value:true});var _jshashes=require('jshashes');var _jshashes2=_interopRequireDefault(_jshashes);function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj};} 2 | 3 | 4 | 5 | 6 | var flatten=function flatten(object){ 7 | var cleanObject={}; 8 | for(var key in object){ 9 | if(!object.hasOwnProperty(key))continue; 10 | cleanObject[key]= 11 | typeof object[key]==='object'|| 12 | typeof object[key]==='function'? 13 | object[key].toString(): 14 | object[key]; 15 | } 16 | return cleanObject; 17 | };exports.default= 18 | 19 | 20 | 21 | function(css){ 22 | var cssHash=new _jshashes2.default.MD5().hex(JSON.stringify(flatten(css))).substr(0,7); 23 | return'ur-'+cssHash; 24 | }; -------------------------------------------------------------------------------- /src/plugins/index.js: -------------------------------------------------------------------------------- 1 | import mergeArray from './mergeArray' 2 | import copyStyles from './copyStyles' 3 | import mediaQueries from './mediaQueries' 4 | 5 | // Plugins follow this format: 6 | // 7 | // const myPlugin = (element, forceUpdate, config) { 8 | // const { props } = element 9 | // const newProps = { ...props } 10 | // // Do stuff, transform styles, attach event listeners that forceUpdate() 11 | // return React.cloneElement(element, newProps) 12 | // } 13 | // 14 | // element: the element being processed 15 | // forceUpdate: forces the Uranium-enhanced element to re-evaluate styles 16 | // config: The config passed to Uranium 17 | // 18 | // See merge-array for a simple example, and media-queries for a 19 | // simple example using event listeners 20 | export default [ 21 | mergeArray, 22 | copyStyles, 23 | mediaQueries, 24 | ] 25 | -------------------------------------------------------------------------------- /src/utils/makeClassName.js: -------------------------------------------------------------------------------- 1 | import Hashes from 'jshashes' 2 | 3 | // Recursively removes functions and nested objects from an object 4 | // Needed because Animated.values had circular dependencies 5 | // which were throwing errors in JSON.stringify below 6 | const flatten = object => { 7 | const cleanObject = {} 8 | for (const key in object) { 9 | if (!object.hasOwnProperty(key)) continue 10 | cleanObject[key] = 11 | typeof object[key] === 'object' || 12 | typeof object[key] === 'function' ? 13 | object[key].toString() : 14 | object[key] 15 | } 16 | return cleanObject 17 | } 18 | 19 | // Uses a 7-length substring of the MD5 of the css for a unique 20 | // id for className 21 | export default css => { 22 | const cssHash = new Hashes.MD5().hex(JSON.stringify(flatten(css))).substr(0, 7) 23 | return `ur-${cssHash}` 24 | } 25 | -------------------------------------------------------------------------------- /lib/plugins/mergeArray.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports,"__esModule",{value:true});var _extends=Object.assign||function(target){for(var i=1;i", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "babel-cli": "^6.14.0", 25 | "babel-core": "^6.14.0", 26 | "babel-eslint": "^6.1.2", 27 | "babel-preset-react-native": "^1.9.0", 28 | "eslint": "^3.4.0", 29 | "eslint-config-airbnb": "^10.0.1", 30 | "eslint-plugin-babel": "^3.3.0", 31 | "eslint-plugin-import": "^1.14.0", 32 | "eslint-plugin-jsx-a11y": "^2.2.1", 33 | "eslint-plugin-react": "^6.2.0" 34 | }, 35 | "peerDependencies": { 36 | "react": ">=15.3.2" 37 | }, 38 | "dependencies": { 39 | "decamelize": "^1.2.0", 40 | "jshashes": "^1.0.5", 41 | "lodash": "^4.17.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | 3 | import matchMediaMock from './matchMediaMock' 4 | import resolveStyles from './resolveStyles' 5 | 6 | if (!global.matchMedia) { 7 | global.matchMedia = matchMediaMock; 8 | 9 | if (!global.__exponent) { 10 | console.warn( // eslint-disable-line no-console 11 | 'global.matchMedia not found. Uranium has mocked it for you. ' + 12 | 'To get rid of this warning, in your index.ios.js or index.android.js ' + 13 | 'set global.matchMedia to react-native-match-media or to Uranium\'s `matchMediaMock`. ' + 14 | 'See the Uranium docs for details.' 15 | ) 16 | } 17 | } 18 | 19 | export { default as animate } from './animate' 20 | export { default as matchMediaMock } from './matchMediaMock' 21 | 22 | export default component => { 23 | // Handle stateless functional components 24 | const ComposedComponent = (component.render || component.prototype.render) ? 25 | component : 26 | class extends Component { 27 | static propTypes = component.propTypes; 28 | static defaultProps = component.defaultProps; 29 | 30 | render() { 31 | return component(this.props) 32 | } 33 | } 34 | 35 | class Uranium extends ComposedComponent { 36 | componentWillUnmount() { 37 | if (super.componentWillUnmount) super.componentWillUnmount() 38 | this._unmounted = true 39 | } 40 | _protectedForceUpdate = () => !this._unmounted && this.forceUpdate() 41 | 42 | render() { 43 | return resolveStyles(super.render(), this._protectedForceUpdate) 44 | } 45 | } 46 | 47 | /* eslint-disable prefer-template */ 48 | Uranium.displayName = 49 | 'Uranium(' + 50 | (ComposedComponent.displayName || ComposedComponent.name || 'Component') + 51 | ')' 52 | /* eslint-enable prefer-template */ 53 | 54 | return Uranium 55 | } 56 | -------------------------------------------------------------------------------- /src/resolveStyles.js: -------------------------------------------------------------------------------- 1 | import omit from 'lodash/omit' 2 | import React from 'react' 3 | 4 | import Plugins from './plugins' 5 | 6 | let resolveStyles = component => component 7 | 8 | const _resolveChildren = (element, forceUpdate) => { 9 | const { children } = element.props 10 | let newChildren 11 | 12 | if (typeof children === 'string' || typeof children === 'number') { 13 | newChildren = children 14 | } else if (typeof children === 'function') { 15 | newChildren = (...args) => { 16 | const result = children.apply(null, args) 17 | if (!React.isValidElement(result)) return result 18 | return resolveStyles(result, forceUpdate) 19 | } 20 | } else if (React.Children.count(children) === 1 && children.type) { 21 | const onlyChild = React.Children.only(children) 22 | newChildren = resolveStyles(onlyChild, forceUpdate) 23 | } else { 24 | newChildren = React.Children.map( 25 | children, 26 | child => { 27 | if (React.isValidElement(child)) return resolveStyles(child, forceUpdate) 28 | return child 29 | } 30 | ) 31 | } 32 | 33 | return React.cloneElement( 34 | element, 35 | omit(element.props, 'key', 'ref'), 36 | newChildren 37 | ) 38 | } 39 | 40 | const _resolveProps = (element, forceUpdate) => { 41 | const newProps = Object.keys(element.props).reduce( 42 | (resolvedProps, prop) => { 43 | if (prop === 'children') return resolvedProps 44 | if (!React.isValidElement(element.props[prop])) return resolvedProps 45 | return { 46 | ...resolvedProps, 47 | prop: resolveStyles(element.props[prop], forceUpdate), 48 | } 49 | }, 50 | { ...element.props } 51 | ) 52 | 53 | return React.cloneElement( 54 | element, 55 | omit(newProps, 'key', 'ref') 56 | ) 57 | } 58 | 59 | const _runPlugins = (element, forceUpdate) => { 60 | if ( 61 | !React.isValidElement(element) || 62 | !element.props.css 63 | ) { 64 | return element 65 | } 66 | 67 | return Plugins.reduce( 68 | (element, plugin) => plugin(element, forceUpdate), 69 | element 70 | ) 71 | } 72 | 73 | resolveStyles = (element, forceUpdate) => 74 | [ 75 | _resolveChildren, 76 | _resolveProps, 77 | _runPlugins, 78 | ].reduce( 79 | (element, reducer) => reducer(element, forceUpdate), 80 | element 81 | ) 82 | 83 | export default resolveStyles 84 | -------------------------------------------------------------------------------- /src/plugins/copyStyles.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import makeClassName from '../utils/makeClassName' 4 | import { expandStyle, createCSSDeclarations } from '../utils/expandCSS' 5 | import isWeb, { isWebVoidElement } from '../utils/isWeb' 6 | 7 | export const URANIUM_CLASSNAME = 'ur' 8 | 9 | // This plugin assumes its the first plugin run, and makes the 10 | // style tag the rest of the plugins will use 11 | export default element => { 12 | const { props } = element 13 | const { css, style } = props 14 | 15 | // If we're on a native platform, copy css to style and be done 16 | // with it 17 | if (!isWeb() || isWebVoidElement(element)) { 18 | let newStyle = Object.keys(css).reduce( 19 | (styleAccumulator, property) => { 20 | if (property.match(/@media/)) return styleAccumulator 21 | return { 22 | ...styleAccumulator, 23 | [property]: css[property], 24 | } 25 | }, 26 | {} 27 | ) 28 | // Override css props with style prop 29 | newStyle = { ...newStyle, ...(style || {}) } 30 | return React.cloneElement(element, { ...props, style: newStyle }) 31 | } 32 | 33 | // If on web 34 | 35 | // Get animated values so we can pass them to the `style` prop instead of 36 | // trying to put them in the