From cd89f88d96a9b895d649ff627710eb8dcdd21af4 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Sat, 17 Oct 2015 17:40:54 -0700 Subject: [PATCH] [add] StyleSheet API Initial StyleSheet implementation for Web. Converts style object declarations to "atomic" CSS rules. Close gh-25 --- CONTRIBUTING.md | 2 +- README.md | 186 ++--- config/webpack.config.base.js | 8 - docs/apis/StyleSheet.md | 117 +++ docs/style.md | 123 ---- package.json | 18 +- src/components/CoreComponent/index.js | 13 +- .../CoreComponent/modules/autoprefix.js | 47 -- .../CoreComponent/modules/restyle.js | 40 - src/components/Image/index.js | 9 +- src/components/Text/index.js | 5 +- src/components/TextInput/index.js | 5 +- src/components/Touchable/index.js | 5 +- src/components/View/index.js | 5 +- src/example.js | 22 +- src/index.html | 1 + src/index.js | 5 + .../StylePropTypes/index.js} | 30 +- src/modules/StyleSheet/Store.js | 99 +++ .../__tests__/getStyleObjects-test.js | 33 + .../StyleSheet/__tests__/hyphenate-test.js | 13 + .../StyleSheet/__tests__/index-test.js | 34 + .../StyleSheet/__tests__/isObject-test.js | 15 + .../__tests__/isStyleObject-test.js | 16 + .../__tests__/normalizeValue-test.js | 13 + .../StyleSheet/__tests__/store-test.js | 127 ++++ src/modules/StyleSheet/getStyleObjects.js | 22 + src/modules/StyleSheet/hyphenate.js | 1 + src/modules/StyleSheet/index.js | 75 ++ src/modules/StyleSheet/isObject.js | 5 + src/modules/StyleSheet/isStyleObject.js | 9 + src/modules/StyleSheet/normalizeValue.js | 33 + src/modules/StyleSheet/predefs.js | 28 + src/modules/StyleSheet/prefixer.js | 3 + src/modules/styles/index.js | 2 - src/modules/styles/styles.css | 688 ------------------ 36 files changed, 809 insertions(+), 1048 deletions(-) create mode 100644 docs/apis/StyleSheet.md delete mode 100644 docs/style.md delete mode 100644 src/components/CoreComponent/modules/autoprefix.js delete mode 100644 src/components/CoreComponent/modules/restyle.js rename src/{components/CoreComponent/modules/stylePropTypes.js => modules/StylePropTypes/index.js} (81%) create mode 100644 src/modules/StyleSheet/Store.js create mode 100644 src/modules/StyleSheet/__tests__/getStyleObjects-test.js create mode 100644 src/modules/StyleSheet/__tests__/hyphenate-test.js create mode 100644 src/modules/StyleSheet/__tests__/index-test.js create mode 100644 src/modules/StyleSheet/__tests__/isObject-test.js create mode 100644 src/modules/StyleSheet/__tests__/isStyleObject-test.js create mode 100644 src/modules/StyleSheet/__tests__/normalizeValue-test.js create mode 100644 src/modules/StyleSheet/__tests__/store-test.js create mode 100644 src/modules/StyleSheet/getStyleObjects.js create mode 100644 src/modules/StyleSheet/hyphenate.js create mode 100644 src/modules/StyleSheet/index.js create mode 100644 src/modules/StyleSheet/isObject.js create mode 100644 src/modules/StyleSheet/isStyleObject.js create mode 100644 src/modules/StyleSheet/normalizeValue.js create mode 100644 src/modules/StyleSheet/predefs.js create mode 100644 src/modules/StyleSheet/prefixer.js delete mode 100644 src/modules/styles/index.js delete mode 100644 src/modules/styles/styles.css diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d43d0ac..77206c8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,7 +68,7 @@ not want to merge into the project. Development commands: * `npm run build` – build the library -* `npm run dev` – start the dev server and develop against live examples +* `npm run examples` – start the dev server and develop against live examples * `npm run lint` – run the linter * `npm run test` – run the linter and unit tests diff --git a/README.md b/README.md index 3427d0c7..973bf132 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ [![Build Status][travis-image]][travis-url] [![npm version][npm-image]][npm-url] -The core [React Native][react-native-url] components adapted and expanded upon -for the web, backed by a precomputed CSS library. ~21KB minified and gzipped. +[React Native][react-native-url] components and APIs for the Web. +~19 KB minified and gzipped. * [Slack: reactiflux channel #react-native-web][slack-url] * [Gitter: react-native-web][gitter-url] @@ -12,7 +12,8 @@ for the web, backed by a precomputed CSS library. ~21KB minified and gzipped. ## Table of contents * [Install](#install) -* [Use](#use) +* [Example](#example) +* [APIs](#APIs) * [Components](#components) * [Styling](#styling) * [Contributing](#contributing) @@ -25,32 +26,107 @@ for the web, backed by a precomputed CSS library. ~21KB minified and gzipped. npm install --save react react-native-web ``` -## Use +## Example React Native for Web exports its components and a reference to the `React` -installation. Styles are authored in JavaScript as plain objects. +installation. Styles are defined with, and used as JavaScript objects. + +Component: ```js -import React, { View } from 'react-native-web' +import React, { Image, StyleSheet, Text, View } from 'react-native-web' -class MyComponent extends React.Component { +const Title = ({ children }) => {children} + +const Summary = ({ children }) => ( + + {children} + +) + +class App extends React.Component { render() { return ( - + + + React Native Web + Build high quality web apps using React + ) - } -} + }, +}) -const styles = { - root: { - borderColor: 'currentcolor' - borderWidth: '5px', - flexDirection: 'row' - height: '5em' +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + margin: 40 + }, + image: { + height: 40, + marginRight: 10, + width: 40, + }, + text: { + flex: 1, + justifyContent: 'center' + }, + title: { + fontSize: '1.25rem', + fontWeight: 'bold' + }, + subtitle: { + fontSize: '1rem' } -} +}) ``` +Pre-render styles on the server: + +```js +// server.js +import App from './components/App' +import React, { StyleSheet } from 'react-native-web' + +const html = React.renderToString(); +const css = StyleSheet.renderToString(); + +const Html = () => ( + + + + + +
+ + +) +``` + +Render styles on the client: + +```js +// client.js +import App from './components/App' +import React, { StyleSheet } from 'react-native-web' + +React.render( + , + document.getElementById('react-root') +) + +document.getElementById('stylesheet').textContent = StyleSheet.renderToString() +``` + +## APIs + +### [`StyleSheet`](docs/apis/StyleSheet.md) + +StyleSheet is a style abstraction that transforms inline styles to CSS on the +client or the server. It provides a minimal CSS reset. + ## Components ### [`Image`](docs/components/Image.md) @@ -88,78 +164,14 @@ The fundamental UI building block using flexbox for layout. ## Styling -React Native for Web provides a mechanism to declare all your styles in -JavaScript within your components. The `View` component makes it easy to build -common layouts with flexbox, such as stacked and nested boxes with margin -and padding. See this [guide to flexbox][flexbox-guide-url]. +React Native for Web relies on styles being defined in JavaScript. -Authoring `style` is no different to the existing use of inline styles in -React, but most inline styles are converted to single-purpose class names. The -current implementation includes 300+ precomputed CSS declarations (~4.5KB -gzipped) that covers many common property-value pairs. A more sophisticated -build-time implementation may produce a slightly larger CSS file for large -apps, and fall back to fewer inline styles. Read more about the [styling -strategy](docs/style.md). +The `View` component makes it easy to build common layouts with flexbox, such +as stacked and nested boxes with margin and padding. See this [guide to +flexbox][flexbox-guide-url]. -```js -import React, { Image, Text, View } from 'react-native-web' - -class App extends React.Component { - render() { - return ( - - - - - React Native Web - - - Build high quality web apps using React - - - - ) - }, -}) - -const styles = { - row: { - flexDirection: 'row', - margin: 40 - }, - image: { - height: 40, - marginRight: 10, - width: 40, - }, - text: { - flex: 1, - justifyContent: 'center' - }, - title: { - fontSize: '1.25rem', - fontWeight: 'bold' - }, - subtitle: { - fontSize: '1rem' - } -} -``` - -Combine and override style objects: - -```js -import baseStyle from './baseStyle' - -const buttonStyle = { - ...baseStyle, - backgroundColor: '#333', - color: '#fff' -} -``` +Styling components can be achieved with inline styles or the use of +[StyleSheet](docs/apis/StyleSheet.md). ## Contributing diff --git a/config/webpack.config.base.js b/config/webpack.config.base.js index 95ffd64b..671950b1 100644 --- a/config/webpack.config.base.js +++ b/config/webpack.config.base.js @@ -25,14 +25,6 @@ if (process.env.NODE_ENV === 'production') { module.exports = { module: { loaders: [ - { - test: /\.css$/, - loader: [ - 'style-loader', - 'css-loader?module&localIdentName=[hash:base64:5]', - 'autoprefixer-loader' - ].join('!') - }, { test: /\.jsx?$/, exclude: /node_modules/, diff --git a/docs/apis/StyleSheet.md b/docs/apis/StyleSheet.md new file mode 100644 index 00000000..fe6869a9 --- /dev/null +++ b/docs/apis/StyleSheet.md @@ -0,0 +1,117 @@ +# StyleSheet + +React Native for Web will automatically vendor-prefix styles applied to the +libraries components. The `StyleSheet` abstraction converts predefined styles +to CSS without a compile-time step. Some styles cannot be resolved outside of +the render loop and are applied as inline styles. + +The `style`-to-`className` conversion strategy is optimized to minimize the +amount of CSS required. Unique declarations are defined using "atomic" CSS – a +unique class name for a unique declaration. + +React Native for Web includes a CSS reset to remove unwanted user agent styles +from elements and pseudo-elements beyond the reach of React (e.g., `html` and +`body`). + +Create a new StyleSheet: + +``` +const styles = StyleSheet.create({ + container: { + borderRadius: 4, + borderWidth: 0.5, + borderColor: '#d6d7da', + }, + title: { + fontSize: 19, + fontWeight: 'bold', + }, + activeTitle: { + color: 'red', + }, +}) +``` + +Use styles: + +```js + + + +``` + +Render styles on the server or in the browser: + +```js +StyleSheet.renderToString() +``` + +## Methods + +**create**(obj: {[key: string]: any}) + +**renderToString**() + +## Strategy + +Mapping entire `style` objects to CSS rules can lead to increasingly large CSS +files. Each new component adds new rules to the stylesheet. + +![](../static/styling-strategy.png) + +React Native for Web uses an alternative strategy: mapping declarations to +declarations. + +For example: + +```js +... + +const styles = StyleSheet.create({ + root: { + background: 'transparent', + display: 'flex', + flexGrow: 1, + justifyContent: 'center' + } +}) +``` + +Yields (in development): + +```html +
...
+``` + +And is backed by the following CSS: + +```css +.background\:transparent {background:transparent;} +.display\:flex {display:flex;} +.flexGrow\:1 {flex-grow:1;} +.justifyContext\:center {justify-content:center;} +``` + +In production the class names are obfuscated. + +(CSS libraries like [Atomic CSS](http://acss.io/), +[Basscss](http://www.basscss.com/), [SUIT CSS](https://suitcss.github.io/), and +[tachyons](http://tachyons.io/) are attempts to limit style scope and limit +stylesheet growth in a similar way. But they're CSS utility libraries, each with a +particular set of classes and features to learn. All of them require developers +to manually connect CSS classes for given styles.) + +## Media Queries, pseudo-classes, and pseudo-elements + +Media Queries in JavaScript can be used to modify the render tree and styles. +This has the benefit of co-locating breakpoint-specific DOM and style changes. + +Pseudo-classes like `:hover` and `:focus` can be replaced with JavaScript +events. + +Pseudo-elements are not supported. diff --git a/docs/style.md b/docs/style.md deleted file mode 100644 index 3da2d0f6..00000000 --- a/docs/style.md +++ /dev/null @@ -1,123 +0,0 @@ -# Styling strategy - -Using the `style` attribute would normally produce inline styles. There are -several existing approaches to using the `style` attribute, some of which -convert inline styles to static CSS: -[jsxstyle](https://github.com/petehunt/jsxstyle), -[react-free-style](https://github.com/blakeembrey/react-free-style/), -[react-inline](https://github.com/martinandert/react-inline), -[react-native](https://facebook.github.io/react-native/), -[react-style](https://github.com/js-next/react-style), -[stilr](https://github.com/kodyl/stilr). - -## Style syntax: native vs proprietary data structure - -React Native for Web diverges from React Native by using plain JS objects for -styles: - -```js -... - -const styles = { - root: { - background: 'transparent', - display: 'flex', - flexGrow: 1, - justifyContent: 'center' - } -}; -``` - -Most approaches to managing style in React introduce a proprietary data -structure, often via an implementation of `Stylesheet.create`. - -```js -... - -const styles = Stylesheet.create({ - root: { - background: 'transparent', - display: 'flex', - flexGrow: 1, - justifyContent: 'center' - } -}); -``` - -## JS-to-CSS: conversion strategies - -Mapping entire `style` objects to CSS rules can lead to increasingly large CSS -files. Each new component adds new rules to the stylesheet. - -![](../static/styling-strategy.png) - -One strategy for converting styles from JS to CSS is to map style objects to -CSS rules. Another strategy is to map declarations to declarations. - -React Native for Web currently includes a proof-of-concept implementation of -the latter strategy. This results in smaller CSS files because all applications -has fewer unique declarations than total declarations. Creating a new component -with no new unique declarations results in no change to the CSS file. - -For example: - -```js -... - -const styles = { - root: { - background: 'transparent', - display: 'flex', - flexGrow: 1, - justifyContent: 'center' - } -}; -``` - -Yields: - -```html -... -``` - -And is backed by: - -```css -._abcde { background: transparent } -._fghij { display: flex } -._klmno { flex-grow: 1 } -._pqrst { justify-content: center } -``` - -The current implementation uses a precomputed CSS library of single-declaration -rules, with obfuscated selectors. This handles a signficant portion of possible -declarations. A build-time implementation would produce more accurate CSS -files and fall through to inline styles significantly less often. - -(CSS libraries like [Atomic CSS](http://acss.io/), -[Basscss](http://www.basscss.com/), [SUIT CSS](https://suitcss.github.io/), and -[tachyons](http://tachyons.io/) are attempts to limit style scope and limit -stylesheet growth in a similar way. But they're CSS utility libraries, each with a -particular set of classes and features to learn. All of them require developers -to manually connect CSS classes for given styles.) - -## Dynamic styles: use inline styles - -Some styles cannot be resolved ahead of time and continue to rely on inline -styles: - -```js - 0.5 ? 'red' : 'black') }}>... -``` - -## Media Queries, pseudo-classes, and pseudo-elements - -Media Queries could be replaced with `mediaMatch`. This would have the added -benefit of co-locating breakpoint-specific DOM and style changes. Perhaps Media -Query data could be accessed on `this.content`? - -Pseudo-classes like `:hover` and `:focus` can be handled with JavaScript. - -Pseudo-elements should be avoided in general, but for particular cases like -`::placeholder` it might be necessary to reimplement it in the `TextInput` -component (see React Native's API). diff --git a/package.json b/package.json index 9cf96450..e91f700a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ ], "scripts": { "build": "rm -rf ./dist && webpack --config config/webpack.config.publish.js --sort-assets-by --progress", - "dev": "webpack-dev-server --config config/webpack.config.example.js --inline --colors --quiet", + "examples": "webpack-dev-server --config config/webpack.config.example.js --inline --hot --colors --quiet", "lint": "eslint config src", "prepublish": "NODE_ENV=publish npm run build", "test": "npm run lint && npm run test:unit", @@ -16,24 +16,22 @@ "test:watch": "npm run test:unit -- --no-single-run" }, "dependencies": { + "inline-style-prefixer": "^0.3.3", "react": ">=0.13.3", "react-swipeable": "^3.0.2", "react-tappable": "^0.6.0", "react-textarea-autosize": "^2.5.3" }, "devDependencies": { - "autoprefixer-loader": "^3.1.0", "babel-core": "^5.8.23", "babel-eslint": "^4.1.1", "babel-loader": "^5.3.2", "babel-runtime": "^5.8.20", - "css-loader": "^0.18.0", "eslint": "^1.3.1", "eslint-config-standard": "^4.3.1", "eslint-config-standard-react": "^1.0.4", "eslint-plugin-react": "^3.3.1", "eslint-plugin-standard": "^1.3.0", - "extract-text-webpack-plugin": "^0.8.2", "karma": "^0.13.9", "karma-browserstack-launcher": "^0.1.5", "karma-chrome-launcher": "^0.2.0", @@ -45,7 +43,6 @@ "mocha": "^2.3.0", "node-libs-browser": "^0.5.2", "object-assign": "^4.0.1", - "style-loader": "^0.12.3", "webpack": "^1.12.1", "webpack-dev-server": "^1.10.1" }, @@ -54,5 +51,14 @@ "repository": { "type": "git", "url": "git://github.com/necolas/react-native-web.git" - } + }, + "tags": [ + "react" + ], + "keywords": [ + "react", + "react-component", + "react-native", + "web" + ] } diff --git a/src/components/CoreComponent/index.js b/src/components/CoreComponent/index.js index 3798917c..dd88989e 100644 --- a/src/components/CoreComponent/index.js +++ b/src/components/CoreComponent/index.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react' -import restyle from './modules/restyle' -import stylePropTypes from './modules/stylePropTypes' +import StylePropTypes from '../../modules/StylePropTypes' +import StyleSheet from '../../modules/StyleSheet' class CoreComponent extends React.Component { static propTypes = { @@ -13,18 +13,15 @@ class CoreComponent extends React.Component { testID: PropTypes.string } - static stylePropTypes = stylePropTypes; - static defaultProps = { - className: '', component: 'div' } + static stylePropTypes = StylePropTypes; + render() { const { - className, component: Component, - style, testID, ...other } = this.props @@ -32,7 +29,7 @@ class CoreComponent extends React.Component { return ( ) diff --git a/src/components/CoreComponent/modules/autoprefix.js b/src/components/CoreComponent/modules/autoprefix.js deleted file mode 100644 index 6216a7a3..00000000 --- a/src/components/CoreComponent/modules/autoprefix.js +++ /dev/null @@ -1,47 +0,0 @@ -export default function prefixStyles(style) { - if (style.hasOwnProperty('flexBasis')) { - style = { - WebkitFlexBasis: style.flexBasis, - msFlexBasis: style.flexBasis, - ...style - } - } - - if (style.hasOwnProperty('flexGrow')) { - style = { - WebkitBoxFlex: style.flexGrow, - WebkitFlexGrow: style.flexGrow, - msFlexPositive: style.flexGrow, - ...style - } - } - - if (style.hasOwnProperty('flexShrink')) { - style = { - WebkitFlexShrink: style.flexShrink, - msFlexNegative: style.flexShrink, - ...style - } - } - - // NOTE: adding `;` to the string value prevents React from automatically - // adding a `px` suffix to the unitless value - if (style.hasOwnProperty('order')) { - style = { - WebkitBoxOrdinalGroup: `${parseInt(style.order, 10) + 1};`, - WebkitOrder: `${style.order}`, - msFlexOrder: `${style.order}`, - ...style - } - } - - if (style.hasOwnProperty('transform')) { - style = { - WebkitTransform: style.transform, - msTransform: style.transform, - ...style - } - } - - return style -} diff --git a/src/components/CoreComponent/modules/restyle.js b/src/components/CoreComponent/modules/restyle.js deleted file mode 100644 index c6375395..00000000 --- a/src/components/CoreComponent/modules/restyle.js +++ /dev/null @@ -1,40 +0,0 @@ -import autoprefix from './autoprefix' -import styles from '../../../modules/styles' - -/** - * Get the HTML class that corresponds to a style declaration - * @param prop {string} prop name - * @param style {Object} style - * @return {string} class name - */ -function getSinglePurposeClassName(prop, style) { - const className = `${prop}-${style[prop]}` - if (style.hasOwnProperty(prop) && styles[className]) { - return styles[className] - } -} - -/** - * Replace inline styles with single purpose classes where possible - * @param props {Object} React Element properties - * @return {Object} - */ -export default function stylingStrategy(props) { - let className - let style = {} - - const classList = [ props.className ] - for (const prop in props.style) { - const styleClass = getSinglePurposeClassName(prop, props.style) - if (styleClass) { - classList.push(styleClass) - } else { - style[prop] = props.style[prop] - } - } - - className = classList.join(' ') - style = autoprefix(style) - - return { className, style } -} diff --git a/src/components/Image/index.js b/src/components/Image/index.js index f8845fae..50ecccf2 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -1,5 +1,6 @@ /* global window */ import { pickProps } from '../../modules/filterObjectProps' +import StyleSheet from '../../modules/StyleSheet' import CoreComponent from '../CoreComponent' import ImageStylePropTypes from './ImageStylePropTypes' import React, { PropTypes } from 'react' @@ -13,7 +14,7 @@ const STATUS_IDLE = 'IDLE' const imageStyleKeys = Object.keys(ImageStylePropTypes) -const styles = { +const styles = StyleSheet.create({ initial: { alignSelf: 'flex-start', backgroundColor: 'lightgray', @@ -49,7 +50,7 @@ const styles = { backgroundSize: '100% 100%' } } -} +}) class Image extends React.Component { constructor(props, context) { @@ -195,10 +196,10 @@ class Image extends React.Component { accessible={accessible} component='div' style={{ - ...(styles.initial), + ...styles.initial, ...resolvedStyle, ...(backgroundImage && { backgroundImage }), - ...(styles.resizeMode[resizeMode]) + ...styles.resizeMode[resizeMode] }} testID={testID} > diff --git a/src/components/Text/index.js b/src/components/Text/index.js index 1707dc44..b999d994 100644 --- a/src/components/Text/index.js +++ b/src/components/Text/index.js @@ -1,11 +1,12 @@ import { pickProps } from '../../modules/filterObjectProps' import CoreComponent from '../CoreComponent' import React, { PropTypes } from 'react' +import StyleSheet from '../../modules/StyleSheet' import TextStylePropTypes from './TextStylePropTypes' const textStyleKeys = Object.keys(TextStylePropTypes) -const styles = { +const styles = StyleSheet.create({ initial: { color: 'inherit', display: 'inline-block', @@ -20,7 +21,7 @@ const styles = { textOverflow: 'ellipsis', whiteSpace: 'nowrap' } -} +}) class Text extends React.Component { static propTypes = { diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js index 8774ccab..6c513959 100644 --- a/src/components/TextInput/index.js +++ b/src/components/TextInput/index.js @@ -1,12 +1,13 @@ import { pickProps } from '../../modules/filterObjectProps' import CoreComponent from '../CoreComponent' import React, { PropTypes } from 'react' +import StyleSheet from '../../modules/StyleSheet' import TextareaAutosize from 'react-textarea-autosize' import TextInputStylePropTypes from './TextInputStylePropTypes' const textInputStyleKeys = Object.keys(TextInputStylePropTypes) -const styles = { +const styles = StyleSheet.create({ initial: { appearance: 'none', backgroundColor: 'transparent', @@ -17,7 +18,7 @@ const styles = { font: 'inherit', padding: 0 } -} +}) class TextInput extends React.Component { static propTypes = { diff --git a/src/components/Touchable/index.js b/src/components/Touchable/index.js index 20fb756d..6240ca3d 100644 --- a/src/components/Touchable/index.js +++ b/src/components/Touchable/index.js @@ -1,14 +1,15 @@ import React, { PropTypes } from 'react' import Tappable from 'react-tappable' import View from '../View' +import StyleSheet from '../../modules/StyleSheet' -const styles = { +const styles = StyleSheet.create({ initial: { ...View.defaultProps.style, cursor: 'pointer', userSelect: undefined } -} +}) class Touchable extends React.Component { constructor(props, context) { diff --git a/src/components/View/index.js b/src/components/View/index.js index 92a19ed9..2570bf92 100644 --- a/src/components/View/index.js +++ b/src/components/View/index.js @@ -1,11 +1,12 @@ import { pickProps } from '../../modules/filterObjectProps' import CoreComponent from '../CoreComponent' import React, { PropTypes } from 'react' +import StyleSheet from '../../modules/StyleSheet' import ViewStylePropTypes from './ViewStylePropTypes' const viewStyleKeys = Object.keys(ViewStylePropTypes) -const styles = { +const styles = StyleSheet.create({ // https://github.com/facebook/css-layout#default-values initial: { alignItems: 'stretch', @@ -26,7 +27,7 @@ const styles = { font: 'inherit', textAlign: 'inherit' } -} +}) class View extends React.Component { static propTypes = { diff --git a/src/example.js b/src/example.js index 1058459d..7b79a11d 100644 --- a/src/example.js +++ b/src/example.js @@ -1,12 +1,12 @@ -import React, { Image, Swipeable, Text, TextInput, Touchable, View } from '.' +import React, { Image, StyleSheet, Swipeable, Text, TextInput, Touchable, View } from '.' const { Component, PropTypes } = React -class Heading extends Component { +class Heading extends React.Component { static propTypes = { - children: Text.propTypes.children, - level: PropTypes.oneOf(['1', '2', '3']), - size: PropTypes.oneOf(['xlarge', 'large', 'normal']) + children: PropTypes.any, + level: PropTypes.string, + size: PropTypes.string } static defaultProps = { @@ -27,7 +27,7 @@ class Heading extends Component { } } -const headingStyles = { +const headingStyles = StyleSheet.create({ size: { xlarge: { fontSize: '2rem', @@ -44,7 +44,7 @@ const headingStyles = { marginTop: '0.5em' } } -} +}) class Example extends Component { static propTypes = { @@ -205,7 +205,7 @@ class Example extends Component { } } -const styles = { +const styles = StyleSheet.create({ root: { maxWidth: '600px', margin: '0 auto' @@ -226,7 +226,7 @@ const styles = { pointerEventsBox: { alignItems: 'center', borderWidth: '1px', - flexGrow: '1', + flexGrow: 1, height: '100px', justifyContent: 'center' }, @@ -236,6 +236,8 @@ const styles = { height: '200px', justifyContent: 'center' } -} +}) React.render(, document.getElementById('react-root')) + +document.getElementById('react-stylesheet').textContent = StyleSheet.renderToString() diff --git a/src/index.html b/src/index.html index 9ca34596..8c64bf03 100644 --- a/src/index.html +++ b/src/index.html @@ -4,5 +4,6 @@ +
diff --git a/src/index.js b/src/index.js index e5f2bf13..0354b50b 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,7 @@ import React from 'react' +import StyleSheet from './modules/StyleSheet' + // components import Image from './components/Image' import ListView from './components/ListView' @@ -13,6 +15,9 @@ import View from './components/View' export default React export { + StyleSheet, + + // components Image, ListView, ScrollView, diff --git a/src/components/CoreComponent/modules/stylePropTypes.js b/src/modules/StylePropTypes/index.js similarity index 81% rename from src/components/CoreComponent/modules/stylePropTypes.js rename to src/modules/StylePropTypes/index.js index efe1afb9..fe226527 100644 --- a/src/components/CoreComponent/modules/stylePropTypes.js +++ b/src/modules/StylePropTypes/index.js @@ -1,11 +1,7 @@ import { PropTypes } from 'react' -const numberOrString = PropTypes.oneOfType([ - PropTypes.number, - PropTypes.string -]) - -const { string } = PropTypes +const { number, string } = PropTypes +const numberOrString = PropTypes.oneOfType([ number, string ]) export default { alignContent: string, @@ -20,21 +16,22 @@ export default { backgroundPosition: string, backgroundRepeat: string, backgroundSize: string, - borderColor: numberOrString, - borderBottomColor: numberOrString, - borderLeftColor: numberOrString, - borderRightColor: numberOrString, - borderTopColor: numberOrString, + border: string, + borderColor: string, + borderBottomColor: string, + borderLeftColor: string, + borderRightColor: string, + borderTopColor: string, borderRadius: numberOrString, borderTopLeftRadius: numberOrString, borderTopRightRadius: numberOrString, borderBottomLeftRadius: numberOrString, borderBottomRightRadius: numberOrString, - borderStyle: numberOrString, - borderBottomStyle: numberOrString, - borderLeftStyle: numberOrString, - borderRightStyle: numberOrString, - borderTopStyle: numberOrString, + borderStyle: string, + borderBottomStyle: string, + borderLeftStyle: string, + borderRightStyle: string, + borderTopStyle: string, borderWidth: numberOrString, borderBottomWidth: numberOrString, borderLeftWidth: numberOrString, @@ -47,6 +44,7 @@ export default { cursor: string, direction: string, display: string, + flex: string, flexBasis: string, flexDirection: string, flexGrow: numberOrString, diff --git a/src/modules/StyleSheet/Store.js b/src/modules/StyleSheet/Store.js new file mode 100644 index 00000000..bd14451a --- /dev/null +++ b/src/modules/StyleSheet/Store.js @@ -0,0 +1,99 @@ +import hyphenate from './hyphenate' +import normalizeValue from './normalizeValue' +import prefixer from './prefixer' + +export default class Store { + constructor( + initialState:Object = {}, + options:Object = { obfuscateClassNames: false } + ) { + this._counter = 0 + this._classNames = { ...initialState.classNames } + this._declarations = { ...initialState.declarations } + this._options = options + } + + get(property, value) { + const normalizedValue = normalizeValue(property, value) + const key = this._getDeclarationKey(property, normalizedValue) + return this._classNames[key] + } + + set(property, value) { + if (value != null) { + const normalizedValue = normalizeValue(property, value) + const values = this._getPropertyValues(property) || [] + if (values.indexOf(normalizedValue) === -1) { + values.push(normalizedValue) + this._setClassName(property, normalizedValue) + this._setPropertyValues(property, values) + } + } + } + + toString() { + const obfuscate = this._options.obfuscateClassNames + + // sort the properties to ensure shorthands are first in the cascade + const properties = Object.keys(this._declarations).sort() + + // transform the class name to a valid CSS selector + const getCssSelector = (property, value) => { + let className = this.get(property, value) + if (!obfuscate && className) { + className = className.replace(/[:?.%\\$#]/g, '\\$&') + } + return className + } + + // transform the declarations into CSS rules with vendor-prefixes + const buildCSSRules = (property, values) => { + return values.reduce((cssRules, value) => { + const declarations = prefixer.prefix({ [property]: value }) + const cssDeclarations = Object.keys(declarations).reduce((str, prop) => { + str += `${hyphenate(prop)}:${value};` + return str + }, '') + const selector = getCssSelector(property, value) + + cssRules += `\n.${selector}{${cssDeclarations}}` + + return cssRules + }, '') + } + + const css = properties.reduce((css, property) => { + const values = this._declarations[property] + css += buildCSSRules(property, values) + return css + }, '') + + return (`/* ${this._counter} unique declarations */${css}`) + } + + _getDeclarationKey(property, value) { + return `${property}:${value}` + } + + _getPropertyValues(property) { + return this._declarations[property] + } + + _setPropertyValues(property, values) { + this._declarations[property] = values.map(value => normalizeValue(property, value)) + } + + _setClassName(property, value) { + const key = this._getDeclarationKey(property, value) + const exists = !!this._classNames[key] + if (!exists) { + this._counter += 1 + if (this._options.obfuscateClassNames) { + this._classNames[key] = `_rn_${this._counter}` + } else { + const val = `${value}`.replace(/\s/g, '-') + this._classNames[key] = `${property}:${val}` + } + } + } +} diff --git a/src/modules/StyleSheet/__tests__/getStyleObjects-test.js b/src/modules/StyleSheet/__tests__/getStyleObjects-test.js new file mode 100644 index 00000000..600c5044 --- /dev/null +++ b/src/modules/StyleSheet/__tests__/getStyleObjects-test.js @@ -0,0 +1,33 @@ +/* eslint-env mocha */ + +import assert from 'assert' +import getStyleObjects from '../getStyleObjects' + +const fixture = { + rule: { + margin: 0, + padding: 0 + }, + nested: { + auto: { + backgroundSize: 'auto' + }, + contain: { + backgroundSize: 'contain' + } + }, + ignored: { + pading: 0 + } +} + +suite('modules/StyleSheet/getStyleObjects', () => { + test('returns only style objects', () => { + const actual = getStyleObjects(fixture) + assert.deepEqual(actual, [ + { margin: 0, padding: 0 }, + { backgroundSize: 'auto' }, + { backgroundSize: 'contain' } + ]) + }) +}) diff --git a/src/modules/StyleSheet/__tests__/hyphenate-test.js b/src/modules/StyleSheet/__tests__/hyphenate-test.js new file mode 100644 index 00000000..980485d7 --- /dev/null +++ b/src/modules/StyleSheet/__tests__/hyphenate-test.js @@ -0,0 +1,13 @@ +/* eslint-env mocha */ + +import assert from 'assert' +import hyphenate from '../hyphenate' + +suite('modules/StyleSheet/hyphenate', () => { + test('style property', () => { + assert.equal(hyphenate('alignItems'), 'align-items') + }) + test('vendor prefixed style property', () => { + assert.equal(hyphenate('WebkitAppearance'), '-webkit-appearance') + }) +}) diff --git a/src/modules/StyleSheet/__tests__/index-test.js b/src/modules/StyleSheet/__tests__/index-test.js new file mode 100644 index 00000000..e2226667 --- /dev/null +++ b/src/modules/StyleSheet/__tests__/index-test.js @@ -0,0 +1,34 @@ +/* eslint-env mocha */ + +import { resetCSS, predefinedCSS } from '../predefs' +import assert from 'assert' +import StyleSheet from '..' + +const styles = { root: { border: 0 } } + +suite('modules/StyleSheet', () => { + setup(() => { + StyleSheet.destroy() + }) + + test('create', () => { + assert.equal(StyleSheet.create(styles), styles) + }) + + test('renderToString', () => { + StyleSheet.create(styles) + assert.equal( + StyleSheet.renderToString(), + `${resetCSS}\n${predefinedCSS}\n` + + `/* 1 unique declarations */\n` + + `.border\\:0px{border:0px;}` + ) + }) + + test('resolve', () => { + const props = { className: 'className', style: styles.root } + const expected = { className: 'className border:0px', style: {} } + StyleSheet.create(styles) + assert.deepEqual(StyleSheet.resolve(props), expected) + }) +}) diff --git a/src/modules/StyleSheet/__tests__/isObject-test.js b/src/modules/StyleSheet/__tests__/isObject-test.js new file mode 100644 index 00000000..f8bb977a --- /dev/null +++ b/src/modules/StyleSheet/__tests__/isObject-test.js @@ -0,0 +1,15 @@ +/* eslint-env mocha */ + +import assert from 'assert' +import isObject from '../isObject' + +suite('modules/StyleSheet/isObject', () => { + test('returns "true" for objects', () => { + assert.ok(isObject({}) === true) + }) + test('returns "false" for non-objects', () => { + assert.ok(isObject(function () {}) === false) + assert.ok(isObject([]) === false) + assert.ok(isObject('') === false) + }) +}) diff --git a/src/modules/StyleSheet/__tests__/isStyleObject-test.js b/src/modules/StyleSheet/__tests__/isStyleObject-test.js new file mode 100644 index 00000000..350ec666 --- /dev/null +++ b/src/modules/StyleSheet/__tests__/isStyleObject-test.js @@ -0,0 +1,16 @@ +/* eslint-env mocha */ + +import assert from 'assert' +import isStyleObject from '../isStyleObject' + +const style = { margin: 0 } +const notStyle = { root: style } + +suite('modules/StyleSheet/isStyleObject', () => { + test('returns "true" for style objects', () => { + assert.ok(isStyleObject(style) === true) + }) + test('returns "false" for non-style objects', () => { + assert.ok(isStyleObject(notStyle) === false) + }) +}) diff --git a/src/modules/StyleSheet/__tests__/normalizeValue-test.js b/src/modules/StyleSheet/__tests__/normalizeValue-test.js new file mode 100644 index 00000000..5dee4d3e --- /dev/null +++ b/src/modules/StyleSheet/__tests__/normalizeValue-test.js @@ -0,0 +1,13 @@ +/* eslint-env mocha */ + +import assert from 'assert' +import normalizeValue from '../normalizeValue' + +suite('modules/StyleSheet/normalizeValue', () => { + test('normalizes property values requiring units', () => { + assert.deepEqual(normalizeValue('margin', 0), '0px') + }) + test('ignores unitless property values', () => { + assert.deepEqual(normalizeValue('flexGrow', 1), 1) + }) +}) diff --git a/src/modules/StyleSheet/__tests__/store-test.js b/src/modules/StyleSheet/__tests__/store-test.js new file mode 100644 index 00000000..65436d18 --- /dev/null +++ b/src/modules/StyleSheet/__tests__/store-test.js @@ -0,0 +1,127 @@ +/* eslint-env mocha */ + +import assert from 'assert' +import Store from '../Store' + +suite('modules/StyleSheet/Store', () => { + suite('the constructor', () => { + test('initialState', () => { + const initialState = { classNames: { 'alignItems:center': '__classname__' } } + const store = new Store(initialState) + assert.deepEqual(store._classNames['alignItems:center'], '__classname__') + }) + }) + + suite('#get', () => { + test('returns a declaration-specific className', () => { + const initialState = { + classNames: { + 'alignItems:center': '__expected__', + 'alignItems:flex-start': '__error__' + } + } + const store = new Store(initialState) + assert.deepEqual(store.get('alignItems', 'center'), '__expected__') + }) + }) + + suite('#set', () => { + test('stores declarations', () => { + const store = new Store() + store.set('alignItems', 'center') + store.set('flexGrow', 0) + store.set('flexGrow', 1) + store.set('flexGrow', 2) + assert.deepEqual(store._declarations, { + alignItems: [ 'center' ], + flexGrow: [ 0, 1, 2 ] + }) + }) + + test('human-readable classNames', () => { + const store = new Store() + store.set('alignItems', 'center') + store.set('flexGrow', 0) + store.set('flexGrow', 1) + store.set('flexGrow', 2) + assert.deepEqual(store._classNames, { + 'alignItems:center': 'alignItems:center', + 'flexGrow:0': 'flexGrow:0', + 'flexGrow:1': 'flexGrow:1', + 'flexGrow:2': 'flexGrow:2' + }) + }) + + test('obfuscated classNames', () => { + const store = new Store({}, { obfuscateClassNames: true }) + store.set('alignItems', 'center') + store.set('flexGrow', 0) + store.set('flexGrow', 1) + store.set('flexGrow', 2) + assert.deepEqual(store._classNames, { + 'alignItems:center': '_rn_1', + 'flexGrow:0': '_rn_2', + 'flexGrow:1': '_rn_3', + 'flexGrow:2': '_rn_4' + }) + }) + + test('value normalization', () => { + const store = new Store() + store.set('flexGrow', 0) + store.set('margin', 0) + assert.deepEqual(store._declarations, { + flexGrow: [ 0 ], + margin: [ '0px' ] + }) + assert.deepEqual(store._classNames, { + 'flexGrow:0': 'flexGrow:0', + 'margin:0px': 'margin:0px' + }) + }) + + test('replaces space characters', () => { + const store = new Store() + store.set('margin', '0 auto') + assert.deepEqual(store.get('margin', '0 auto'), 'margin:0-auto') + }) + }) + + suite('#toString', () => { + test('human-readable style sheet', () => { + const store = new Store() + store.set('alignItems', 'center') + store.set('marginBottom', 0) + store.set('margin', 1) + store.set('margin', 2) + store.set('margin', 3) + + const expected = '/* 5 unique declarations */\n' + + '.alignItems\\:center{align-items:center;}\n' + + '.margin\\:1px{margin:1px;}\n' + + '.margin\\:2px{margin:2px;}\n' + + '.margin\\:3px{margin:3px;}\n' + + '.marginBottom\\:0px{margin-bottom:0px;}' + + assert.equal(store.toString(), expected) + }) + + test('obfuscated style sheet', () => { + const store = new Store({}, { obfuscateClassNames: true }) + store.set('alignItems', 'center') + store.set('marginBottom', 0) + store.set('margin', 1) + store.set('margin', 2) + store.set('margin', 3) + + const expected = '/* 5 unique declarations */\n' + + '._rn_1{align-items:center;}\n' + + '._rn_3{margin:1px;}\n' + + '._rn_4{margin:2px;}\n' + + '._rn_5{margin:3px;}\n' + + '._rn_2{margin-bottom:0px;}' + + assert.equal(store.toString(), expected) + }) + }) +}) diff --git a/src/modules/StyleSheet/getStyleObjects.js b/src/modules/StyleSheet/getStyleObjects.js new file mode 100644 index 00000000..ebec0aba --- /dev/null +++ b/src/modules/StyleSheet/getStyleObjects.js @@ -0,0 +1,22 @@ +import isObject from './isObject' +import isStyleObject from './isStyleObject' + +/** + * Recursively check for objects that are style rules. + */ +const getStyleObjects = (styles: Object): Array => { + const keys = Object.keys(styles) + return keys.reduce((rules, key) => { + const possibleRule = styles[key] + if (isObject(possibleRule)) { + if (isStyleObject(possibleRule)) { + rules.push(possibleRule) + } else { + rules = rules.concat(getStyleObjects(possibleRule)) + } + } + return rules + }, []) +} + +export default getStyleObjects diff --git a/src/modules/StyleSheet/hyphenate.js b/src/modules/StyleSheet/hyphenate.js new file mode 100644 index 00000000..fa12e660 --- /dev/null +++ b/src/modules/StyleSheet/hyphenate.js @@ -0,0 +1 @@ +export default (string) => string.replace(/([A-Z])/g, '-$1').toLowerCase() diff --git a/src/modules/StyleSheet/index.js b/src/modules/StyleSheet/index.js new file mode 100644 index 00000000..8345f1ba --- /dev/null +++ b/src/modules/StyleSheet/index.js @@ -0,0 +1,75 @@ +import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs' +import getStyleObjects from './getStyleObjects' +import prefixer from './prefixer' +import Store from './Store' + +/** + * Initialize the store with pointer-event styles mapping to our custom pointer + * event classes + */ +const initialState = { classNames: predefinedClassNames } +const options = { obfuscateClassNames: process.env.NODE_ENV === 'production' } +const createStore = () => new Store(initialState, options) +let store = createStore() + +/** + * Process all unique declarations + */ +const create = (styles: Object): Object => { + const rules = getStyleObjects(styles) + rules.forEach((rule) => { + Object.keys(rule).forEach(property => { + const value = rule[property] + // add each declaration to the store + store.set(property, value) + }) + }) + return styles +} + +/** + * Destroy existing styles + */ +const destroy = () => { + store = createStore() +} + +/** + * Render the styles as a CSS style sheet + */ +const renderToString = () => { + const css = store.toString() + return `${resetCSS}\n${predefinedCSS}\n${css}` +} + +/** + * Accepts React props and converts inline styles to single purpose classes + * where possible. + */ +const resolve = ({ className = '', style = {} }) => { + let _className + let _style = {} + + const classList = [ className ] + for (const prop in style) { + let styleClass = store.get(prop, style[prop]) + + if (styleClass) { + classList.push(styleClass) + } else { + _style[prop] = style[prop] + } + } + + _className = classList.join(' ') + _style = prefixer.prefix(_style) + + return { className: _className, style: _style } +} + +export default { + create, + destroy, + renderToString, + resolve +} diff --git a/src/modules/StyleSheet/isObject.js b/src/modules/StyleSheet/isObject.js new file mode 100644 index 00000000..056792fd --- /dev/null +++ b/src/modules/StyleSheet/isObject.js @@ -0,0 +1,5 @@ +const isObject = (obj) => { + return Object.prototype.toString.call(obj) === '[object Object]' +} + +export default isObject diff --git a/src/modules/StyleSheet/isStyleObject.js b/src/modules/StyleSheet/isStyleObject.js new file mode 100644 index 00000000..939b6480 --- /dev/null +++ b/src/modules/StyleSheet/isStyleObject.js @@ -0,0 +1,9 @@ +import { pickProps } from '../filterObjectProps' +import StylePropTypes from '../StylePropTypes' + +const isStyleObject = (obj) => { + const declarations = pickProps(obj, Object.keys(StylePropTypes)) + return Object.keys(declarations).length > 0 +} + +export default isStyleObject diff --git a/src/modules/StyleSheet/normalizeValue.js b/src/modules/StyleSheet/normalizeValue.js new file mode 100644 index 00000000..c91e9698 --- /dev/null +++ b/src/modules/StyleSheet/normalizeValue.js @@ -0,0 +1,33 @@ +const unitlessNumbers = { + boxFlex: true, + boxFlexGroup: true, + columnCount: true, + flex: true, + flexGrow: true, + flexPositive: true, + flexShrink: true, + flexNegative: true, + fontWeight: true, + lineClamp: true, + lineHeight: true, + opacity: true, + order: true, + orphans: true, + widows: true, + zIndex: true, + zoom: true, + // SVG-related + fillOpacity: true, + strokeDashoffset: true, + strokeOpacity: true, + strokeWidth: true +} + +const normalizeValues = (property, value) => { + if (!unitlessNumbers[property] && typeof value === 'number') { + value = `${value}px` + } + return value +} + +export default normalizeValues diff --git a/src/modules/StyleSheet/predefs.js b/src/modules/StyleSheet/predefs.js new file mode 100644 index 00000000..7caafcb1 --- /dev/null +++ b/src/modules/StyleSheet/predefs.js @@ -0,0 +1,28 @@ +/** + * Reset unwanted styles beyond the control of React inline styles + */ +export const resetCSS = +`/* React Native Web */ +html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%} +body {margin:0} +button::-moz-focus-inner, input::-moz-focus-inner {border:0;padding:0} +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {-webkit-appearance:none}` + +/** + * Custom pointer event styles + */ +export const predefinedCSS = +`/* pointer-events */ +._rn_pe-a {pointer-events:auto} +._rn_pe-bn {pointer-events:none} +._rn_pe-bn * {pointer-events:auto} +._rn_pe-bo {pointer-events:auto} +._rn_pe-bo * {pointer-events:none} +._rn_pe-n {pointer-events:none}` + +export const predefinedClassNames = { + 'pointerEvents:auto': '_rn_pe-a', + 'pointerEvents:box-none': '_rn_pe-bn', + 'pointerEvents:box-only': '_rn_pe-bo', + 'pointerEvents:none': '_rn_pe-n' +} diff --git a/src/modules/StyleSheet/prefixer.js b/src/modules/StyleSheet/prefixer.js new file mode 100644 index 00000000..4292d803 --- /dev/null +++ b/src/modules/StyleSheet/prefixer.js @@ -0,0 +1,3 @@ +import Prefixer from 'inline-style-prefixer' +const prefixer = new Prefixer() +export default prefixer diff --git a/src/modules/styles/index.js b/src/modules/styles/index.js deleted file mode 100644 index b84a2a0b..00000000 --- a/src/modules/styles/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import styles from './styles.css' -export default styles diff --git a/src/modules/styles/styles.css b/src/modules/styles/styles.css deleted file mode 100644 index 7a0347d8..00000000 --- a/src/modules/styles/styles.css +++ /dev/null @@ -1,688 +0,0 @@ -/* align-content */ - -.alignContent-center { align-content: center; } -.alignContent-flex-end { align-content: flex-end; } -.alignContent-flex-start { align-content: flex-start; } -.alignContent-stretch { align-content: stretch; } -.alignContent-space-around { align-content: space-around; } -.alignContent-space-between { align-content: space-between; } - -/* align-items */ - -.alignItems-center { align-items: center; } -.alignItems-flex-end { align-items: flex-end; } -.alignItems-flex-start { align-items: flex-start; } -.alignItems-stretch { align-items: stretch; } -.alignItems-space-around { align-items: space-around; } -.alignItems-space-between { align-items: space-between; } - -/* align-self */ - -.alignSelf-auto { align-self: auto; } -.alignSelf-baseline { align-self: baseline; } -.alignSelf-center { align-self: center; } -.alignSelf-flex-end { align-self: flex-end; } -.alignSelf-flex-start { align-self: flex-start; } -.alignSelf-stretch { align-self: stretch; } - -/* appearance */ - -.appearance-none { appearance: none; } - -/* background-attachment */ - -.backgroundAttachment-fixed { background-attachment: fixed; } -.backgroundAttachment-inherit { background-attachment: inherit; } -.backgroundAttachment-local { background-attachment: local; } -.backgroundAttachment-scroll { background-attachment: scroll; } - -/* background-clip */ - -.backgroundClip-border-box { background-clip: border-box; } -.backgroundClip-content-box { background-clip: content-box; } -.backgroundClip-inherit { background-clip: inherit; } -.backgroundClip-padding-box { background-clip: padding-box; } - -/* background-color */ - -.backgroundColor-\#000, -.backgroundColor-black { background-color: black; } -.backgroundColor-\#fff, -.backgroundColor-white { background-color: white; } -.backgroundColor-currentcolor, -.backgroundColor-currentColor { background-color: currentcolor; } -.backgroundColor-inherit { background-color: inherit; } -.backgroundColor-transparent { background-color: transparent; } - -/* background-image */ - -.backgroundImage { background-image: none; } - -/* background-origin */ - -.backgroundOrigin-border-box { background-clip: border-box; } -.backgroundOrigin-content-box { background-clip: content-box; } -.backgroundOrigin-inherit { background-clip: inherit; } -.backgroundOrigin-padding-box { background-clip: padding-box; } - -/* background-position */ - -.backgroundPosition-bottom { background-position: bottom; } -.backgroundPosition-center { background-position: center; } -.backgroundPosition-left { background-position: left; } -.backgroundPosition-right { background-position: right; } -.backgroundPosition-top { background-position: top; } - -/* background-repeat */ - -.backgroundRepeat-inherit { background-repeat: inherit; } -.backgroundRepeat-no-repeat { background-repeat: no-repeat; } -.backgroundRepeat-repeat { background-repeat: repeat; } -.backgroundRepeat-repeat-x { background-repeat: repeat-x; } -.backgroundRepeat-repeat-y { background-repeat: repeat-y; } -.backgroundRepeat-round { background-repeat: round; } -.backgroundRepeat-space { background-repeat: space; } - -/* background-size */ - -.backgroundSize-auto { background-size: auto; } -.backgroundSize-contain { background-size: contain; } -.backgroundSize-cover { background-size: cover; } -.backgroundSize-inherit { background-size: inherit; } - -/* border-color */ - -.borderColor-\#fff, -.borderColor-white { border-color: white; } -.borderColor-currentcolor { border-color: currentcolor; } -.borderColor-inherit { border-color: inherit; } -.borderColor-transparent { border-color: transparent; } - -/* border-bottom-color */ - -.borderBottomColor-\#fff, -.borderBottomColor-white { border-bottom-color: white; } -.borderBottomColor-currentcolor { border-bottom-color: currentcolor; } -.borderBottomColor-inherit { border-bottom-color: inherit; } -.borderBottomColor-transparent { border-bottom-color: transparent; } - -/* border-left-color */ - -.borderLeftColor-\#fff, -.borderLeftColor-white { border-left-color: white; } -.borderLeftColor-currentcolor { border-left-color: currentcolor; } -.borderLeftColor-inherit { border-left-color: inherit; } -.borderLeftColor-transparent { border-left-color: transparent; } - -/* border-right-color */ - -.borderRightColor-\#fff, -.borderRightColor-white { border-right-color: white; } -.borderRightColor-currentcolor { border-right-color: currentcolor; } -.borderRightColor-inherit { border-right-color: inherit; } -.borderRightColor-transparent { border-right-color: transparent; } - -/* border-top-color */ - -.borderTopColor-\#fff, -.borderTopColor-white { border-top-color: white; } -.borderTopColor-currentcolor { border-top-color: currentcolor; } -.borderTopColor-inherit { border-top-color: inherit; } -.borderTopColor-transparent { border-top-color: transparent; } - -/* border-style */ - -.borderStyle-dashed { border-style: dashed; } -.borderStyle-dotted { border-style: dotted; } -.borderStyle-inherit { border-style: inherit; } -.borderStyle-none { border-style: none; } -.borderStyle-solid { border-style: solid; } - -/* border-bottom-style */ - -.borderBottomStyle-dashed { border-bottom-style: dashed; } -.borderBottomStyle-dotted { border-bottom-style: dotted; } -.borderBottomStyle-inherit { border-bottom-style: inherit; } -.borderBottomStyle-none { border-bottom-style: none; } -.borderBottomStyle-solid { border-bottom-style: solid; } - -/* border-left-style */ - -.borderLeftStyle-dashed { border-left-style: dashed; } -.borderLeftStyle-dotted { border-left-style: dotted; } -.borderLeftStyle-inherit { border-left-style: inherit; } -.borderLeftStyle-none { border-left-style: none; } -.borderLeftStyle-solid { border-left-style: solid; } - -/* border-right-style */ - -.borderRightStyle-dashed { border-right-style: dashed; } -.borderRightStyle-dotted { border-right-style: dotted; } -.borderRightStyle-inherit { border-right-style: inherit; } -.borderRightStyle-none { border-right-style: none; } -.borderRightStyle-solid { border-right-style: solid; } - -/* border-top-style */ - -.borderTopStyle-dashed { border-top-style: dashed; } -.borderTopStyle-dotted { border-top-style: dotted; } -.borderTopStyle-inherit { border-top-style: inherit; } -.borderTopStyle-none { border-top-style: none; } -.borderTopStyle-solid { border-top-style: solid; } - -/* border-width */ - -.borderWidth-0 { border-width: 0; } -.borderWidth-1px { border-width: 1px; } -.borderWidth-2px { border-width: 2px; } -.borderWidth-3px { border-width: 3px; } -.borderWidth-4px { border-width: 4px; } -.borderWidth-5px { border-width: 5px; } - -/* border-bottom-width */ - -.borderBottomWidth-0 { border-bottom-width: 0; } -.borderBottomWidth-1px { border-bottom-width: 1px; } -.borderBottomWidth-2px { border-bottom-width: 2px; } -.borderBottomWidth-3px { border-bottom-width: 3px; } -.borderBottomWidth-4px { border-bottom-width: 4px; } -.borderBottomWidth-5px { border-bottom-width: 5px; } - -/* border-left-width */ - -.borderLeftWidth-0 { border-left-width: 0; } -.borderLeftWidth-1px { border-left-width: 1px; } -.borderLeftWidth-2px { border-left-width: 2px; } -.borderLeftWidth-3px { border-left-width: 3px; } -.borderLeftWidth-4px { border-left-width: 4px; } -.borderLeftWidth-5px { border-left-width: 5px; } - -/* border-right-width */ - -.borderRightWidth-0 { border-right-width: 0; } -.borderRightWidth-1px { border-right-width: 1px; } -.borderRightWidth-2px { border-right-width: 2px; } -.borderRightWidth-3px { border-right-width: 3px; } -.borderRightWidth-4px { border-right-width: 4px; } -.borderRightWidth-5px { border-right-width: 5px; } - -/* border-top-width */ - -.borderTopWidth-0 { border-top-width: 0; } -.borderTopWidth-1px { border-top-width: 1px; } -.borderTopWidth-2px { border-top-width: 2px; } -.borderTopWidth-3px { border-top-width: 3px; } -.borderTopWidth-4px { border-top-width: 4px; } -.borderTopWidth-5px { border-top-width: 5px; } - -/* bottom */ - -.bottom-0 { bottom: 0; } -.bottom-50% { bottom: 50%; } -.bottom-100% { bottom: 100%; } - -/* box-sizing */ - -.boxSizing-border-box { box-sizing: border-box; } -.boxSizing-content-box { box-sizing: content-box; } -.boxSizing-inherit { box-sizing: inherit; } -.boxSizing-padding-box { box-sizing: padding-box; } - -/* clear */ - -.clear-both { clear: both; } -.clear-inherit { clear: inherit; } -.clear-left { clear: left; } -.clear-none { clear: none; } -.clear-right { clear: right; } - -/* color */ - -.color-#000, -.color-black { color: black; } -.color-\#fff, -.color-white { color: white; } -.color-inherit { color: inherit; } -.color-transparent { color: transparent; } - -/* cursor */ - -.cursor-default { cursor: default; } -.cursor-pointer { cursor: pointer; } - -/* direction */ - -.direction-inherit { direction: inherit; } -.direction-ltr { direction: ltr; } -.direction-rtl { direction: rtl; } - -/* display */ - -.display-block { display: block; } -.display-contents { display: contents; } -.display-flex { display: flex; } -.display-grid { display: grid; } -.display-inherit { display: inherit; } -.display-initial { display: initial; } -.display-inline { display: inline; } -.display-inline-block { display: inline-block; } -.display-inline-flex { display: inline-flex; } -.display-inline-grid { display: inline-grid; } -.display-inline-table { display: inline-table; } -.display-list-item { display: list-item; } -.display-none { display: none; } -.display-table { display: table; } -.display-table-cell { display: table-cell; } -.display-table-column { display: table-column; } -.display-table-column-group { display: table-column-group; } -.display-table-footer-group { display: table-footer-group; } -.display-table-header-group { display: table-header-group; } -.display-table-row { display: table-row; } -.display-table-row-group { display: table-row-group; } -.display-unset { display: unset; } - -/* flex-basis */ - -.flexBasis-0 { flex-basis: 0%; } -.flexBasis-auto { flex-basis: auto; } - -/* flex-direction */ - -.flexDirection-column { flex-direction: column; } -.flexDirection-column-reverse { flex-direction: column-reverse; } -.flexDirection-row { flex-direction: row; } -.flexDirection-row-reverse { flex-direction: row-reverse; } - -/* flex-grow */ - -.flexGrow-0 { flex-grow: 0; } -.flexGrow-1 { flex-grow: 1; } -.flexGrow-2 { flex-grow: 2; } -.flexGrow-3 { flex-grow: 3; } -.flexGrow-4 { flex-grow: 4; } -.flexGrow-5 { flex-grow: 5; } -.flexGrow-6 { flex-grow: 6; } -.flexGrow-7 { flex-grow: 7; } -.flexGrow-8 { flex-grow: 8; } -.flexGrow-9 { flex-grow: 9; } -.flexGrow-10 { flex-grow: 10; } -.flexGrow-11 { flex-grow: 11; } - -/* flex-shrink */ - -.flexShrink-0 { flex-shrink: 0; } -.flexShrink-1 { flex-shrink: 1; } -.flexShrink-2 { flex-shrink: 2; } -.flexShrink-3 { flex-shrink: 3; } -.flexShrink-4 { flex-shrink: 4; } -.flexShrink-5 { flex-shrink: 5; } -.flexShrink-6 { flex-shrink: 6; } -.flexShrink-7 { flex-shrink: 7; } -.flexShrink-8 { flex-shrink: 8; } -.flexShrink-9 { flex-shrink: 9; } -.flexShrink-10 { flex-shrink: 10; } -.flexShrink-11 { flex-shrink: 11; } - -/* flex-wrap */ - -.flexWrap-nowrap { flex-wrap: nowrap; } -.flexWrap-wrap { flex-wrap: wrap; } -.flexWrap-wrap-reverse { flex-wrap: wrap-reverse; } - -/* float */ - -.float-left { float: left; } -.float-none { float: none; } -.float-right { float: right; } - -/* font */ - -.font-inherit { font: inherit; } - -/* font-family */ - -.fontFamily-inherit { font-family: inherit; } -.fontFamily-monospace { font-family: monospace; } -.fontFamily-sans-serif { font-family: sans-serif; } -.fontFamily-serif { font-family: serif; } - -/* font-size */ - -.fontSize-100\% { font-size: 100%; } -.fontSize-inherit { font-size: inherit; } - -/* font-style */ - -.fontStyle-inherit { font-style: inherit; } -.fontStyle-italic { font-style: italic; } -.fontStyle-normal { font-style: normal; } -.fontStyle-oblique { font-style: oblique; } - -/* font-weight */ - -.fontWeight-100 { font-weight: 100; } -.fontWeight-200 { font-weight: 200; } -.fontWeight-300 { font-weight: 300; } -.fontWeight-400 { font-weight: 400; } -.fontWeight-500 { font-weight: 500; } -.fontWeight-600 { font-weight: 600; } -.fontWeight-700 { font-weight: 700; } -.fontWeight-800 { font-weight: 800; } -.fontWeight-900 { font-weight: 900; } -.fontWeight-bold { font-weight: bold; } -.fontWeight-bolder { font-weight: bolder; } -.fontWeight-inherit { font-weight: inherit; } -.fontWeight-lighter { font-weight: lighter; } -.fontWeight-normal { font-weight: normal; } - -/* height */ - -.height-0 { height: 0; } -.height-10\% { height: 10%; } -.height-12\.5\% { height: 12.5%; } -.height-20\% { height: 20%; } -.height-25\% { height: 25%; } -.height-30\% { height: 30%; } -.height-37\.5\% { height: 37.5%; } -.height-40\% { height: 40%; } -.height-50\% { height: 50%; } -.height-60\% { height: 60%; } -.height-62\.5\% { height: 62.5%; } -.height-70\% { height: 70%; } -.height-75\% { height: 75%; } -.height-80\% { height: 80%; } -.height-87\.5\% { height: 87.5%; } -.height-90\% { height: 90%; } -.height-100\% { height: 100%; } - -/* justify-content */ - -.justifyContent-center { justify-content: center; } -.justifyContent-flex-end { justify-content: flex-end; } -.justifyContent-flex-start { justify-content: flex-start; } -.justifyContent-space-around { justify-content: space-around; } -.justifyContent-space-between { justify-content: space-between; } - -/* left */ - -.left-0 { left: 0; } -.left-50% { left: 50%; } -.left-100% { left: 100%; } - -/* line-height */ - -.lineHeight-inherit { line-height: inherit; } -.lineHeight-normal { line-height: normal; } - -/* list-style */ - -.listStyle-none { list-style: none; } - -/* margin */ - -.margin-0 { margin: 0; } -.margin-auto { margin: auto; } - -/* margin-bottom */ - -.marginBottom-auto { margin-bottom: auto; } -.marginBottom-0 { margin-bottom: 0; } -.marginBottom-1em { margin-bottom: 1em; } -.marginBottom-1rem { margin-bottom: 1rem; } - -/* margin-left */ - -.marginLeft-auto { margin-left: auto; } -.marginLeft-0 { margin-left: 0; } -.marginLeft-1em { margin-left: 1em; } -.marginLeft-1rem { margin-left: 1rem; } - -/* margin-right */ - -.marginRight-auto { margin-right: auto; } -.marginRight-0 { margin-right: 0; } -.marginRight-1em { margin-right: 1em; } -.marginRight-1rem { margin-right: 1rem; } - -/* margin-top */ - -.marginTop-auto { margin-top: auto; } -.marginTop-0 { margin-top: 0; } -.marginTop-1em { margin-top: 1em; } -.marginTop-1rem { margin-top: 1rem; } - -/* max-height */ - -.maxHeight-100\% { max-height: 100%; } - -/* max-width */ - -.maxWidth-100\% { max-width: 100%; } - -/* min-height */ - -.minHeight-100\% { min-height: 100%; } - -/* min-width */ - -.minWidth-100\% { min-width: 100%; } - -/* opacity */ - -.opacity-0 { opacity: 0; } -.opacity-0\.1 { opacity: 0.1; } -.opacity-0\.2 { opacity: 0.2; } -.opacity-0\.25 { opacity: 0.25; } -.opacity-0\.3 { opacity: 0.3; } -.opacity-0\.4 { opacity: 0.4; } -.opacity-0\.5 { opacity: 0.5; } -.opacity-0\.6 { opacity: 0.6; } -.opacity-0\.7 { opacity: 0.7; } -.opacity-0\.75 { opacity: 0.75; } -.opacity-0\.8 { opacity: 0.8; } -.opacity-0\.9 { opacity: 0.9; } -.opacity-1 { opacity: 1; } - -/* order */ - -.order--1 { order: -1; } -.order-1 { order: 1; } -.order-2 { order: 2; } -.order-3 { order: 3; } -.order-4 { order: 4; } -.order-5 { order: 5; } -.order-6 { order: 6; } -.order-7 { order: 7; } -.order-8 { order: 8; } -.order-9 { order: 9; } -.order-10 { order: 10; } -.order-11 { order: 11; } - -/* overflow */ - -.overflow-auto { overflow: auto; } -.overflow-hidden { overflow: hidden; } -.overflow-inherit { overflow: inherit; } -.overflow-scroll { overflow: scroll; } -.overflow-visible { overflow: visible; } - -/* overflow-x */ - -.overflowX-auto { overflow-x: auto; } -.overflowX-hidden { overflow-x: hidden; } -.overflowX-inherit { overflow-x: inherit; } -.overflowX-scroll { overflow-x: scroll; } -.overflowX-visible { overflow-x: visible; } - -/* overflow-y */ - -.overflowY-auto { overflow-y: auto; } -.overflowY-hidden { overflow-y: hidden; } -.overflowY-inherit { overflow-y: inherit; } -.overflowY-scroll { overflow-y: scroll; } -.overflowY-visible { overflow-y: visible; } - -/* padding */ - -.padding-0 { padding: 0; } -.padding-1em { padding: 1em; } -.padding-1rem { padding: 1rem; } - -/* padding-bottom */ - -.paddingBottom-0 { padding-bottom: 0; } -.paddingBottom-1em { padding-bottom: 1em; } -.paddingBottom-1rem { padding-bottom: 1rem; } - -/* padding-left */ - -.paddingLeft-0 { padding-left: 0; } -.paddingLeft-1em { padding-left: 1em; } -.paddingLeft-1rem { padding-left: 1rem; } - -/* padding-right */ - -.paddingRight-0 { padding-right: 0; } -.paddingRight-1em { padding-right: 1em; } -.paddingRight-1rem { padding-right: 1rem; } - -/* padding-top */ - -.paddingTop-0 { padding-top: 0; } -.paddingTop-1em { padding-top: 1em; } -.paddingTop-1rem { padding-top: 1rem; } - -/* pointer-events */ - -.pointerEvents-auto { pointer-events: auto; } -.pointerEvents-none { pointer-events: none; } -.pointerEvents-box-none { pointer-events: none; } -.pointerEvents-box-none * { pointer-events: auto;} -.pointerEvents-box-only { pointer-events: auto; } -.pointerEvents-box-only * { pointer-events: none; } - -/* position */ - -.position-absolute { position: absolute; } -.position-fixed { position: fixed; } -.position-relative { position: relative; } - -/* right */ - -.right-0 { right: 0; } -.right-50% { right: 50%; } -.right-100% { right: 100%; } - -/* text-align */ - -.textAlign-center { text-align: center; } -.textAlign-end { text-align: end; } -.textAlign-inherit { text-align: inherit; } -.textAlign-left { text-align: left; } -.textAlign-right { text-align: right; } -.textAlign-justify { text-align: justify; } -.textAlign-start { text-align: start; } - -/* text-decoration */ - -.textDecoration-inherit { text-decoration: inherit; } -.textDecoration-none { text-decoration: none; } -.textDecoration-underline { text-decoration: underline; } - -/* text-overflow */ - -.textOverflow-clip { text-overflow: clip; } -.textOverflow-ellipsis { text-overflow: ellipsis; } - -/* text-rendering */ - -.textRendering-auto { text-rendering: auto; } -.textRendering-geometricPrecision { text-rendering: geometricPrecision; } -.textRendering-inherit { text-rendering: inherit; } -.textRendering-optimizeLegibility { text-rendering: optimizeLegibility; } -.textRendering-optimizeSpeed { text-rendering: optimizeSpeed; } - -/* text-transform */ - -.textTransform-capitalize { text-transform: capitalize; } -.textTransform-lowercase { text-transform: lowercase; } -.textTransform-none { text-transform: none; } -.textTransform-uppercase { text-transform: uppercase; } - -/* top */ - -.top-0 { top: 0; } -.top-50% { top: 50%; } -.top-100% { top: 100%; } - -/* unicode-bidi */ - -.unicodeBidi-bidi-override { unicode-bidi: bidi-override; } -.unicodeBidi-embed { unicode-bidi: embed; } -.unicodeBidi-inherit { unicode-bidi: inherit; } -.unicodeBidi-isolate { unicode-bidi: isolate; } -.unicodeBidi-isolate-override { unicode-bidi: isolate-override; } -.unicodeBidi-normal { unicode-bidi: normal; } -.unicodeBidi-plaintext { unicode-bidi: plaintext; } - -/* user-select */ - -.userSelect-all { user-select: all; } -.userSelect-inherit { user-select: inherit; } -.userSelect-none { user-select: none; } -.userSelect-text { user-select: text; } - -/* visibility */ - -.visibility-collapse { visibility: collapse; } -.visibility-hidden { visibility: hidden; } -.visibility-inherit { visibility: inherit; } -.visibility-visible { visibility: visible; } - -/* white-space */ - -.whiteSpace-normal { white-space: normal; } -.whiteSpace-nowrap { white-space: nowrap; } -.whiteSpace-pre { white-space: pre; } -.whiteSpace-pre-line { white-space: pre-line; } -.whiteSpace-pre-wrap { white-space: pre-wrap; } - -/* width */ - -.width-0 { width: 0; } -.width-10\% { width: 10%; } -.width-12\.5\% { width: 12.5%; } -.width-20\% { width: 20%; } -.width-25\% { width: 25%; } -.width-30\% { width: 30%; } -.width-37\.5\% { width: 37.5%; } -.width-40\% { width: 40%; } -.width-50\% { width: 50%; } -.width-60\% { width: 60%; } -.width-62\.5\% { width: 62.5%; } -.width-70\% { width: 70%; } -.width-75\% { width: 75%; } -.width-80\% { width: 80%; } -.width-87\.5\% { width: 87.5%; } -.width-90\% { width: 90%; } -.width-100\% { width: 100%; } - -/* word-wrap */ - -.wordWrap-break-word { word-wrap: break-word; } -.wordWrap-normal { word-wrap: normal; } - -/* z-index */ - -.zIndex--1 { z-index: -1; } -.zIndex-0 { z-index: 0; } -.zIndex-1 { z-index: 1; } -.zIndex-2 { z-index: 2; } -.zIndex-3 { z-index: 3; } -.zIndex-4 { z-index: 4; } -.zIndex-5 { z-index: 5; } -.zIndex-6 { z-index: 6; } -.zIndex-7 { z-index: 7; } -.zIndex-8 { z-index: 8; } -.zIndex-9 { z-index: 9; } -.zIndex-10 { z-index: 10; }