diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..c6dc7972 --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "optional": [ + "runtime" + ], + "stage": 1 +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..5206c6f3 --- /dev/null +++ b/README.md @@ -0,0 +1,327 @@ +# react-web-sdk + +**Experimental / API proof of concept** + +Components for building web applications and SDKs. Based on `react-native`'s +components. + +1. Styles are plain JavaScript objects. +2. Styles are passed to a component's `style` prop. +3. Non-dynamic styles rely on static CSS classes. +4. Dynamic styles are written as inline styles on the DOM node. + +Each Component defines `StylePropTypes` and filters out any style properties +that are not part of its style API. For example, `View` does not support any +typographic properties. You must use `Text` to wrap and style any text strings. + +### Implementation notes + +The current implementation uses a prebuilt CSS library – 200+ single-purpose, +obfuscated selectors. It provides a large number of common, knowable styles +needed to build apps. This makes the foundational CSS fixed (~5KB gzipped) and +highly cachable. Inline styles are used for anything the CSS library doesn't +provide, but their use is significantly reduced. + +A better implementation would generate the CSS library from the declarations +defined in the source code, replace static variables (use webpack's +`DefinePlugin`?), and only use inline-styles for dynamic, instance-specific +styles. + +What about media queries? Perhaps rely on `mediaMatch` and re-render when the +`style` prop needs to change; works better with DOM-related changes that need +to happen at breakpoints. And that would avoid the need for any additional +CSS. Alternatively, rely on something similar to `react-style`'s approach and +duplicate the single-purpose classes within static CSS media queries. + +## Example + +```js +import React from 'react'; +import {Image, Text, View} from 'react-web-sdk'; + +class Example extends React.Component { + render() { + return ( + + accessibility text + 0.5 && style.text) }}> + Example component + + + ); + } +} + +const style = { + common: { + backgroundColor: 'white', + borderRadius: '1em' + }, + root: { + flexDirection: 'row', + justifyContent: 'space-between' + }, + image: { + opacity: 0.5 + }, + text: { + fontWeight: '300' + } +}; + +export default Example; +``` + +## Component: `SDKComponent` + +A component that manages styles across the `className` and `style` properties. +The building block upon which all other components in `react-web-sdk` are +build. + +### PropTypes + +All other props are transferred directly to the `element`. + ++ `className`: `string` ++ `element`: `func` or `string` ++ `style`: `object` + +### Examples + +```js +const ViewStylePropTypes = { + opacity: PropTypes.number +}; + +const ViewStyleDefaultProps = { + opacity: 1 +}; + +class ViewExample extends React.Component { + render() { + // filter other props + const other = ReactWebSDK.getOtherProps(this); + // exclude other styles + const supportedStyle = ReactWebSDK.objectWithProps(this.props.style, ViewStylePropTypes); + // merge defaults with instance styles + const style = { ...ViewStyleDefaultProps, ...supportedStyle } + + return ( + + ); + } +} +``` + + +## Component: `Image` + +TODO + +### PropTypes + +All other props are transferred directly to the `element`. + ++ `alt`: `string` ++ `async`: `bool` (TODO) ++ `className`: `string` ++ `src`: `string` ++ `style`: `ImageStylePropTypes` + +### ImageStylePropTypes + ++ `BackgroundPropTypes` ++ `BorderThemePropTypes` ++ `LayoutPropTypes` ++ `opacity`: `string` + + +## Component: `Text` + +Text layout and styles. + +### PropTypes + +All other props are transferred directly to the `element`. + ++ `className`: `string` ++ `element`: `func` or `string` (default `"div"`) ++ `style`: `TextStylePropTypes` + +### TextStylePropTypes + ++ ViewStylePropTypes ++ TypographicPropTypes + + +## Component: `View` + +`View` is a flexbox container and the fundamental building block for UI. It is +designed to be nested inside other `View`'s and to have 0-to-many children of +any type. + +### PropTypes + +All other props are transferred directly to the `element`. + ++ `className`: `string` ++ `element`: `func` or `string` (default `"div"`) ++ `pointerEvents`: `oneOf('all', 'box-only', 'box-none', 'none')` ++ `style`: `ViewStylePropTypes` + +### ViewStylePropTypes + ++ BackgroundPropTypes ++ BorderThemePropTypes ++ LayoutPropTypes ++ `boxShadow`: `string` ++ `color`: `string` ++ `opacity`: `number` + +### ViewStyleDefaultProps + +Implements the default styles from +[facebook/css-layout](https://github.com/facebook/css-layout). + +```js +const ViewStyleDefaultProps = { + alignItems: 'stretch', // 1 + borderWidth: 0, + borderStyle: 'solid', + boxSizing: 'border-box', // 2 + display: 'flex', // 3 + flexBasis: 'auto', // 1 + flexDirection: 'column', // 1 + flexShrink: 0, // 1 + listStyle: 'none', + margin: 0, + padding: 0, + position: 'relative' // 4 +}; +``` + +1. All the flex elements are oriented from top-to-bottom, left-to-right and do + not shrink. This is how things are laid out using the default CSS settings + and what you'd expect. + +2. The most convenient way to express the relation between width and other + box-model properties. + +3. Everything is `display:flex` by default. All the behaviors of `block` and + `inline-block` can be expressed in term of flex but not the opposite. + +4. Everything is `position:relative`. This makes `position:absolute` target the + direct parent and not some parent which is either relative or absolute. If + you want to position an element relative to something else, you should move + it in the DOM instead of relying of CSS. It also makes `top`, `left`, + `right`, `bottom` do something when not specifying `position:absolute`. + +### Examples + +```js +// TODO +``` + +## StylePropTypes + +### Background + ++ `backgroundColor`: `string` ++ `backgroundImage`: `string` ++ `backgroundPosition`: `string` ++ `backgroundRepeat`: `string` ++ `backgroundSize`: `string` + +### BorderTheme + ++ `borderColor`: `string` ++ `borderTopColor`: `string` ++ `borderRightColor`: `string` ++ `borderBottomColor`: `string` ++ `borderLeftColor`: `string` ++ `borderStyle`: `string` ++ `borderRadius`: `number` or `string` ++ `borderTopLeftRadius`: `number` or `string` ++ `borderTopRightRadius`: `number` or `string` ++ `borderBottomLeftRadius`: `number` or `string` ++ `borderBottomRightRadius`: `number` or `string` + +### BoxModel + ++ `borderWidth`: `number` or `string` ++ `borderTopWidth`: `number` or `string` ++ `borderRightWidth`: `number` or `string` ++ `borderBottomWidth`: `number` or `string` ++ `borderLeftWidth`: `number` or `string` ++ `boxSizing`: `oneOf('border-box', 'content-box')` ++ `display`: `oneOf('block', 'flex', 'inline', 'inline-block', 'inline-flex')` ++ `height`: `number` or `string` ++ `margin`: `number` or `string` ++ `marginTop`: `number` or `string` ++ `marginRight`: `number` or `string` ++ `marginBottom`: `number` or `string` ++ `marginLeft`: `number` or `string` ++ `padding`: `number` or `string` ++ `paddingTop`: `number` or `string` ++ `paddingRight`: `number` or `string` ++ `paddingBottom`: `number` or `string` ++ `paddingLeft`: `number` or `string` ++ `width`: `number` or `string` + +### Flexbox + +* `alignContent`: `oneOf('center', 'flex-end', 'flex-start', 'stretch', 'space-around', 'space-between')` +* `alignItems`: `oneOf('baseline', 'center', 'flex-end', 'flex-start', 'stretch')` +* `alignSelf`: `oneOf('auto', 'baseline', 'center', 'flex-end', 'flex-start', 'stretch')` +* `flexBasis`: `string` +* `flexDirection`: `oneOf('column', 'column-reverse', 'row', 'row-reverse')` +* `flexGrow`: `number` +* `flexShrink`: `number` +* `flexWrap`: `oneOf('nowrap', 'wrap', 'wrap-reverse')` +* `justifyContent`: `oneOf('center', 'flex-end', 'flex-start', 'space-around', 'space-between')` +* `order`: `number` + +See this [guide to flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/). + +### Layout + +* BoxModel +* Flexbox +* Position + +### Position + +* `position`: `oneOf('absolute', 'fixed', 'relative')` +* `bottom`: `number` or `string` +* `left`: `number` or `string` +* `right`: `number` or `string` +* `top`: `number` or `string` +* `zIndex`: `number` + +### Typographic + +* `direction`: `oneOf('auto', 'ltr', 'rtl')` +* `fontFamily`: `string` +* `fontSize`: `string` +* `fontWeight`: `oneOf('100', '200', '300', '400', '500', '600', '700', '800', '900', 'bold', 'normal')` +* `fontStyle`: `oneOf('normal', 'italic')` +* `letterSpacing`: `string` +* `lineHeight`: `number` or `string` +* `textAlign`: `oneOf('auto', 'left', 'right', 'center')` +* `textDecoration`: `oneOf('none', 'underline')` +* `textTransform`: `oneOf('capitalize', 'lowercase', 'none', 'uppercase')` +* `wordWrap`: `oneOf('break-word', 'normal')` + +## Development + +``` +npm run build +npm run build:watch +open index.html +``` diff --git a/example.js b/example.js new file mode 100644 index 00000000..2b155732 --- /dev/null +++ b/example.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { Image, Text, View } from '.'; + +class Example extends React.Component { + render() { + return ( + + {[1,2,3,4,5,6].map((item) => { + return ( + + {item} + + ); + })} + + ); + } +} + +const style = { + root: { + flexDirection: 'row', + flexWrap: 'wrap', + height: '100px' + }, + box: { + alignItems: 'center', + backgroundColor: 'lightblue', + flexGrow: 1, + justifyContent: 'center', + borderColor: 'blue', + borderWidth: '5px' + }, + boxFull: { + width: '100%' + } +} + +React.render(, document.getElementById('react-root')); diff --git a/index.html b/index.html new file mode 100644 index 00000000..a4ba6afe --- /dev/null +++ b/index.html @@ -0,0 +1,4 @@ + + +
+ diff --git a/index.js b/index.js new file mode 100644 index 00000000..283ef7c1 --- /dev/null +++ b/index.js @@ -0,0 +1,14 @@ +import getOtherProps, {objectWithProps} from './lib/getOtherProps'; +import Image from './lib/components/Image'; +import SDKComponent from './lib/components/SDKComponent'; +import Text from './lib/components/Text'; +import View from './lib/components/View'; + +export default { + getOtherProps, + Image, + objectWithProps, + SDKComponent, + Text, + View +}; diff --git a/lib/.gitkeep b/lib/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/lib/StylePropTypes/Background.js b/lib/StylePropTypes/Background.js new file mode 100644 index 00000000..c4d3a313 --- /dev/null +++ b/lib/StylePropTypes/Background.js @@ -0,0 +1,9 @@ +import {PropTypes} from 'react'; + +export default { + backgroundColor: PropTypes.string, + backgroundImage: PropTypes.string, + backgroundPosition: PropTypes.string, + backgroundRepeat: PropTypes.string, + backgroundSize: PropTypes.string +}; diff --git a/lib/StylePropTypes/BorderTheme.js b/lib/StylePropTypes/BorderTheme.js new file mode 100644 index 00000000..09446508 --- /dev/null +++ b/lib/StylePropTypes/BorderTheme.js @@ -0,0 +1,28 @@ +import {PropTypes} from 'react'; + +export default { + // border-color + borderColor: PropTypes.string, + borderTopColor: PropTypes.string, + borderRightColor: PropTypes.string, + borderBottomColor: PropTypes.string, + borderLeftColor: PropTypes.string, + // border-style + borderStyle: PropTypes.string, + // border-radius + borderRadius: PropTypes.oneOfType([ + PropTypes.number, PropTypes.string + ]), + borderTopLeftRadius: PropTypes.oneOfType([ + PropTypes.number, PropTypes.string + ]), + borderTopRightRadius: PropTypes.oneOfType([ + PropTypes.number, PropTypes.string + ]), + borderBottomLeftRadius: PropTypes.oneOfType([ + PropTypes.number, PropTypes.string + ]), + borderBottomRightRadius: PropTypes.oneOfType([ + PropTypes.number, PropTypes.string + ]) +}; diff --git a/lib/StylePropTypes/BoxModel.js b/lib/StylePropTypes/BoxModel.js new file mode 100644 index 00000000..fd989d73 --- /dev/null +++ b/lib/StylePropTypes/BoxModel.js @@ -0,0 +1,71 @@ +import {PropTypes} from 'react'; + +export default { + boxSizing: PropTypes.oneOf([ + 'border-box', + 'content-box' + ]), + // display + display: PropTypes.oneOf([ + 'block', + 'flex', + 'inline', + 'inline-block', + 'inline-flex' + ]), + // dimensions + height: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + width: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + // border width + borderWidth: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + borderTopWidth: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + borderRightWidth: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + borderBottomWidth: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + borderLeftWidth: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + // margin + margin: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + marginTop: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + marginBottom: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + marginLeft: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + marginRight: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + // padding + padding: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + paddingTop: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + paddingBottom: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + paddingLeft: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]), + paddingRight: PropTypes.oneOfType([ + PropTypes.string, PropTypes.number + ]) +}; diff --git a/lib/StylePropTypes/Flexbox.js b/lib/StylePropTypes/Flexbox.js new file mode 100644 index 00000000..a95718c9 --- /dev/null +++ b/lib/StylePropTypes/Flexbox.js @@ -0,0 +1,49 @@ +import {PropTypes} from 'react'; + +export default { + alignContent: PropTypes.oneOf([ + 'center', + 'flex-end', + 'flex-start', + 'stretch', + 'space-around', + 'space-between' + ]), + alignItems: PropTypes.oneOf([ + 'baseline', + 'center', + 'flex-end', + 'flex-start', + 'stretch' + ]), + alignSelf: PropTypes.oneOf([ + 'auto', + 'baseline', + 'center', + 'flex-end', + 'flex-start', + 'stretch' + ]), + flexBasis: PropTypes.string, + flexDirection: PropTypes.oneOf([ + 'column', + 'column-reverse', + 'row', + 'row-reverse' + ]), + flexGrow: PropTypes.number, + flexShrink: PropTypes.number, + flexWrap: PropTypes.oneOf([ + 'nowrap', + 'wrap', + 'wrap-reverse' + ]), + justifyContent: PropTypes.oneOf([ + 'center', + 'flex-end', + 'flex-start', + 'space-around', + 'space-between' + ]), + order: PropTypes.number +}; diff --git a/lib/StylePropTypes/Layout.js b/lib/StylePropTypes/Layout.js new file mode 100644 index 00000000..b2aebf7e --- /dev/null +++ b/lib/StylePropTypes/Layout.js @@ -0,0 +1,9 @@ +import BoxModel from './BoxModel'; +import Flexbox from './Flexbox'; +import Position from './Position'; + +export default { + ...BoxModel, + ...Flexbox, + ...Position +}; diff --git a/lib/StylePropTypes/Position.js b/lib/StylePropTypes/Position.js new file mode 100644 index 00000000..f984ecb6 --- /dev/null +++ b/lib/StylePropTypes/Position.js @@ -0,0 +1,14 @@ +import {PropTypes} from 'react'; + +export default { + position: PropTypes.oneOf([ + 'absolute', + 'fixed', + 'relative' + ]), + bottom: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), + left: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), + right: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), + top: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), + zIndex: PropTypes.number +}; diff --git a/lib/StylePropTypes/Typographic.js b/lib/StylePropTypes/Typographic.js new file mode 100644 index 00000000..ed19e6ae --- /dev/null +++ b/lib/StylePropTypes/Typographic.js @@ -0,0 +1,30 @@ +import {PropTypes} from 'react'; + +export default { + direction: PropTypes.oneOf([ + 'auto', 'ltr', 'rtl' + ]), + fontFamily: PropTypes.string, + fontSize: PropTypes.string, + fontWeight: PropTypes.oneOf([ + '100', '200', '300', '400', '500', '600', '700', '800', '900', + 'bold', 'normal' + ]), + fontStyle: PropTypes.oneOf([ + 'normal', 'italic' + ]), + letterSpacing: PropTypes.string, + lineHeight: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]), + textAlign: PropTypes.oneOf([ + 'auto', 'left', 'right', 'center' + ]), + textDecoration: PropTypes.oneOf([ + 'none', 'underline' + ]), + textTransform: PropTypes.oneOf([ + 'capitalize', 'lowercase', 'none', 'uppercase' + ]), + wordWrap: PropTypes.oneOf([ + 'break-word', 'normal' + ]) +}; diff --git a/lib/StylePropTypes/index.js b/lib/StylePropTypes/index.js new file mode 100644 index 00000000..accdfa81 --- /dev/null +++ b/lib/StylePropTypes/index.js @@ -0,0 +1,13 @@ +import BackgroundPropTypes from './Background'; +import BorderThemePropTypes from './BorderTheme'; +import FlexboxPropTypes from './Flexbox'; +import LayoutPropTypes from './Layout'; +import TypographicPropTypes from './Typographic'; + +export default { + BackgroundPropTypes, + BorderThemePropTypes, + FlexboxPropTypes, + LayoutPropTypes, + TypographicPropTypes +}; diff --git a/lib/autoprefix.js b/lib/autoprefix.js new file mode 100644 index 00000000..930d9a7a --- /dev/null +++ b/lib/autoprefix.js @@ -0,0 +1,47 @@ +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/lib/components/Image.js b/lib/components/Image.js new file mode 100644 index 00000000..bb755cbd --- /dev/null +++ b/lib/components/Image.js @@ -0,0 +1,61 @@ +import {objectWithProps} from '../getOtherProps'; +import React, {PropTypes} from 'react'; +import StylePropTypes from '../StylePropTypes'; +import SDKComponent from './SDKComponent'; + +const ImageStyleDefaultProps = { + backgroundColor: 'lightGray', + borderWidth: 0, + maxWidth: '100%' +}; + +const ImageStylePropTypes = { + ...StylePropTypes.BorderThemePropTypes, + ...StylePropTypes.LayoutPropTypes, + backgroundColor: PropTypes.string, + opacity: PropTypes.string +}; + +class Image extends React.Component { + static _getPropTypes() { + return { + alt: PropTypes.string, + async: PropTypes.bool, + className: PropTypes.string, + src: PropTypes.string, + style: PropTypes.shape(ImageStylePropTypes) + }; + } + + static _getDefaultProps() { + return { + async: true, + className: '', + src: 'data:image/gif;base64,' + + 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', + style: {} + }; + } + + render() { + const { alt, className, src, style, ...other } = this.props; + const filteredStyle = objectWithProps(style, Object.keys(ImageStylePropTypes)); + const mergedStyle = { ...ImageStyleDefaultProps, ...filteredStyle }; + + return ( + + ); + } +} + +Image.propTypes = Image._getPropTypes(); +Image.defaultProps = Image._getDefaultProps(); + +export default Image; diff --git a/lib/components/SDKComponent.js b/lib/components/SDKComponent.js new file mode 100644 index 00000000..c0f83fae --- /dev/null +++ b/lib/components/SDKComponent.js @@ -0,0 +1,74 @@ +import autoprefix from '../autoprefix'; +import getOtherProps from '../getOtherProps'; +import React, {PropTypes} from 'react'; + +// styles +import styleMap from '../styleMap'; + +class SDKComponent extends React.Component { + static _getPropTypes() { + return { + className: PropTypes.string, + element: PropTypes.oneOfType([ + PropTypes.string, PropTypes.func + ]), + style: PropTypes.object + }; + } + + static _getDefaultProps() { + return { + className: '', + element: 'div', + style: {} + }; + } + + render() { + const other = getOtherProps(this); + const { classNames, inlineStyles } = this._separateClassNamesAndStyles(); + + return ( + + ); + } + + _getSinglePurposeClassName(prop, style) { + const uniqueClassName = `${prop}-${style[prop]}`; + if ( + style.hasOwnProperty(prop) && + styleMap[uniqueClassName] + ) { + return styleMap[uniqueClassName]; + } + } + + _separateClassNamesAndStyles() { + const classNameProp = this.props.className; + const styleProp = this.props.style; + + let classNames = [ classNameProp ]; + let inlineStyles = {}; + + for (let prop in styleProp) { + let singlePurposeClassName = + this._getSinglePurposeClassName(prop, styleProp); + if (singlePurposeClassName) { + classNames.push(singlePurposeClassName); + } else { + inlineStyles[prop] = styleProp[prop]; + } + } + + return { classNames, inlineStyles }; + } +} + +SDKComponent.propTypes = SDKComponent._getPropTypes(); +SDKComponent.defaultProps = SDKComponent._getDefaultProps(); + +export default SDKComponent; diff --git a/lib/components/Text.js b/lib/components/Text.js new file mode 100644 index 00000000..f9b104be --- /dev/null +++ b/lib/components/Text.js @@ -0,0 +1,65 @@ +import {objectWithProps} from '../getOtherProps'; +import React, {PropTypes} from 'react'; +import StylePropTypes from '../StylePropTypes'; +import SDKComponent from './SDKComponent'; +import {ViewStylePropTypes} from './View'; + +const TextStyleDefaultProps = { + alignItems: 'stretch', /* 1 */ + borderWidth: '0', + borderStyle: 'solid', + boxSizing: 'border-box', /* 2 */ + display: 'flex', /* 3 */ + flexBasis: 'auto', /* 1 */ + flexDirection: 'column', /* 1 */ + flexShrink: 0, /* 1 */ + listStyle: 'none', + margin: '0', + padding: '0', + position: 'relative' /* 4 */ +}; + +const TextStylePropTypes = { + ...ViewStylePropTypes, + ...StylePropTypes.TypographicPropTypes +}; + +class Text extends React.Component { + static _getPropTypes() { + return { + className: PropTypes.string, + element: PropTypes.oneOfType([ + PropTypes.string, PropTypes.func + ]), + style: PropTypes.shape(TextStylePropTypes) + }; + } + + static _getDefaultProps() { + return { + className: '', + element: 'div', + style: {} + }; + } + + render() { + const { className, element, style, ...other } = this.props; + const filteredStyle = objectWithProps(style, Object.keys(TextStylePropTypes)); + const mergedStyle = { ...TextStyleDefaultProps, ...filteredStyle }; + + return ( + + ); + } +} + +Text.propTypes = Text._getPropTypes(); +Text.defaultProps = Text._getDefaultProps(); + +export default Text; diff --git a/lib/components/View.js b/lib/components/View.js new file mode 100644 index 00000000..9d978ca9 --- /dev/null +++ b/lib/components/View.js @@ -0,0 +1,81 @@ +import {objectWithProps} from '../getOtherProps'; +import React, {PropTypes} from 'react'; +import StylePropTypes from '../StylePropTypes'; +import SDKComponent from './SDKComponent'; + +// https://github.com/facebook/css-layout#default-values +const ViewStyleDefaultProps = { + alignItems: 'stretch', + borderWidth: 0, + borderStyle: 'solid', + boxSizing: 'border-box', + display: 'flex', + flexBasis: 'auto', + flexDirection: 'column', + flexShrink: 0, + listStyle: 'none', + margin: 0, + padding: 0, + position: 'relative' +}; + +const ViewStylePropTypes = { + ...StylePropTypes.BackgroundPropTypes, + ...StylePropTypes.BorderThemePropTypes, + ...StylePropTypes.LayoutPropTypes, + boxShadow: PropTypes.string, + opacity: PropTypes.number, + transform: PropTypes.string +}; + +class View extends React.Component { + static _getPropTypes() { + return { + className: PropTypes.string, + element: PropTypes.oneOfType([ + PropTypes.string, PropTypes.func + ]), + pointerEvents: PropTypes.oneOf([ + 'auto', + 'box-none', + 'box-only', + 'none' + ]), + style: PropTypes.shape(ViewStylePropTypes) + }; + } + + static _getDefaultProps() { + return { + className: '', + element: 'div', + style: {} + }; + } + + render() { + const { className, element, pointerEvents, style, ...other } = this.props; + const filteredStyle = objectWithProps(style, Object.keys(ViewStylePropTypes)); + const pointerEventsStyle = pointerEvents && { pointerEvents }; + const mergedStyle = { + ...ViewStyleDefaultProps, + ...filteredStyle, + ...pointerEventsStyle + }; + + return ( + + ); + } +} + +View.propTypes = View._getPropTypes(); +View.defaultProps = View._getDefaultProps(); + +export default View; +export {ViewStylePropTypes}; diff --git a/lib/css/.gitkeep b/lib/css/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/lib/css/background.css b/lib/css/background.css new file mode 100644 index 00000000..37ed2fb8 --- /dev/null +++ b/lib/css/background.css @@ -0,0 +1,13 @@ +:local .backgroundColor-#fff { background-color: #fff; } +:local .backgroundColor-transparent { background-color: currentcolor; } + +:local .backgroundImage { background-image: none; } + +:local .backgroundPosition-center { background-position: center; } +:local .backgroundSize-contain { background-size: contain; } +:local .backgroundSize-cover { background-size: cover; } + +:local .backgroundRepeat-no-repeat { background-repeat: no-repeat; } +:local .backgroundRepeat-repeat { background-repeat: repeat; } +:local .backgroundRepeat-repeat-x { background-repeat: repeat-x; } +:local .backgroundRepeat-repeat-y { background-repeat: repeat-y; } diff --git a/lib/css/border.css b/lib/css/border.css new file mode 100644 index 00000000..3be1fffb --- /dev/null +++ b/lib/css/border.css @@ -0,0 +1,42 @@ +:local .borderColor-currentcolor { border-color: currentcolor; } +:local .borderColor-transparent { border-color: transparent; } + +:local .borderStyle-dotted { border-style: dotted; } +:local .borderStyle-dashed { border-style: dashed; } +:local .borderStyle-solid { border-style: solid; } + +:local .borderWidth-0 { border-width: 0; } +:local .borderTopWidth-0 { border-top-width: 0; } +:local .borderRightWidth-0 { border-right-width: 0; } +:local .borderBottomWidth-0 { border-bottom-width: 0; } +:local .borderLeftWidth-0 { border-left-width: 0; } + +:local .borderWidth-1px { border-width: 1px; } +:local .borderTopWidth-1px { border-top-width: 1px; } +:local .borderRightWidth-1px { border-right-width: 1px; } +:local .borderBottomWidth-1px { border-bottom-width: 1px; } +:local .borderLeftWidth-1px { border-left-width: 1px; } + +:local .borderWidth-2px { border-width: 2px; } +:local .borderTopWidth-2px { border-top-width: 2px; } +:local .borderRightWidth-2px { border-right-width: 2px; } +:local .borderBottomWidth-2px { border-bottom-width: 2px; } +:local .borderLeftWidth-2px { border-left-width: 2px; } + +:local .borderWidth-3px { border-width: 3px; } +:local .borderTopWidth-3px { border-top-width: 3px; } +:local .borderRightWidth-3px { border-right-width: 3px; } +:local .borderBottomWidth-3px { border-bottom-width: 3px; } +:local .borderLeftWidth-3px { border-left-width: 3px; } + +:local .borderWidth-4px { border-width: 4px; } +:local .borderTopWidth-4px { border-top-width: 4px; } +:local .borderRightWidth-4px { border-right-width: 4px; } +:local .borderBottomWidth-4px { border-bottom-width: 4px; } +:local .borderLeftWidth-4px { border-left-width: 4px; } + +:local .borderWidth-5px { border-width: 5px; } +:local .borderTopWidth-5px { border-top-width: 5px; } +:local .borderRightWidth-5px { border-right-width: 5px; } +:local .borderBottomWidth-5px { border-bottom-width: 5px; } +:local .borderLeftWidth-5px { border-left-width: 5px; } diff --git a/lib/css/boxSizing.css b/lib/css/boxSizing.css new file mode 100644 index 00000000..e6eb38bf --- /dev/null +++ b/lib/css/boxSizing.css @@ -0,0 +1,2 @@ +:local .boxSizing-border-box { box-sizing: border-box; } +:local .boxSizing-content-box { box-sizing: content-box; } diff --git a/lib/css/clear.css b/lib/css/clear.css new file mode 100644 index 00000000..8ad9a6a9 --- /dev/null +++ b/lib/css/clear.css @@ -0,0 +1,4 @@ +:local .clear-both { clear: both; } +:local .clear-left { clear: left; } +:local .clear-none { clear: none; } +:local .clear-right { clear: right; } diff --git a/lib/css/color.css b/lib/css/color.css new file mode 100644 index 00000000..35472915 --- /dev/null +++ b/lib/css/color.css @@ -0,0 +1,4 @@ +:local .color-\#fff { color: #fff; } +:local .color-\#000 { color: #000; } +:local .color-transparent { color: transparent; } +:local .color-inherit { color: inherit; } diff --git a/lib/css/direction.css b/lib/css/direction.css new file mode 100644 index 00000000..977f2881 --- /dev/null +++ b/lib/css/direction.css @@ -0,0 +1,3 @@ +:local .direction-auto { direction: auto; } +:local .direction-ltr { direction: ltr; } +:local .direction-rtl { direction: rtl; } diff --git a/lib/css/display.css b/lib/css/display.css new file mode 100644 index 00000000..1dff8342 --- /dev/null +++ b/lib/css/display.css @@ -0,0 +1,12 @@ +:local .display-block { display: block; } +:local .display-flex { display: flex; } +:local .display-inline { display: inline; } +:local .display-inline-block { display: inline-block; } +:local .display-inline-flex { display: inline-flex; } +:local .display-inline-table { display: inline-table; } +:local .display-none { display: none; } +:local .display-table { display: table; } +:local .display-table-row { display: table-row; } +:local .display-table-cell { display: table-cell; } +:local .display-table-column { display: table-column; } +:local .display-table-column-group { display: table-column-group; } diff --git a/lib/css/flexbox.css b/lib/css/flexbox.css new file mode 100644 index 00000000..86c92f84 --- /dev/null +++ b/lib/css/flexbox.css @@ -0,0 +1,70 @@ +:local .alignContent-center { align-content: center; } +:local .alignContent-flex-end { align-content: flex-end; } +:local .alignContent-flex-start { align-content: flex-start; } +:local .alignContent-stretch { align-content: stretch; } +:local .alignContent-space-around { align-content: space-around; } +:local .alignContent-space-between { align-content: space-between; } + +:local .alignItems-center { align-items: center; } +:local .alignItems-flex-end { align-items: flex-end; } +:local .alignItems-flex-start { align-items: flex-start; } +:local .alignItems-stretch { align-items: stretch; } +:local .alignItems-space-around { align-items: space-around; } +:local .alignItems-space-between { align-items: space-between; } + +:local .alignSelf-auto { align-self: auto; } +:local .alignSelf-baseline { align-self: baseline; } +:local .alignSelf-center { align-self: center; } +:local .alignSelf-flex-end { align-self: flex-end; } +:local .alignSelf-flex-start { align-self: flex-start; } +:local .alignSelf-stretch { align-self: stretch; } + +:local .flexBasis-0 { flex-basis: 0%; } +:local .flexBasis-auto { flex-basis: auto; } + +:local .flexDirection-column { flex-direction: column; } +:local .flexDirection-column-reverse { flex-direction: column-reverse; } +:local .flexDirection-row { flex-direction: row; } +:local .flexDirection-row-reverse { flex-direction: row-reverse; } + +:local .flexGrow-0 { flex-grow: 0; } +:local .flexGrow-1 { flex-grow: 1; } +:local .flexGrow-2 { flex-grow: 2; } +:local .flexGrow-3 { flex-grow: 3; } +:local .flexGrow-4 { flex-grow: 4; } +:local .flexGrow-5 { flex-grow: 5; } +:local .flexGrow-6 { flex-grow: 6; } +:local .flexGrow-7 { flex-grow: 7; } +:local .flexGrow-8 { flex-grow: 8; } +:local .flexGrow-9 { flex-grow: 9; } + +:local .flexShrink-0 { flex-shrink: 0; } +:local .flexShrink-1 { flex-shrink: 1; } +:local .flexShrink-2 { flex-shrink: 2; } +:local .flexShrink-3 { flex-shrink: 3; } +:local .flexShrink-4 { flex-shrink: 4; } +:local .flexShrink-5 { flex-shrink: 5; } +:local .flexShrink-6 { flex-shrink: 6; } +:local .flexShrink-7 { flex-shrink: 7; } +:local .flexShrink-8 { flex-shrink: 8; } +:local .flexShrink-9 { flex-shrink: 9; } + +:local .flexWrap-nowrap { flex-wrap: nowrap; } +:local .flexWrap-wrap { flex-wrap: wrap; } +:local .flexWrap-wrap-reverse { flex-wrap: wrap-reverse; } + +:local .justifyContent-center { justify-content: center; } +:local .justifyContent-flex-end { justify-content: flex-end; } +:local .justifyContent-flex-start { justify-content: flex-start; } +:local .justifyContent-space-around { justify-content: space-around; } +:local .justifyContent-space-between { justify-content: space-between; } + +:local .order-1 { order: 1; } +:local .order-2 { order: 2; } +:local .order-3 { order: 3; } +:local .order-4 { order: 4; } +:local .order-5 { order: 5; } +:local .order-6 { order: 6; } +:local .order-7 { order: 7; } +:local .order-8 { order: 8; } +:local .order-9 { order: 9; } diff --git a/lib/css/float.css b/lib/css/float.css new file mode 100644 index 00000000..7d668754 --- /dev/null +++ b/lib/css/float.css @@ -0,0 +1,3 @@ +:local .float-left { float: left; } +:local .float-none { float: none; } +:local .float-right { float: right; } diff --git a/lib/css/font.css b/lib/css/font.css new file mode 100644 index 00000000..d8b8e446 --- /dev/null +++ b/lib/css/font.css @@ -0,0 +1,16 @@ +:local .font-inherit { font: inherit; } + +:local .fontStyle-italic { font-style: italic; } +:local .fontStyle-normal { font-style: normal; } + +:local .fontWeight-100 { font-weight: 100; } +:local .fontWeight-200 { font-weight: 200; } +:local .fontWeight-300 { font-weight: 300; } +:local .fontWeight-400 { font-weight: 400; } +:local .fontWeight-500 { font-weight: 500; } +:local .fontWeight-600 { font-weight: 600; } +:local .fontWeight-700 { font-weight: 700; } +:local .fontWeight-800 { font-weight: 800; } +:local .fontWeight-900 { font-weight: 900; } +:local .fontWeight-bold { font-weight: bold; } +:local .fontWeight-normal { font-weight: normal; } diff --git a/lib/css/height.css b/lib/css/height.css new file mode 100644 index 00000000..c1e77d72 --- /dev/null +++ b/lib/css/height.css @@ -0,0 +1,3 @@ +:local .maxHeight-100\% { max-height: 100%; } +:local .minHeight-100\% { min-height: 100%; } +:local .height-100\% { height: 100%; } diff --git a/lib/css/margin.css b/lib/css/margin.css new file mode 100644 index 00000000..cae44d82 --- /dev/null +++ b/lib/css/margin.css @@ -0,0 +1,13 @@ +:local .margin-0 { margin: 0; } +:local .margin-auto { margin: auto; } +:local .marginTop-0 { margin-top: 0; } +:local .marginTop-auto { margin-top: auto; } + +:local .marginRight-0 { margin-right: 0; } +:local .marginRight-auto { margin-right: auto; } + +:local .marginBottom-0 { margin-bottom: 0; } +:local .marginBottom-auto { margin-bottom: auto; } + +:local .marginLeft-0 { margin-left: 0; } +:local .marginLeft-auto { margin-left: auto; } diff --git a/lib/css/overflow.css b/lib/css/overflow.css new file mode 100644 index 00000000..5bd8a1c6 --- /dev/null +++ b/lib/css/overflow.css @@ -0,0 +1,14 @@ +:local .overflow-auto { overflow: auto; } +:local .overflow-hidden { overflow: hidden; } +:local .overflow-scroll { overflow: scroll; } +:local .overflow-visible { overflow: visible; } + +:local .overflowX-auto { overflow-x: auto; } +:local .overflowX-hidden { overflow-x: hidden; } +:local .overflowX-scroll { overflow-x: scroll; } +:local .overflowX-visible { overflow-x: visible; } + +:local .overflowY-auto { overflow-y: auto; } +:local .overflowY-hidden { overflow-y: hidden; } +:local .overflowY-scroll { overflow-y: scroll; } +:local .overflowY-visible { overflow-y: visible; } diff --git a/lib/css/padding.css b/lib/css/padding.css new file mode 100644 index 00000000..a3c6a823 --- /dev/null +++ b/lib/css/padding.css @@ -0,0 +1,17 @@ +:local .padding-0 { padding: 0; } +:local .paddingTop-0 { padding-top: 0; } +:local .paddingRight-0 { padding-right: 0; } +:local .paddingBottom-0 { padding-bottom: 0; } +:local .paddingLeft-0 { padding-left: 0; } + +:local .padding-1em { padding: 1em; } +:local .paddingTop-1em { padding-top: 1em; } +:local .paddingRight-1em { padding-right: 1em; } +:local .paddingBottom-1em { padding-bottom: 1em; } +:local .paddingLeft-1em { padding-left: 1em; } + +:local .padding-1rem { padding: 1rem; } +:local .paddingTop-1rem { padding-top: 1rem; } +:local .paddingRight-1rem { padding-right: 1rem; } +:local .paddingBottom-1rem { padding-bottom: 1rem; } +:local .paddingLeft-1rem { padding-left: 1rem; } diff --git a/lib/css/pointerEvents.css b/lib/css/pointerEvents.css new file mode 100644 index 00000000..3eebef75 --- /dev/null +++ b/lib/css/pointerEvents.css @@ -0,0 +1,6 @@ +:local .pointerEvents-auto { pointer-events: auto; } +:local .pointerEvents-none { pointer-events: none; } +:local .pointerEvents-box-none { pointer-events: none; } +:local .pointerEvents-box-none * { pointer-events: auto;} +:local .pointerEvents-box-only { pointer-events: auto; } +:local .pointerEvents-box-only * { pointer-events: none; } diff --git a/lib/css/position.css b/lib/css/position.css new file mode 100644 index 00000000..680ae4a5 --- /dev/null +++ b/lib/css/position.css @@ -0,0 +1,13 @@ +:local .position-absolute { position: absolute; } +:local .position-fixed { position: fixed; } +:local .position-relative { position: relative; } + +:local .top-0 { top: 0; } +:local .right-0 { right: 0; } +:local .left-0 { left: 0; } +:local .bottom-0 { bottom: 0; } + +:local .top-100\% { top: 100%; } +:local .right-100\% { right: 100%; } +:local .left-100\% { left: 100%; } +:local .bottom-100\% { bottom: 100%; } diff --git a/lib/css/text.css b/lib/css/text.css new file mode 100644 index 00000000..8297842f --- /dev/null +++ b/lib/css/text.css @@ -0,0 +1,14 @@ +:local .textAlign-center { text-align: center; } +:local .textAlign-left { text-align: left; } +:local .textAlign-right { text-align: right; } +:local .textAlign-justify { text-align: justify; } + +:local .textDecoration-none { text-decoration: none; } +:local .textDecoration-underline { text-decoration: underline; } + +:local .textOverflow-ellipsis { text-overflow: ellipsis } + +:local .textTransform-capitalize { text-transform: capitalize; } +:local .textTransform-lowercase { text-transform: lowercase; } +:local .textTransform-none { text-transform: none; } +:local .textTransform-uppercase { text-transform: uppercase; } diff --git a/lib/css/userSelect.css b/lib/css/userSelect.css new file mode 100644 index 00000000..984d95ad --- /dev/null +++ b/lib/css/userSelect.css @@ -0,0 +1,3 @@ +:local .userSelect-all { user-select: all; } +:local .userSelect-inherit { user-select: inherit; } +:local .userSelect-none { user-select: none; } diff --git a/lib/css/visibility.css b/lib/css/visibility.css new file mode 100644 index 00000000..503f317c --- /dev/null +++ b/lib/css/visibility.css @@ -0,0 +1,2 @@ +:local .visibility-hidden { visibility: hidden; } +:local .visibility-visible { visibility: visible; } diff --git a/lib/css/whiteSpace.css b/lib/css/whiteSpace.css new file mode 100644 index 00000000..8ec64086 --- /dev/null +++ b/lib/css/whiteSpace.css @@ -0,0 +1,3 @@ +:local .whiteSpace-normal { white-space: normal; } +:local .whiteSpace-nowrap { white-space: nowrap; } +:local .whiteSpace-pre { white-space: pre; } diff --git a/lib/css/width.css b/lib/css/width.css new file mode 100644 index 00000000..60788def --- /dev/null +++ b/lib/css/width.css @@ -0,0 +1,3 @@ +:local .maxWidth-100\% { max-width: 100%; } +:local .minWidth-100\% { min-width: 100%; } +:local .width-100\% { width: 100%; } diff --git a/lib/css/word.css b/lib/css/word.css new file mode 100644 index 00000000..ac39c258 --- /dev/null +++ b/lib/css/word.css @@ -0,0 +1,2 @@ +:local .wordWrap-break-word { word-wrap: break-word; } +:local .wordWrap-normal { word-wrap: normal; } diff --git a/lib/css/zIndex.css b/lib/css/zIndex.css new file mode 100644 index 00000000..a261fad3 --- /dev/null +++ b/lib/css/zIndex.css @@ -0,0 +1,12 @@ +:local .zIndex--1 { z-index: -1; } +:local .zIndex-0 { z-index: 0; } +:local .zIndex-1 { z-index: 1; } +:local .zIndex-2 { z-index: 2; } +:local .zIndex-3 { z-index: 3; } +:local .zIndex-4 { z-index: 4; } +:local .zIndex-5 { z-index: 5; } +:local .zIndex-6 { z-index: 6; } +:local .zIndex-7 { z-index: 7; } +:local .zIndex-8 { z-index: 8; } +:local .zIndex-9 { z-index: 9; } +:local .zIndex-10 { z-index: 10; } diff --git a/lib/getOtherProps.js b/lib/getOtherProps.js new file mode 100644 index 00000000..a0cd4c6c --- /dev/null +++ b/lib/getOtherProps.js @@ -0,0 +1,37 @@ +/** + * Extract all props that are not part of the React Component's 'propTypes' + */ +export default (componentInstance) => { + const excludedProps = Object.keys(componentInstance.constructor.propTypes); + return objectWithoutProps(componentInstance.props, excludedProps); +}; + +function objectFilterProps(obj, props, excluded=false) { + if (!Array.isArray(props)) { + throw new TypeError('props is not an Array'); + } + + let filtered = {}; + for (let prop in obj) { + if (Object.prototype.hasOwnProperty.call(obj, prop)) { + let isMatch = props.indexOf(prop) > -1; + if (excluded && isMatch) { + continue; + } else if (!excluded && !isMatch) { + continue; + } + + filtered[prop] = obj[prop]; + } + } + + return filtered; +} + +export function objectWithProps(obj, props) { + return objectFilterProps(obj, props); +} + +export function objectWithoutProps(obj, props) { + return objectFilterProps(obj, props, true); +} diff --git a/lib/styleMap.js b/lib/styleMap.js new file mode 100644 index 00000000..e4c5ac75 --- /dev/null +++ b/lib/styleMap.js @@ -0,0 +1,47 @@ +import border from './css/border.css'; +import boxSizing from './css/boxSizing.css'; +import clear from './css/clear.css'; +import color from './css/color.css'; +import cssFloat from './css/float.css'; +import direction from './css/direction.css'; +import display from './css/display.css'; +import flexbox from './css/flexbox.css'; +import font from './css/font.css'; +import height from './css/height.css'; +import margin from './css/margin.css'; +import overflow from './css/overflow.css'; +import padding from './css/padding.css'; +import pointerEvents from './css/pointerEvents.css'; +import position from './css/position.css'; +import text from './css/text.css'; +import userSelect from './css/userSelect.css'; +import visibility from './css/visibility.css'; +import whiteSpace from './css/whiteSpace.css'; +import width from './css/width.css'; +import word from './css/word.css'; + +const map = Object.assign({}, + border, + boxSizing, + clear, + color, + cssFloat, + direction, + display, + flexbox, + font, + height, + margin, + overflow, + padding, + pointerEvents, + position, + text, + userSelect, + visibility, + whiteSpace, + width, + word +); + +export default map; diff --git a/package.json b/package.json index 939647b2..8c307a44 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,28 @@ "version": "0.0.1", "description": "UI SDK based on react-native", "main": "index.js", + "scripts": { + "build": "npm run build:package & npm run build:example", + "build:example": "webpack index.js dist/main.js --config webpack.config.js", + "build:example:watch": "npm run build:example -- --watch", + "build:package": "webpack example.js dist/example.js --config webpack.config.js", + "build:package:watch": "npm run build:package -- --watch", + "build:watch": "npm run build:package:watch & npm run build:example:watch" + }, "author": "Nicolas Gallagher", - "license": "MIT" + "license": "MIT", + "devDependencies": { + "autoprefixer-core": "^5.2.0", + "babel-core": "^5.5.6", + "babel-loader": "^5.1.4", + "babel-runtime": "^5.5.6", + "css-loader": "^0.14.4", + "node-libs-browser": "^0.5.2", + "postcss-loader": "^0.4.4", + "style-loader": "^0.12.3", + "webpack": "^1.9.10" + }, + "dependencies": { + "react": "^0.13.3" + } } diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..5950195f --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,19 @@ +var autoprefixer = require('autoprefixer-core'); + +module.exports = { + module: { + loaders: [ + { + test: /\.css$/, + loader: 'style-loader!css-loader?localIdentName=[hash:base64:5]!postcss-loader' + }, + { + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'babel-loader', + query: { cacheDirectory: true } + } + ] + }, + postcss: [ autoprefixer ] +};