├── .github └── LICENSE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── __snapshots__ └── test.js.snap ├── docs ├── App.js ├── Banner.js ├── Border.js ├── Btn.js ├── Button.js ├── Card.js ├── Fill.js ├── Nav.js ├── Pre.js ├── Text.js ├── bundle.js ├── card.png ├── colors.js ├── entry.js ├── examples │ ├── Align.jsx │ ├── Column.jsx │ ├── Directions.jsx │ ├── FlexAuto.jsx │ ├── Grid.jsx │ ├── Hero.jsx │ ├── MarginAuto.jsx │ ├── Offset.jsx │ ├── Padding.jsx │ ├── Wrap.jsx │ └── index.js ├── index.html └── updaters.js ├── package.json ├── src ├── Box.js ├── Flex.js ├── ReflexProvider.js ├── config.js ├── context-types.js ├── css.js ├── index.js ├── reflex.js └── sheet.js ├── test.js └── webpack.config.js /.github/LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # The MIT License (MIT) 3 | Copyright (c) 2016 Brent Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist/* 4 | docs/dev.js 5 | package-lock.json 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxnblk/reflexbox/76c8f1bb1a15a5c35f0c16855273d5c2b67de0e3/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8.0 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Reflexbox 3 | 4 | Responsive React Flexbox Grid System 5 | 6 | http://jxnblk.com/reflexbox 7 | 8 | [![Build Status](https://travis-ci.org/jxnblk/reflexbox.svg?branch=master)](https://travis-ci.org/jxnblk/reflexbox) 9 | [![npm version](https://badge.fury.io/js/reflexbox.svg)](https://badge.fury.io/js/reflexbox) 10 | 11 | 12 | ## Features 13 | 14 | - Simple API for quickly controlling layout 15 | - Helps promote composability and separation of concerns 16 | - CSS-in-JS built in - no external dependencies 17 | - Only generates the CSS needed to render 18 | 19 | ## Getting Started 20 | 21 | ``` 22 | npm install reflexbox 23 | ``` 24 | 25 | ```jsx 26 | import React from 'react' 27 | import { Flex, Box } from 'reflexbox' 28 | 29 | class Component extends React.Component { 30 | render() { 31 | return ( 32 | 33 | Box A 34 | Box B 35 | 36 | ) 37 | } 38 | } 39 | ``` 40 | 41 | ## Usage 42 | 43 | ```jsx 44 | // Fractional width 45 | 46 | 47 | // Pixel width 48 | 49 | 50 | // Responsive widths 51 | 52 | 53 | // Padding 54 | 55 | 56 | // Responsive padding 57 | 58 | 59 | // Margin 60 | 61 | 62 | // Responsive margin 63 | 64 | 65 | // top, right, bottom, left 66 | 72 | 73 | // x-axis 74 | 75 | 76 | // y-axis 77 | 78 | 79 | // align-items: center 80 | 81 | 82 | // justify-content: space-between 83 | 84 | 85 | // Flex wrap 86 | 87 | 88 | // Flex direction column 89 | 90 | 91 | // Order 92 | 93 | 94 | // flex: 1 1 auto 95 | 96 | ``` 97 | 98 | ## API 99 | 100 | ### `` 101 | 102 | Component primitive with `display: flex` 103 | 104 | ### `` 105 | 106 | Primitive for controlling width, margin, padding and more. 107 | 108 | ### Props 109 | 110 | Both `` and `` share the same props. 111 | 112 | - `w` (number|string) sets width, where numbers 0-1 are percentage values, larger numbers are pixel values, and strings are raw CSS values with units. 113 | - `flex` (boolean) sets `display: flex` 114 | - `wrap` (boolean) sets `flex-wrap: wrap` 115 | - `column` (boolean) sets `flex-direction: column` 116 | - `auto` (boolean) sets `flex: 1 1 auto` 117 | - `order` (number) sets `order` 118 | - `align` (string) sets `align-items` 119 | - `justify` (string) sets `justify-content` 120 | 121 | #### Margin and Padding 122 | 123 | Margin and padding props accept numbers `0-4` for values from the spacing scale `[ 0, 8, 16, 32, 64 ]`. 124 | Numbers greater than 4 will be used as pixel values. 125 | Negative values can be used for negative margins. 126 | Strings can be passed for other CSS values, e.g. `mx='auto'` 127 | 128 | - `m` (number|string) margin based on a scale from 0–4. 129 | - `mx` (number|string) x-axis margin based on a scale from 0–4. 130 | - `my` (number|string) y-axis margin based on a scale from 0–4. 131 | - `mt` (number|string) margin-top based on a scale from 0–4. 132 | - `mb` (number|string) margin-bottom based on a scale from 0–4. 133 | - `ml` (number|string) margin-left based on a scale from 0–4. 134 | - `mr` (number|string) margin-right based on a scale from 0–4. 135 | - `p` (number|string) padding based on a scale from 0–4. 136 | - `px` (number|string) x-axis padding based on a scale from 0–4. 137 | - `py` (number|string) y-axis padding based on a scale from 0–4. 138 | - `pt` (number|string) padding-top based on a scale from 0–4. 139 | - `pb` (number|string) padding-bottom based on a scale from 0–4. 140 | - `pl` (number|string) padding-left based on a scale from 0–4. 141 | - `pr` (number|string) padding-right based on a scale from 0–4. 142 | 143 | ### Responsive Array Prop Values 144 | 145 | All props accept arrays as values for mobile-first responsive styles. 146 | 147 | ```jsx 148 | // Set widths for different breakpoints, starting from smallest to largest 149 | // This example will be 100% width below the smallest breakpoint, 150 | // then 50% and 25% for the next two breakpoints respectively 151 | 152 | ``` 153 | 154 | Null values can be passed to the array to skip a breakpoint. 155 | 156 | ```jsx 157 | 158 | ``` 159 | 160 | 161 | ## Configuration 162 | 163 | Values for the breakpoints and space scale can be configured with 164 | [React Context](https://facebook.github.io/react/docs/context.html). 165 | 166 | Context can be set manually or with the `` component. 167 | 168 | 169 | ```jsx 170 | import React from 'react' 171 | import { ReflexProvider, Flex, Box } from 'reflexbox' 172 | 173 | const space = [ 0, 6, 12, 18, 24 ] 174 | const breakpoints = [ 32, 48, 64 ] 175 | 176 | class App extends React.Component { 177 | render () { 178 | return ( 179 | 182 | 183 | Box 184 | Box 185 | Box 186 | Box 187 | 188 | 189 | ) 190 | } 191 | } 192 | ``` 193 | 194 | ## Higher Order Component 195 | 196 | The core Reflexbox higher-order component can be used on any element that accepts `className` as a prop. 197 | 198 | ```jsx 199 | import React from 'react' 200 | import { reflex } from 'reflexbox' 201 | import MyInput from './MyInput' 202 | 203 | const FlexInput = reflex(MyInput) 204 | 205 | const App = () => ( 206 |
207 | 212 |
213 | ) 214 | ``` 215 | 216 | ### Caveats 217 | 218 | This currently *DOES NOT* work in Node.js server-side applications. 219 | If you need server-side support, see version `^2.2.0` or one of the related libraries below. 220 | 221 | --- 222 | 223 | ### Related 224 | 225 | - [Axs](http://jxnblk.com/axs) 226 | - [Grid Styled](http://jxnblk.com/grid-styled) 227 | - [Gx](http://jxnblk.com/gx) 228 | - [Rebass](http://jxnblk.com/rebass) 229 | - [Reline](http://jxnblk.com/reline) 230 | 231 | [MIT License](.github/LICENSE.md) 232 | 233 | -------------------------------------------------------------------------------- /__snapshots__/test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`react context 1`] = ` 4 |
7 | `; 8 | 9 | exports[`snapshot 1`] = ` 10 |
13 | `; 14 | 15 | exports[`snapshot 2`] = ` 16 |
19 | `; 20 | -------------------------------------------------------------------------------- /docs/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createProvider } from 'funcup' 3 | import Nav from './Nav' 4 | import Banner from './Banner' 5 | import examples from './examples' 6 | import colors from './colors' 7 | 8 | const App = props => { 9 | const example = examples[Math.abs(props.index) % examples.length] 10 | const color = colors[Math.abs(props.index) % colors.length] 11 | 12 | return ( 13 |
14 |
20 | ) 21 | } 22 | 23 | const initialState = { 24 | index: 0, 25 | xray: false, 26 | } 27 | 28 | export default createProvider(initialState)(App) 29 | -------------------------------------------------------------------------------- /docs/Banner.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'funcup' 3 | import { Flex, Box } from 'reflexbox' 4 | import XRay from 'react-x-ray' 5 | import { 6 | LiveProvider, 7 | LiveEditor, 8 | LiveError, 9 | LivePreview 10 | } from 'react-live' 11 | import { dark } from './colors' 12 | import Fill from './Fill' 13 | import Border from './Border' 14 | import Btn from './Btn' 15 | import Text from './Text' 16 | import Pre from './Pre' 17 | import { setCode } from './updaters' 18 | 19 | class ExampleBanner extends React.Component { 20 | update = code => { 21 | this.props.update(setCode(code)) 22 | } 23 | 24 | render () { 25 | const { 26 | example, 27 | color, 28 | xray 29 | } = this.props 30 | 31 | const scope = { 32 | Flex, 33 | Box, 34 | Border, 35 | Fill, 36 | Btn, 37 | Text, 38 | Pre, 39 | color, 40 | } 41 | 42 | const textColor = dark(color) ? '#fff' : '#000' 43 | 44 | const sx = { 45 | root: { 46 | position: 'relative', 47 | }, 48 | inner: { 49 | minHeight: '100vh' 50 | }, 51 | top: { 52 | position: 'relative', 53 | height: '80vh', 54 | fontWeight: 'bold', 55 | color: textColor, 56 | backgroundColor: color, 57 | transitionProperty: 'background-color', 58 | transitionDuration: '.5s', 59 | transitionTimingFunction: 'ease-out', 60 | overflow: 'hidden' 61 | }, 62 | bottom: { 63 | height: '50vh' 64 | }, 65 | preview: { 66 | display: 'flex', 67 | alignItems: 'center', 68 | height: '80vh', 69 | paddingTop: 64, 70 | paddingBottom: 64, 71 | minHeight: 512, 72 | overflow: 'auto' 73 | }, 74 | editor: { 75 | fontFamily: 'SF Mono, Menlo, monospace', 76 | fontSize: 12, 77 | padding: 16, 78 | height: '100%', 79 | overflow: 'auto', 80 | margin: 0, 81 | color: color, 82 | backgroundColor: '#000', 83 | outline: 'none' 84 | }, 85 | error: { 86 | position: 'absolute', 87 | zIndex: 2, 88 | top: '50%', 89 | right: 0, 90 | left: 0, 91 | transform: 'TranslateY(-50%)', 92 | fontFamily: 'SF Mono, Menlo, monospace', 93 | fontSize: 12, 94 | margin: 0, 95 | padding: 16, 96 | color: '#fff', 97 | backgroundColor: '#f00' 98 | }, 99 | credits: { 100 | position: 'absolute', 101 | right: 0, 102 | bottom: 0, 103 | marginRight: 16 104 | } 105 | } 106 | 107 | return ( 108 | 109 | 113 | 114 | 115 | 116 | 120 | 123 | 124 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | ) 137 | } 138 | } 139 | 140 | export default connect()(ExampleBanner) 141 | -------------------------------------------------------------------------------- /docs/Border.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from 'reflexbox' 3 | 4 | const Border = props => ( 5 | 12 | ) 13 | 14 | export default Border 15 | -------------------------------------------------------------------------------- /docs/Btn.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Btn = props => ( 4 | 22 | ) 23 | 24 | export default Btn 25 | -------------------------------------------------------------------------------- /docs/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Button = props => ( 4 | 47 | 50 | 51 | 52 | ) 53 | } 54 | 55 | export default connect()(Nav) 56 | -------------------------------------------------------------------------------- /docs/Pre.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Pre = props => ( 4 |
13 | )
14 | 
15 | export default Pre
16 | 


--------------------------------------------------------------------------------
/docs/Text.js:
--------------------------------------------------------------------------------
 1 | import React from 'react'
 2 | 
 3 | const Text = props => (
 4 |   React.createElement(props.is,
 5 |     Object.assign({}, props, {
 6 |       style: {
 7 |         margin: 0,
 8 |         fontSize: scale[props.f]
 9 |       }
10 |     })
11 |   )
12 | )
13 | 
14 | Text.defaultProps = {
15 |   is: 'p',
16 |   f: 4
17 | }
18 | 
19 | const scale = [
20 |   48,
21 |   32,
22 |   24,
23 |   20,
24 |   16,
25 |   14,
26 |   12
27 | ]
28 | 
29 | export default Text
30 | 


--------------------------------------------------------------------------------
/docs/card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jxnblk/reflexbox/76c8f1bb1a15a5c35f0c16855273d5c2b67de0e3/docs/card.png


--------------------------------------------------------------------------------
/docs/colors.js:
--------------------------------------------------------------------------------
 1 | import chroma from 'chroma-js'
 2 | 
 3 | export const dark = n => chroma(n).luminance() < 0.5
 4 | 
 5 | const colors = [
 6 |   '#07c',
 7 |   '#50c',
 8 |   '#b0c',
 9 |   '#0eb',
10 |   '#eb0',
11 |   '#e07',
12 | ]
13 | 
14 | export default colors
15 | 


--------------------------------------------------------------------------------
/docs/entry.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import App from './App'
4 | 
5 | console.log('hello')
6 | render(, app)
7 | 


--------------------------------------------------------------------------------
/docs/examples/Align.jsx:
--------------------------------------------------------------------------------
 1 | 
 9 |   
10 |     
11 |       Align
12 |     
13 |   
14 |   
15 |     Center
16 |   
17 | 
18 | 


--------------------------------------------------------------------------------
/docs/examples/Column.jsx:
--------------------------------------------------------------------------------
 1 | 
 8 |   
 9 |     
10 |       Flex Direction
11 |     
12 |   
13 |   
14 |     Column
15 |   
16 | 
17 | 


--------------------------------------------------------------------------------
/docs/examples/Directions.jsx:
--------------------------------------------------------------------------------
1 | 
2 |   pt
3 |   pr
4 |   pb
5 |   pl
6 |   px
7 |   py
8 | 
9 | 


--------------------------------------------------------------------------------
/docs/examples/FlexAuto.jsx:
--------------------------------------------------------------------------------
1 | 
2 |   Auto
3 |   Auto
4 |   Auto
5 | 
6 | 


--------------------------------------------------------------------------------
/docs/examples/Grid.jsx:
--------------------------------------------------------------------------------
 1 | 
 2 |   1/2
 3 |   1/2
 4 | 
 5 |   1/3
 6 |   1/3
 7 |   1/3
 8 | 
 9 |   1/4
10 |   1/4
11 |   1/4
12 |   1/4
13 | 
14 |   1/5
15 |   1/5
16 |   1/5
17 |   1/5
18 |   1/5
19 | 
20 |   1/6
21 |   1/6
22 |   1/6
23 |   1/6
24 |   1/6
25 |   1/6
26 | 
27 | 


--------------------------------------------------------------------------------
/docs/examples/Hero.jsx:
--------------------------------------------------------------------------------
 1 | 
 2 |   
 3 |     
 9 |       
10 |         
13 |           

Reflex

14 |
15 | 16 |

Box

17 |
18 |
19 |
20 | 21 | 24 | 25 | 26 | 36 | 37 |
38 |     
39 |   
40 | 
41 | 


--------------------------------------------------------------------------------
/docs/examples/MarginAuto.jsx:
--------------------------------------------------------------------------------
1 | 
2 |   
3 |     Margin
4 |   
5 |   
6 |     Auto
7 |   
8 | 
9 | 


--------------------------------------------------------------------------------
/docs/examples/Offset.jsx:
--------------------------------------------------------------------------------
 1 | 
 6 |   1/4
 7 |   1/4
 8 |   1/4
 9 |   1/4
10 |   
11 |     Offset 1/4
12 |   
13 | 
14 | 


--------------------------------------------------------------------------------
/docs/examples/Padding.jsx:
--------------------------------------------------------------------------------
 1 | 
 6 |   
 7 |     p4
 8 |   
 9 |   
10 |     p3
11 |   
12 |   
13 |     p2
14 |   
15 |   
16 |     p1
17 |   
18 | 
19 | 


--------------------------------------------------------------------------------
/docs/examples/Wrap.jsx:
--------------------------------------------------------------------------------
 1 | 
 9 |   
10 |     Wrap
11 |   
12 |   
13 |     Wrap
14 |   
15 |   
16 |     Wrap
17 |   
18 |   
19 |     Wrap
20 |   
21 |   
22 |     Wrap
23 |   
24 |   
25 |     Wrap
26 |   
27 | 
28 | 


--------------------------------------------------------------------------------
/docs/examples/index.js:
--------------------------------------------------------------------------------
 1 | import Hero from './Hero.jsx'
 2 | import FlexAuto from './FlexAuto.jsx'
 3 | import Padding from './Padding.jsx'
 4 | import Align from './Align.jsx'
 5 | import MarginAuto from './MarginAuto.jsx'
 6 | import Directions from './Directions.jsx'
 7 | import Wrap from './Wrap.jsx'
 8 | import Column from './Column.jsx'
 9 | import Grid from './Grid.jsx'
10 | import Offset from './Offset.jsx'
11 | 
12 | export default [
13 |   Hero,
14 |   Grid,
15 |   FlexAuto,
16 |   Padding,
17 |   Align,
18 |   MarginAuto,
19 |   Directions,
20 |   Wrap,
21 |   Column,
22 |   Offset,
23 | ]
24 | 


--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | Reflexbox
 4 | 
 5 | 
 6 | 
 7 | 
 8 | 
 9 | 
10 | 
11 | 
23 | 
24 | 25 | -------------------------------------------------------------------------------- /docs/updaters.js: -------------------------------------------------------------------------------- 1 | import colors from './colors' 2 | import examples from './examples' 3 | 4 | export const setCode = code => state => ({ code }) 5 | export const setColor = color => state => ({ color }) 6 | export const setIndex = index => state => ({ index }) 7 | export const dec = state => ({ index: state.index - 1 }) 8 | export const inc = state => ({ index: state.index + 1 }) 9 | 10 | export const cycleColor = state => { 11 | const i = (colors.indexOf(state.color) + 1) % colors.length 12 | return { color: colors[i] } 13 | } 14 | 15 | export const toggleXRay = state => ({ xray: !state.xray }) 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reflexbox", 3 | "version": "3.0.1", 4 | "description": "Responsive React Flexbox Grid System", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "NODE_ENV=production webpack -p", 8 | "start": "webpack-dev-server", 9 | "prepublish": "mkdir -p dist && babel src --out-dir dist", 10 | "test": "ava", 11 | "card": "repng docs/Card.js --dev" 12 | }, 13 | "author": "Brent Jackson", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "ava": "^0.19.1", 17 | "babel-cli": "^6.24.1", 18 | "babel-core": "^6.24.1", 19 | "babel-eslint": "^6.1.2", 20 | "babel-loader": "^6.2.3", 21 | "babel-preset-env": "^1.5.1", 22 | "babel-preset-react": "^6.24.1", 23 | "babel-preset-stage-0": "^6.24.1", 24 | "babel-register": "^6.24.1", 25 | "browser-env": "^3.2.4", 26 | "chroma-js": "^1.3.4", 27 | "funcup": "^1.0.0-0", 28 | "raw-loader": "^0.5.1", 29 | "react": "^15.5.4", 30 | "react-dom": "^15.5.4", 31 | "react-live": "^1.5.3", 32 | "react-test-renderer": "^15.5.4", 33 | "react-x-ray": "^1.0.0-1", 34 | "reline": "^1.0.0-beta.3", 35 | "repng": "^2.0.0-alpha.1", 36 | "webpack": "^2.6.1", 37 | "webpack-dev-server": "^2.4.5" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/jxnblk/reflexbox.git" 42 | }, 43 | "keywords": [ 44 | "react", 45 | "react-component", 46 | "flexbox", 47 | "layout", 48 | "grid", 49 | "css-in-js" 50 | ], 51 | "bugs": { 52 | "url": "https://github.com/jxnblk/reflexbox/issues" 53 | }, 54 | "babel": { 55 | "presets": [ 56 | "env", 57 | "stage-0", 58 | "react" 59 | ] 60 | }, 61 | "ava": { 62 | "require": [ 63 | "babel-register" 64 | ], 65 | "babel": "inherit" 66 | }, 67 | "homepage": "https://github.com/jxnblk/reflexbox#readme", 68 | "dependencies": { 69 | "prop-types": "^15.5.10" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Box.js: -------------------------------------------------------------------------------- 1 | import reflex from './reflex' 2 | 3 | const Box = reflex('div') 4 | 5 | export default Box 6 | -------------------------------------------------------------------------------- /src/Flex.js: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react' 2 | import Box from './Box' 3 | 4 | const Flex = props => ( 5 | createElement(Box, { ...props, flex: true }) 6 | ) 7 | 8 | export default Flex 9 | -------------------------------------------------------------------------------- /src/ReflexProvider.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import contextTypes from './context-types' 3 | 4 | class ReflexProvider extends React.Component { 5 | getChildContext () { 6 | return { 7 | reflexbox: this.props 8 | } 9 | } 10 | 11 | render () { 12 | return React.Children.only(this.props.children) 13 | } 14 | } 15 | 16 | ReflexProvider.childContextTypes = contextTypes 17 | 18 | export default ReflexProvider 19 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const breakpoints = [ 2 | 40, 3 | 52, 4 | 64 5 | ] 6 | 7 | export const space = [ 8 | 0, 8, 16, 32, 64 9 | ] 10 | 11 | export default { 12 | breakpoints, 13 | space 14 | } 15 | -------------------------------------------------------------------------------- /src/context-types.js: -------------------------------------------------------------------------------- 1 | import { shape, arrayOf, number } from 'prop-types' 2 | 3 | const contextTypes = { 4 | reflexbox: shape({ 5 | breakpoints: arrayOf(number), 6 | space: arrayOf(number) 7 | }) 8 | } 9 | 10 | export default contextTypes 11 | -------------------------------------------------------------------------------- /src/css.js: -------------------------------------------------------------------------------- 1 | import sheet from './sheet' 2 | 3 | const REG = /^([wmp][trblxy]?|flex|wrap|column|auto|align|justify|order)$/ 4 | const cache = {} 5 | 6 | const css = config => props => { 7 | const next = {} 8 | const classNames = [] 9 | 10 | const breaks = [ null, ...config.breakpoints ] 11 | const sx = stylers(config) 12 | 13 | for (let key in props) { 14 | const val = props[key] 15 | if (!REG.test(key)) { 16 | next[key] = val 17 | continue 18 | } 19 | const cx = createRule(breaks, sx)(key, val) 20 | cx.forEach(cn => classNames.push(cn)) 21 | } 22 | 23 | next.className = join(next.className, ...classNames) 24 | 25 | return next 26 | } 27 | 28 | css.reset = () => { 29 | Object.keys(cache).forEach(key => { 30 | delete cache[key] 31 | }) 32 | while (sheet.cssRules.length) { 33 | sheet.deleteRule(0) 34 | } 35 | } 36 | 37 | const createRule = (breaks, sx) => (key, val) => { 38 | const classNames = [] 39 | const id = '_Rfx' + sheet.cssRules.length.toString(36) 40 | const k = key.charAt(0) 41 | const style = sx[key] || sx[k] 42 | 43 | const rules = toArr(val).map((v, i) => { 44 | const bp = breaks[i] 45 | const decs = style(key, v) 46 | const cn = id + '_' + (bp || '') 47 | const body = `.${cn}{${decs}}` 48 | const rule = media(bp, body) 49 | 50 | const _key = decs + (bp || '') 51 | 52 | if (cache[_key]) { 53 | classNames.push(cache[_key]) 54 | return null 55 | } else { 56 | classNames.push(cn) 57 | cache[_key] = cn 58 | return rule 59 | } 60 | }).filter(r => r !== null) 61 | 62 | sheet.insert(rules) 63 | 64 | return classNames 65 | } 66 | 67 | const toArr = n => Array.isArray(n) ? n : [ n ] 68 | const num = n => typeof n === 'number' && !isNaN(n) 69 | 70 | const join = (...args) => args 71 | .filter(a => !!a) 72 | .join(' ') 73 | 74 | const dec = args => args.join(':') 75 | const rule = args => args.join(';') 76 | const media = (bp, body) => bp ? `@media screen and (min-width:${bp}em){${body}}` : body 77 | 78 | const width = (key, n) => dec([ 'width', !num(n) || n > 1 ? px(n) : (n * 100) + '%' ]) 79 | const px = n => num(n) ? n + 'px' : n 80 | 81 | const space = scale => (key, n) => { 82 | const [ a, b ] = key.split('') 83 | const prop = a === 'm' ? 'margin' : 'padding' 84 | const dirs = directions[b] || [''] 85 | const neg = n < 0 ? -1 : 1 86 | const val = !num(n) ? n : px((scale[Math.abs(n)] || Math.abs(n)) * neg) 87 | return rule(dirs.map(d => dec([ prop + d, val ]))) 88 | } 89 | 90 | const directions = { 91 | t: [ '-top' ], 92 | r: [ '-right' ], 93 | b: [ '-bottom' ], 94 | l: [ '-left' ], 95 | x: [ '-left', '-right' ], 96 | y: [ '-top', '-bottom' ], 97 | } 98 | 99 | const flex = (key, n) => dec([ 'display', n ? 'flex' : 'block' ]) 100 | const wrap = (key, n) => dec([ 'flex-wrap', n ? 'wrap' : 'nowrap' ]) 101 | const auto = (key, n) => dec([ 'flex', '1 1 auto' ]) 102 | const column = (key, n) => dec([ 'flex-direction', n ? 'column' : 'row' ]) 103 | const align = (key, n) => dec([ 'align-items', n ]) 104 | const justify = (key, n) => dec([ 'justify-content', n ]) 105 | const order = (key, n) => dec([ 'order', n ]) 106 | 107 | const stylers = config => ({ 108 | w: width, 109 | m: space(config.space), 110 | p: space(config.space), 111 | flex, 112 | wrap, 113 | auto, 114 | column, 115 | align, 116 | justify, 117 | order 118 | }) 119 | 120 | export default css 121 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as config } from './config' 2 | export { default as reflex } from './reflex' 3 | export { default as sheet } from './sheet' 4 | export { default as css } from './css' 5 | export { default as Flex } from './Flex' 6 | export { default as Box } from './Box' 7 | export { default as ReflexProvider } from './ReflexProvider' 8 | -------------------------------------------------------------------------------- /src/reflex.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import css from './css' 3 | import defaultConfig from './config' 4 | import contextTypes from './context-types' 5 | 6 | const reflex = Component => { 7 | const Reflex = (props, context) => { 8 | const config = Object.assign({}, defaultConfig, context.reflexbox) 9 | const next = css(config)(props) 10 | 11 | return React.createElement(Component, next) 12 | } 13 | 14 | Reflex.contextTypes = contextTypes 15 | 16 | return Reflex 17 | } 18 | 19 | export default reflex 20 | -------------------------------------------------------------------------------- /src/sheet.js: -------------------------------------------------------------------------------- 1 | // todo: make node version 2 | const style = document.createElement('style') 3 | style.id = 'reflexbox' 4 | style.type = 'text/css' 5 | document.head.appendChild(style) 6 | 7 | const sheet = style.sheet 8 | 9 | sheet.insert = css => css.map(rule => { 10 | const l = sheet.cssRules.length 11 | sheet.insertRule(rule, l) 12 | }) 13 | 14 | export default sheet 15 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | require('browser-env')() 2 | const test = require('ava') 3 | const React = require('react') 4 | const render = require('react-test-renderer').create 5 | const { 6 | sheet, 7 | css, 8 | config, 9 | ReflexProvider, 10 | Box, 11 | Flex 12 | } = require('./src') 13 | 14 | const props = { 15 | children: 'Hello', 16 | title: 'Boop', 17 | w: [ 1, 1/2, 1/4 ], 18 | m: 2, 19 | flex: true, 20 | px: 3 21 | } 22 | 23 | const cx = css(config) 24 | 25 | test.afterEach(t => { 26 | css.reset() 27 | }) 28 | 29 | test('css returns props', t => { 30 | const a = cx(props) 31 | t.is(typeof a, 'object') 32 | }) 33 | 34 | test('css passes props through', t => { 35 | const a = cx(props) 36 | t.is(a.children, 'Hello') 37 | t.is(a.title, 'Boop') 38 | }) 39 | 40 | test('css strips style props', t => { 41 | const a = cx(props) 42 | t.is(a.w, undefined) 43 | t.is(a.m, undefined) 44 | t.is(a.flex, undefined) 45 | t.is(a.px, undefined) 46 | }) 47 | 48 | test('css adds a className prop', t => { 49 | const a = cx(props) 50 | t.is(typeof a.className, 'string') 51 | }) 52 | 53 | test('css inserts styles to sheet', t => { 54 | const a = cx({ m: 2 }) 55 | t.is(sheet.cssRules.length, 1) 56 | const [ rule ] = sheet.cssRules 57 | t.is(rule.style.margin, '16px') 58 | }) 59 | 60 | test('css inserts responsive styles', t => { 61 | const a = css(config)({ m: [ 1, 2 ] }) 62 | t.is(sheet.cssRules.length, 2) 63 | const [ baseRule, mobile ] = sheet.cssRules 64 | const [ mobileRule ] = mobile.cssRules 65 | t.is(baseRule.style.margin, '8px') 66 | t.is(mobileRule.style.margin, '16px') 67 | }) 68 | 69 | test('css dedupes repeated styles', t => { 70 | const a = cx({ p: 2 }) 71 | const b = cx({ p: 2 }) 72 | t.is(sheet.cssRules.length, 1) 73 | t.deepEqual(a, b) 74 | }) 75 | 76 | test('css parses widths', t => { 77 | const a = cx({ w: 1 }) 78 | const b = cx({ w: 1/2 }) 79 | const c = cx({ w: 0 }) 80 | const d = cx({ w: 24 }) 81 | const e = cx({ w: 'auto' }) 82 | const rules = sheet.cssRules 83 | t.is(rules[0].style.width, '100%') 84 | t.is(rules[1].style.width, '50%') 85 | t.is(rules[2].style.width, '0%') 86 | t.is(rules[3].style.width, '24px') 87 | t.is(rules[4].style.width, 'auto') 88 | }) 89 | 90 | test('css parses margins', t => { 91 | cx({ m: 1 }) 92 | cx({ m: -2 }) 93 | cx({ m: 24 }) 94 | cx({ m: -24 }) 95 | cx({ m: 'auto' }) 96 | const [ a, b, c, d, e ] = sheet.cssRules 97 | t.is(a.style.margin, '8px') 98 | t.is(b.style.margin, '-16px') 99 | t.is(c.style.margin, '24px') 100 | t.is(d.style.margin, '-24px') 101 | t.is(e.style.margin, 'auto') 102 | }) 103 | 104 | test('css parses margin directions', t => { 105 | cx({ m: 1 }) 106 | cx({ mt: 1 }) 107 | cx({ mr: 1 }) 108 | cx({ mb: 1 }) 109 | cx({ ml: 1 }) 110 | cx({ mx: 1 }) 111 | cx({ my: 1 }) 112 | const [ a, top, r, b, l, x, y ] = sheet.cssRules 113 | t.is(a.style.margin, '8px') 114 | t.is(top.style['margin-top'], '8px') 115 | t.is(r.style['margin-right'], '8px') 116 | t.is(b.style['margin-bottom'], '8px') 117 | t.is(l.style['margin-left'], '8px') 118 | t.is(x.style['margin-left'], '8px') 119 | t.is(x.style['margin-right'], '8px') 120 | t.is(y.style['margin-top'], '8px') 121 | t.is(y.style['margin-bottom'], '8px') 122 | }) 123 | 124 | test('css parses paddings', t => { 125 | cx({ p: 1 }) 126 | cx({ p: 24 }) 127 | cx({ p: '20%' }) 128 | const [ a, b, c ] = sheet.cssRules 129 | t.is(a.style.padding, '8px') 130 | t.is(b.style.padding, '24px') 131 | t.is(c.style.padding, '20%') 132 | }) 133 | 134 | test('css parses padding directions', t => { 135 | cx({ p: 1 }) 136 | cx({ pt: 1 }) 137 | cx({ pr: 1 }) 138 | cx({ pb: 1 }) 139 | cx({ pl: 1 }) 140 | cx({ px: 1 }) 141 | cx({ py: 1 }) 142 | const [ a, top, r, b, l, x, y ] = sheet.cssRules 143 | t.is(a.style.padding, '8px') 144 | t.is(top.style['padding-top'], '8px') 145 | t.is(r.style['padding-right'], '8px') 146 | t.is(b.style['padding-bottom'], '8px') 147 | t.is(l.style['padding-left'], '8px') 148 | t.is(x.style['padding-left'], '8px') 149 | t.is(x.style['padding-right'], '8px') 150 | t.is(y.style['padding-top'], '8px') 151 | t.is(y.style['padding-bottom'], '8px') 152 | }) 153 | 154 | test('css parses flexbox styles', t => { 155 | cx({ 156 | flex: true, 157 | wrap: true, 158 | column: true, 159 | auto: true, 160 | order: 5, 161 | align: 'center', 162 | justify: 'space-between' 163 | }) 164 | const [ a, b, c, d, e, f, g ] = sheet.cssRules 165 | t.is(a.style.display, 'flex') 166 | t.is(b.style['flex-wrap'], 'wrap') 167 | t.is(c.style['flex-direction'], 'column') 168 | t.is(d.style.flex, '1 1 auto') 169 | t.is(e.style.order, '5') 170 | t.is(f.style['align-items'], 'center') 171 | t.is(g.style['justify-content'], 'space-between') 172 | }) 173 | 174 | test('snapshot', t => { 175 | const box = render().toJSON() 176 | const flex = render().toJSON() 177 | t.snapshot(box) 178 | t.snapshot(flex) 179 | }) 180 | 181 | test('react context', t => { 182 | const ctx = { 183 | space: [ 0, 6, 12, 18, 24, 30 ], 184 | breakpoints: [ 32, 48, 64 ] 185 | } 186 | const box = render( 187 | 188 | 189 | 190 | ).toJSON() 191 | const [ m, w1, w2 ] = sheet.cssRules 192 | t.snapshot(box) 193 | t.is(m.style.margin, '6px') 194 | t.is(w1.style.width, '100%') 195 | t.is(w2.media[0], 'screen and (min-width:32em)') 196 | }) 197 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './docs/entry.js', 5 | 6 | output: { 7 | path: path.join(__dirname, 'docs'), 8 | filename: 'bundle.js' 9 | }, 10 | 11 | resolve: { 12 | alias: { 13 | 'reflexbox': path.join(__dirname, 'dist') 14 | } 15 | }, 16 | 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | use: 'babel-loader' 23 | }, 24 | { 25 | test: /\.jsx$/, 26 | exclude: /node_modules/, 27 | use: 'raw-loader' 28 | } 29 | ] 30 | }, 31 | 32 | devServer: { 33 | contentBase: 'docs/', 34 | historyApiFallback: true 35 | } 36 | } 37 | --------------------------------------------------------------------------------