├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── docs ├── App.jsx ├── Box.jsx ├── CellDemo.jsx ├── Controls.jsx ├── Cta.jsx ├── Dev.jsx ├── GridDemo.jsx ├── Intro.jsx ├── ModularScaleDemo.jsx ├── NestedGrid.jsx ├── RatiosDemo.jsx ├── Readme.jsx ├── Section.jsx ├── Social.jsx ├── TypographyDemo.jsx ├── base.css ├── bundle.js ├── data.js └── entry.js ├── index.html ├── package.json ├── src ├── Cell.js ├── Grid.js └── index.js ├── test ├── Cell.spec.js ├── Grid.spec.js ├── index.js └── karma.config.js ├── webpack.config.js └── webpack.dev.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | dist 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jxnblk/rgx/9af96aa732f62f2dc81b964f75dc29ebf1f2b1c2/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.7" 4 | addons: 5 | firefox: '42.0' 6 | before_script: 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rgx 2 | 3 | React grid system – constraint-based responsive grid with no CSS and no media queries. 4 | 5 | [![Build Status](https://travis-ci.org/jxnblk/rgx.svg?branch=master)](https://travis-ci.org/jxnblk/rgx) 6 | 7 | ## About 8 | 9 | Rgx is an experimental, responsive grid system based on minimum and maximum widths and designed for content-out layout. 10 | Rgx is built purely in React and uses inline styles, with no CSS and no media queries. 11 | Each Grid row sets its child Cells to display inline block once the Grid is wide enough to fit all Cells’ minimum widths. 12 | Once set inline, each Cell’s width is based on the ratio of its own minimum width to the sum of minimum widths per row. 13 | Once a Cell hits its max-width, the remaining space is distributed to other Cells in the row. 14 | Since this isn’t based on viewport-based media queries, the Grid responds to its own width, similar to element queries. 15 | 16 | ## Getting Started 17 | 18 | ```bash 19 | npm i rgx 20 | ``` 21 | 22 | ```js 23 | import React from 'react' 24 | import { Grid, Cell } from 'rgx' 25 | 26 | class Demo extends React.Component { 27 | render () { 28 | return ( 29 | 30 | Min 256 Max 320 31 | Min 768 32 | 33 | ) 34 | } 35 | } 36 | 37 | React.render(, document.querySelector('#demo')) 38 | ``` 39 | 40 | ## Grid Component 41 | 42 | #### Props 43 | - `gutter` - pixel value to set negative margins on the Grid component and padding on Cell components to create gutters. 44 | - `min` - pixel value to set a default `min` prop for child Cells 45 | 46 | ## Cell Component 47 | 48 | #### Props 49 | - `min` - pixel value to set the min-width at which a Cell is displayed inline. 50 | - `max` - pixel value at which the Cell should not expand. Remaining space is distributed to other Cells. 51 | - `padding` - sets left and right padding. This is used by the Grid component when the `gutter` prop is set and the Cell has no padding set. 52 | - `width` - fraction value used by the Grid component to set a width. This can also be set manually when used independently from the Grid component 53 | - `inline` - boolean value used by the Grid component to display a Cell inline. 54 | 55 | ## Performance 56 | 57 | I have yet to do any performance audits, and since the Grid component listens to window resize events, 58 | this probably has some performance issues. Any help in that area would be greatly appreciated. 59 | 60 | MIT License 61 | 62 | -------------------------------------------------------------------------------- /docs/App.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { Header, Footer } from 'blk' 4 | import Intro from './Intro.jsx' 5 | import ModularScaleDemo from './ModularScaleDemo.jsx' 6 | import GridDemo from './GridDemo.jsx' 7 | import TypographyDemo from './TypographyDemo.jsx' 8 | import NestedGrid from './NestedGrid.jsx' 9 | import RatiosDemo from './RatiosDemo.jsx' 10 | import CellDemo from './CellDemo.jsx' 11 | import Social from './Social.jsx' 12 | import Cta from './Cta.jsx' 13 | import css from './base.css' 14 | 15 | class App extends React.Component { 16 | 17 | constructor () { 18 | super () 19 | this.state = { 20 | base: 16, 21 | } 22 | this.handleChange = this.handleChange.bind(this) 23 | } 24 | 25 | handleChange (e) { 26 | let state = this.state 27 | state[e.target.name] = parseFloat(e.target.value) 28 | this.setState(state) 29 | } 30 | 31 | render () { 32 | let props = this.props 33 | let state = this.state 34 | 35 | return ( 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 | ) 49 | } 50 | 51 | } 52 | 53 | export default App 54 | 55 | -------------------------------------------------------------------------------- /docs/Box.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | class Box extends React.Component { 5 | 6 | render () { 7 | let style = { 8 | fontSize: 12, 9 | fontWeight: 'bold', 10 | textAlign: 'center', 11 | padding: '8px 4px', 12 | marginBottom: 16, 13 | border: '1px solid silver' 14 | } 15 | return ( 16 |
17 | {this.props.children} 18 |
19 | ) 20 | } 21 | 22 | } 23 | 24 | export default Box 25 | 26 | -------------------------------------------------------------------------------- /docs/CellDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { Grid, Cell } from '..' 4 | import Section from './Section.jsx' 5 | import Box from './Box.jsx' 6 | 7 | class CellDemo extends React.Component { 8 | 9 | render () { 10 | let props = this.props 11 | return ( 12 |
13 |

Cell

14 |
15 | 16 | 17 | 18 | {''} 19 | 20 | 21 | 22 | 23 | 24 | 25 | {''} 26 | 27 | 28 | 29 | 30 | 31 | 32 | {''} 33 | 34 | 35 | 36 |
37 | 38 | 39 |

40 | The Cell component can be used independently to manually arrange elements inline with a set percentage based width. 41 |

42 |
43 | 44 |
45 |
46 | ) 47 | } 48 | 49 | } 50 | 51 | export default CellDemo 52 | 53 | -------------------------------------------------------------------------------- /docs/Controls.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { Range } from 'rebass' 4 | import { Grid, Cell } from '..' 5 | 6 | class Controls extends React.Component { 7 | 8 | render () { 9 | let props = this.props 10 | return ( 11 |
12 | 13 | 14 | 21 | 22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | 29 | } 30 | 31 | export default Controls 32 | 33 | -------------------------------------------------------------------------------- /docs/Cta.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Section from './Section.jsx' 4 | 5 | class Cta extends React.Component { 6 | 7 | render () { 8 | return ( 9 |
10 |

Get Started

11 |
npm i rgx
12 |

13 | Read the docs on GitHub to learn more. 14 |

15 | 17 | GitHub 18 | 19 |
20 | ) 21 | } 22 | 23 | } 24 | 25 | export default Cta 26 | 27 | -------------------------------------------------------------------------------- /docs/Dev.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { Grid, Cell } from '..' 4 | import Box from './Box.jsx' 5 | 6 | class Dev extends React.Component { 7 | 8 | render () { 9 | let styles = { 10 | container: { 11 | paddingTop: 48, 12 | paddingBottom: 48, 13 | marginBottom: 48, 14 | border: '1px solid red' 15 | } 16 | } 17 | return ( 18 |
19 | 20 | 21 | min/max 22 | 23 | 24 | min/max 25 | 26 | 27 | min 28 | 29 | 30 | 31 | 32 | 128/256 33 | 34 | 35 | 128 36 | 37 | 38 | 39 | 40 | 256 41 | 42 | 43 | 256 44 | 45 | 46 | 256/320 47 | 48 | 49 | 50 | 51 | 160 52 | 53 | 54 | 320/768 55 | 56 | 57 | 128 58 | 59 | 60 | 61 | 62 | 128/160 63 | 64 | 65 | 128/192 66 | 67 | 68 | min 96 69 | 70 | 71 | min 256 72 | 73 | 74 |
75 | ) 76 | } 77 | 78 | } 79 | 80 | export default Dev 81 | 82 | -------------------------------------------------------------------------------- /docs/GridDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { Grid, Cell } from '..' 4 | import Box from './Box.jsx' 5 | 6 | class GridDemo extends React.Component { 7 | 8 | render () { 9 | let props = this.props 10 | return ( 11 | 12 | {props.grid.cells.map(function(cell, i) { 13 | return ( 14 | 17 | {cell.min} min 18 | 19 | ) 20 | })} 21 | 22 | ) 23 | } 24 | 25 | } 26 | 27 | GridDemo.propTypes = { 28 | gutter: React.PropTypes.number, 29 | grid: React.PropTypes.object 30 | } 31 | 32 | export default GridDemo 33 | 34 | -------------------------------------------------------------------------------- /docs/Intro.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Section from './Section.jsx' 4 | 5 | class Intro extends React.Component { 6 | 7 | render () { 8 | return ( 9 |
10 |

11 | Rgx is an experimental, responsive grid system based on minimum and maximum widths and designed for content-out layout. 12 | Rgx is built purely in React and uses inline styles, with no CSS and no media queries. 13 | Each Grid row sets its child Cells to display inline block once the Grid is wide enough to fit all Cells’ minimum widths. 14 | Once set inline, each Cell’s width is based on the ratio of its own minimum width to the sum of minimum widths per row. 15 | Once a Cell hits its max-width, the remaining space is distributed to other Cells in the row. 16 | Since this isn’t based on viewport-based media queries, the Grid responds to its own width, similar to element queries. 17 |

18 |
19 | ) 20 | } 21 | 22 | } 23 | 24 | export default Intro 25 | 26 | -------------------------------------------------------------------------------- /docs/ModularScaleDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ms from 'simple-modular-scale' 4 | import { Grid, Cell } from '..' 5 | import GridDemo from './GridDemo.jsx' 6 | import Section from './Section.jsx' 7 | 8 | class ModularScaleDemo extends React.Component { 9 | 10 | render () { 11 | let props = this.props 12 | let scale = ms({ 13 | base: 4 * props.base, 14 | ratios: [3/2, 4/3], 15 | length: 8 16 | }) 17 | 18 | let g1 = [] 19 | for (var i = scale.length - 1; i > -1; i--) { 20 | let l = scale.length - i 21 | let cells = [] 22 | let min = scale[i] 23 | for (var j = 0; j < l; j++) { 24 | cells.push({ min: min }) 25 | } 26 | g1.push({ cells: cells }) 27 | } 28 | 29 | return ( 30 |
31 | {g1.map(function(grid, i) { 32 | return ( 33 | 36 | ) 37 | })} 38 | 39 | 40 |

41 | Each Cell has a min prop that defines the minimum width at which it can be set inline as a column. 42 | Once set inline, each Cell’s width is determined as the ratio of its minimum width to the total for all Cells in a Grid row. 43 |

44 |

45 | Sizes in this demo are based on a modular scale: {scale.join(' : ')} 46 |

47 |
48 | 49 |
50 |
51 | ) 52 | } 53 | 54 | } 55 | 56 | export default ModularScaleDemo 57 | 58 | -------------------------------------------------------------------------------- /docs/NestedGrid.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { Grid, Cell } from '..' 4 | import Section from './Section.jsx' 5 | import Box from './Box.jsx' 6 | 7 | class NestedGrid extends React.Component { 8 | 9 | render () { 10 | let props = this.props 11 | return ( 12 |
13 |

Nested Grid

14 | 15 | 16 | 256 min 17 | 18 | 19 | 20 | 21 | 256 min 22 | 23 | 24 | 256 min 25 | 26 | 27 | 28 | 29 | 30 | 31 | 256/320 min/max 32 | 33 | 34 | 35 | 36 | 256 min 37 | 38 | 39 | 256 min 40 | 41 | 42 | 43 | 44 | 45 | 46 |

47 | The Cells in this demo all have a min value of 256, and two Cells are nested within another. 48 | The two top-level Cells take up 50% of the width, and the nested Cells are 50% of the parent Cell. 49 | Since the collapsing behavior is based on the container Grid’s width, the nested Cells will collapse before the parent Cells do. 50 | In the second row, the first Cell has a max of 320, and the remaining Cell stretches to fill the space. 51 |

52 |
53 | 54 |
55 |
56 | ) 57 | } 58 | 59 | } 60 | 61 | export default NestedGrid 62 | 63 | -------------------------------------------------------------------------------- /docs/RatiosDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ms from 'simple-modular-scale' 4 | import { Grid, Cell } from '..' 5 | import Section from './Section.jsx' 6 | import GridDemo from './GridDemo.jsx' 7 | 8 | class RatiosDemo extends React.Component { 9 | 10 | render () { 11 | let props = this.props 12 | let scale = ms({ 13 | base: 64, 14 | ratios: [3/2, 4/3], 15 | length: 8 16 | }).reverse() 17 | let grids = [] 18 | 19 | for (var i = 0; i < scale.length - 1; i++) { 20 | if (i % 2 === 0) { 21 | grids.push({ 22 | cells: [ 23 | { min: scale[i] }, 24 | { min: scale[i+1] } 25 | ] 26 | }) 27 | } 28 | } 29 | 30 | return ( 31 |
32 |

Similar Ratios

33 | {grids.map(function(grid, i) { 34 | return ( 35 | 38 | ) 39 | })} 40 | 41 | 42 |

43 | Cells with similar ratios will align horizontally when they are set inline. Different `min` values will cause the Cells to collapse at different widths. 44 |

45 |
46 | 47 |
48 |
49 | ) 50 | } 51 | 52 | } 53 | 54 | export default RatiosDemo 55 | 56 | -------------------------------------------------------------------------------- /docs/Readme.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import md from '../README.md' 4 | import Section from './Section.jsx' 5 | 6 | class Readme extends React.Component { 7 | 8 | render () { 9 | let html = { 10 | __html: md 11 | } 12 | let style = { 13 | lineHeight: 1.625, 14 | maxWidth: '40em', 15 | } 16 | return ( 17 |
18 |
20 |
21 | ) 22 | } 23 | 24 | } 25 | 26 | export default Readme 27 | 28 | -------------------------------------------------------------------------------- /docs/Section.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ms from 'simple-modular-scale' 4 | 5 | class Section extends React.Component { 6 | 7 | render () { 8 | let scale = ms() 9 | let style = { 10 | paddingTop: scale[3], 11 | paddingBottom: scale[3], 12 | } 13 | return ( 14 |
15 | {this.props.children} 16 |
17 | ) 18 | } 19 | 20 | } 21 | 22 | export default Section 23 | 24 | -------------------------------------------------------------------------------- /docs/Social.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { TweetButton, GithubButton, CarbonAd } from 'blk' 4 | 5 | class Social extends React.Component { 6 | 7 | render () { 8 | return ( 9 |
10 |
11 | 12 |
13 | 16 |
17 |
18 |
19 | 20 |
21 |
22 | ) 23 | } 24 | } 25 | 26 | export default Social 27 | 28 | -------------------------------------------------------------------------------- /docs/TypographyDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ms from 'simple-modular-scale' 4 | import { Grid, Cell } from '..' 5 | import Section from './Section.jsx' 6 | 7 | class TypographyDemo extends React.Component { 8 | 9 | render () { 10 | let props = this.props 11 | let scale = ms({ 12 | base: props.base, 13 | ratios: [ 9/8, 4/3, 4/3 ], 14 | length: 6 15 | }) 16 | let font = 'Garamond, Baskerville, "Baskerville Old Face", "Hoefler Text", "Times New Roman", serif' 17 | let styles = { 18 | container: { 19 | fontFamily: font 20 | }, 21 | heading: { 22 | fontFamily: font, 23 | fontSize: scale[5] 24 | }, 25 | a: { 26 | fontFamily: font, 27 | fontSize: scale[2], 28 | marginTop: 0 29 | }, 30 | b: { 31 | fontFamily: font, 32 | fontSize: scale[1], 33 | marginTop: 0 34 | }, 35 | c: { 36 | fontFamily: font, 37 | fontSize: scale[0], 38 | marginTop: 0 39 | } 40 | } 41 | return ( 42 |
43 |
44 |

Typography Demo

45 |

46 | In this example, font sizes are based on a modular scale, and each Cell’s min property is set to the font size multiplied by 16. 47 |

48 | 49 | 50 |

{scale[2]}px • {16 * scale[2]}/{32 * scale[2]}px min/max

51 |

52 | {this.props.bacon.substring(0, scale[2] / (1/16) )}... 53 |

54 |
55 | 56 |

{scale[1]}px • {16 * scale[1]}/{32 * scale[1]}px min/max

57 |

58 | {this.props.bacon.substring(0, scale[1] / (1/32))}... 59 |

60 |
61 | 62 |

{scale[0]}px • {16 * scale[0]}px min

63 |

64 | {this.props.bacon} 65 |

66 |
67 | 68 |
69 |
70 |
71 | ) 72 | } 73 | 74 | } 75 | 76 | export default TypographyDemo 77 | 78 | -------------------------------------------------------------------------------- /docs/base.css: -------------------------------------------------------------------------------- 1 | 2 | @import 'blk'; 3 | 4 | -------------------------------------------------------------------------------- /docs/data.js: -------------------------------------------------------------------------------- 1 | 2 | import pkg from '../package.json' 3 | import { capitalize } from 'lodash' 4 | 5 | export default { 6 | name: pkg.name, 7 | title: capitalize(pkg.name), 8 | href: 'http://jxnblk.com/rgx', 9 | description: pkg.description, 10 | links: [ 11 | { href: pkg.homepage, text: 'GitHub' }, 12 | { href: '///npmjs.com/package/' + pkg.name, text: 'npm' }, 13 | ], 14 | homepage: pkg.homepage, 15 | bacon: 'Bacon ipsum dolor amet short loin capicola porchetta pork pork chop cow, tri-tip bresaola tenderloin short ribs picanha drumstick chicken t-bone. Bacon rump tail meatloaf, salami chicken shank swine short loin porchetta shankle kielbasa. Pork chop brisket kevin pancetta bacon, jowl sirloin leberkas. Tenderloin shoulder filet mignon kielbasa cupim brisket turducken tail drumstick. Sausage pig porchetta, pork turkey t-bone fatback kevin. Pork loin bacon rump venison, meatloaf salami doner pig pork belly chicken pancetta jowl leberkas t-bone. Porchetta andouille ham ball tip pork turducken tail pork chop fatback ground round doner t-bone.', 16 | ipsum: 'Leberkas spare ribs swine kevin turkey turducken landjaeger shoulder. Doner tongue bacon, drumstick alcatra beef pork loin swine frankfurter strip steak hamburger meatball. Turducken prosciutto shoulder sausage pastrami pig ham hock, beef ribeye tongue short ribs tri-tip ground round. Turducken brisket sausage prosciutto landjaeger, hamburger drumstick filet mignon ball tip sirloin jerky. Brisket venison hamburger jerky spare ribs, ribeye chicken bacon pig. Tenderloin tail swine cow pastrami tri-tip. Beef ribs meatloaf andouille pork loin ham tail beef, kielbasa alcatra swine tongue hamburger jerky sausage pork belly.', 17 | } 18 | -------------------------------------------------------------------------------- /docs/entry.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import data from './data' 5 | import App from './App' 6 | 7 | ReactDOM.render(, document.querySelector('#app')) 8 | 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | rgx 6 | 7 | 8 |
9 | 10 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rgx", 3 | "version": "0.2.4", 4 | "description": "React grid system – constraint-based responsive grid with no CSS and no media queries", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prepublish": "babel src --out-dir dist", 8 | "dev": "webpack-dev-server --progress --colors --config webpack.dev.config.js", 9 | "docs": "webpack -p --progress --colors", 10 | "start": "npm run dev", 11 | "mocha": "mocha test --compilers js:babel-register", 12 | "karma:watch": "karma start test/karma.config.js --no-single-run", 13 | "karma": "./node_modules/.bin/karma start test/karma.config.js", 14 | "test": "npm run mocha && npm run karma" 15 | }, 16 | "author": "Brent Jackson", 17 | "license": "MIT", 18 | "keywords": [ 19 | "constraint", 20 | "grid", 21 | "layout", 22 | "react", 23 | "react-component", 24 | "style" 25 | ], 26 | "devDependencies": { 27 | "babel-cli": "^6.5.1", 28 | "babel-core": "^6.5.2", 29 | "babel-loader": "^6.2.4", 30 | "babel-preset-es2015": "^6.5.0", 31 | "babel-preset-react": "^6.5.0", 32 | "babel-preset-stage-0": "^6.5.0", 33 | "babel-register": "^6.5.2", 34 | "basscss-color-input-range": "^1.0.0", 35 | "basscss-input-range": "^1.1.5", 36 | "blk": "^3.1.0", 37 | "css-loader": "^0.15.1", 38 | "cssnext-loader": "^1.0.1", 39 | "expect": "^1.10.0", 40 | "file-loader": "^0.8.4", 41 | "html-loader": "^0.3.0", 42 | "json-loader": "^0.5.2", 43 | "karma": "^0.13.9", 44 | "karma-chai": "^0.1.0", 45 | "karma-chai-plugins": "^0.6.0", 46 | "karma-chrome-launcher": "^0.2.0", 47 | "karma-cli": "^0.1.0", 48 | "karma-firefox-launcher": "^0.1.6", 49 | "karma-mocha": "^0.2.0", 50 | "karma-mocha-reporter": "^1.2.3", 51 | "karma-webpack": "^1.7.0", 52 | "lodash": "^4.5.1", 53 | "markdown-loader": "^0.1.3", 54 | "marked": "^0.3.3", 55 | "mocha": "^2.3.2", 56 | "node-libs-browser": "^0.5.2", 57 | "raw-loader": "^0.5.1", 58 | "react": "^0.14.7", 59 | "react-addons-test-utils": "^0.14.7", 60 | "react-dom": "^0.14.7", 61 | "react-hot-loader": "^1.3.0", 62 | "rebass": "^0.1.3", 63 | "simple-modular-scale": "^1.0.2", 64 | "style-loader": "^0.12.3", 65 | "webpack": "^1.10.1", 66 | "webpack-dev-server": "^1.10.1" 67 | }, 68 | "peerDependencies": { 69 | "react": "^0.14.0" 70 | }, 71 | "repository": { 72 | "type": "git", 73 | "url": "git+https://github.com/jxnblk/rgx.git" 74 | }, 75 | "bugs": { 76 | "url": "https://github.com/jxnblk/rgx/issues" 77 | }, 78 | "homepage": "https://github.com/jxnblk/rgx", 79 | "babel": { 80 | "presets": [ 81 | "es2015", 82 | "stage-0", 83 | "react" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Cell.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | /** 5 | * Child component of Grid that displays inline when 6 | * there is enough space in the container 7 | */ 8 | 9 | class Cell extends React.Component { 10 | 11 | render () { 12 | const { inline, width, padding, children } = this.props 13 | const style = { 14 | boxSizing: 'border-box', 15 | display: inline ? 'inline-block' : 'block', 16 | width: inline ? `${width * 100}%` : '100%', 17 | verticalAlign: 'top', 18 | paddingLeft: padding, 19 | paddingRight: padding, 20 | position: 'relative' 21 | } 22 | 23 | return ( 24 |
25 | {children} 26 |
27 | ) 28 | } 29 | } 30 | 31 | Cell.propTypes = { 32 | /** Min-width to display inline */ 33 | min: React.PropTypes.number, 34 | /** Max-width for Cell */ 35 | max: React.PropTypes.number, 36 | /** Width of cell when inline is true - value should be 0–1 */ 37 | width: React.PropTypes.number, 38 | /** Left and right padding for creating gutters */ 39 | padding: React.PropTypes.number, 40 | /** Sets display inline-block and activates width prop */ 41 | inline: React.PropTypes.bool, 42 | } 43 | 44 | Cell.defaultProps = { 45 | min: 640, 46 | max: null, 47 | width: 100, 48 | padding: 0, 49 | inline: false 50 | } 51 | 52 | export default Cell 53 | 54 | -------------------------------------------------------------------------------- /src/Grid.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { throttle } from 'lodash' 4 | 5 | const win = typeof window !== 'undefined' ? window : false 6 | 7 | /** 8 | * Parent component for Cell that calculates available 9 | * width for setting Cells inline. 10 | */ 11 | 12 | class Grid extends React.Component { 13 | 14 | constructor () { 15 | super () 16 | this.updateWidth = this.updateWidth.bind(this) 17 | this.getMinTotal = this.getMinTotal.bind(this) 18 | this.state = { 19 | width: 768 20 | } 21 | } 22 | 23 | updateWidth () { 24 | const el = this.refs.root 25 | const { width } = el.getBoundingClientRect() 26 | this.setState({ width }) 27 | } 28 | 29 | getMinTotal () { 30 | let total = 0 31 | const { children, min } = this.props 32 | React.Children.map(children, (child, i) => { 33 | let childMin = child.props.min || false 34 | if (!childMin) { 35 | childMin = min 36 | } 37 | total += childMin 38 | }) 39 | return total 40 | } 41 | 42 | componentDidMount () { 43 | this.updateWidth() 44 | if (win) { 45 | this.startListeningForResize() 46 | } 47 | } 48 | 49 | componentWillUnmount () { 50 | if (win) { 51 | this.stopListeningForResize() 52 | } 53 | } 54 | 55 | componentDidUpdate (prevProps) { 56 | if (win && prevProps.throttleResize !== this.props.throttleResize) { 57 | this.stopListeningForResize() 58 | this.startListeningForResize() 59 | } 60 | } 61 | 62 | startListeningForResize () { 63 | this.throttledUpdateWidth = throttle(this.updateWidth, this.props.throttleResize) 64 | win.addEventListener('resize', this.throttledUpdateWidth) 65 | } 66 | 67 | stopListeningForResize () { 68 | win.removeEventListener('resize', this.throttledUpdateWidth) 69 | } 70 | 71 | render () { 72 | const { children, gutter } = this.props 73 | const { width } = this.state 74 | const style = { 75 | overflow: 'hidden', 76 | marginLeft: -gutter, 77 | marginRight: -gutter, 78 | position: 'relative' 79 | } 80 | 81 | // min width denominator 82 | const dmin = this.getMinTotal() 83 | // min values of max cells 84 | let maxmins = [] 85 | // max values of max cells 86 | let maxes = [] 87 | 88 | React.Children.map(children, (child) => { 89 | if (child.props.max && child.props.min / dmin * width > child.props.max) { 90 | maxes.push(child.props.max) 91 | maxmins.push(child.props.min) 92 | } 93 | }) 94 | 95 | // sum of max cell values 96 | const maxSum = maxes.length ? maxes.reduce((a, b) => { return a + b }) : 0 97 | // sum of min values for max cells 98 | const maxminSum = maxmins.length ? maxmins.reduce((a, b) => { return a + b }) : 0 99 | // percent offset from remaining min cell widths 100 | const offset = (maxSum / width) / ((children ? children.length : 0) - maxes.length) 101 | const denominator = dmin - maxminSum 102 | 103 | // set child props 104 | const modifiedChildren = React.Children.map(children, (child) => { 105 | let childWidth = child.props.min / denominator - offset 106 | if (child.props.max && child.props.min / dmin * width > child.props.max) { 107 | childWidth = child.props.max / width 108 | } 109 | let childProps = { 110 | width: childWidth, 111 | inline: dmin < width 112 | } 113 | if (!child.props.padding) { 114 | childProps.padding = gutter 115 | } 116 | return React.cloneElement(child, childProps) 117 | }) 118 | 119 | return ( 120 |
123 | {modifiedChildren} 124 |
125 | ) 126 | } 127 | 128 | } 129 | 130 | Grid.propTypes = { 131 | /** Sets a default min prop on child Cell components */ 132 | min: React.PropTypes.number, 133 | /** Sets negative left and right margins to compensate for Cell padding prop */ 134 | gutter: React.PropTypes.number, 135 | /** Milliseconds for throttling window resize listener */ 136 | throttleResize: React.PropTypes.number, 137 | } 138 | 139 | Grid.defaultProps = { 140 | min: 640, 141 | gutter: 0, 142 | throttleResize: 200 143 | } 144 | 145 | export default Grid 146 | 147 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | export { default as Grid } from './Grid' 3 | export { default as Cell } from './Cell' 4 | 5 | -------------------------------------------------------------------------------- /test/Cell.spec.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import TestUtils from 'react-addons-test-utils' 4 | import expect from 'expect' 5 | import Cell from '../src/Cell' 6 | 7 | const renderer = TestUtils.createRenderer() 8 | 9 | describe('Cell', () => { 10 | let cell 11 | 12 | describe('shallow tests', () => { 13 | beforeEach(() => { 14 | renderer.render() 15 | cell = renderer.getRenderOutput() 16 | }) 17 | 18 | it('should render', () => { 19 | expect(cell.type).toEqual('div') 20 | }) 21 | 22 | it('should default to 100% width', () => { 23 | expect(cell.props.style.width).toEqual('100%') 24 | }) 25 | 26 | it('should have no default padding', () => { 27 | expect(cell.props.style.paddingLeft).toEqual(0) 28 | expect(cell.props.style.paddingRight).toEqual(0) 29 | }) 30 | 31 | it('should not be inline by default', () => { 32 | expect(cell.props.inline).toNotExist() 33 | }) 34 | 35 | it('should not have a default max', () => { 36 | expect(cell.props.max).toNotExist() 37 | }) 38 | }) 39 | 40 | describe('browser tests', () => { 41 | if (typeof document === 'undefined') { 42 | return false 43 | } 44 | 45 | beforeEach(() => { 46 | cell = TestUtils.renderIntoDocument( 47 | 48 | Cell 49 | 50 | ) 51 | }) 52 | 53 | it('should render', () => { 54 | expect(cell).toExist() 55 | }) 56 | 57 | it('should be properly styled', () => { 58 | const style = cell.refs.cell.style 59 | expect(style.boxSizing).toEqual('border-box') 60 | expect(style.position).toEqual('relative') 61 | expect(style.paddingLeft).toEqual('16px') 62 | }) 63 | }) 64 | 65 | }) 66 | 67 | -------------------------------------------------------------------------------- /test/Grid.spec.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import TestUtils from 'react-addons-test-utils' 5 | import expect from 'expect' 6 | import Grid from '../src/Grid' 7 | import Cell from '../src/Cell' 8 | 9 | const renderer = TestUtils.createRenderer() 10 | 11 | describe('Grid', () => { 12 | let grid 13 | 14 | describe('shallow tests', () => { 15 | beforeEach(() => { 16 | renderer.render() 17 | grid = renderer.getRenderOutput() 18 | }) 19 | 20 | it('should render', () => { 21 | expect(grid.type).toEqual('div') 22 | }) 23 | 24 | it('should have no default margin', () => { 25 | expect(grid.props.style.marginLeft).toEqual(0) 26 | expect(grid.props.style.marginRight).toEqual(0) 27 | }) 28 | 29 | context('when gutter is set', () => { 30 | beforeEach(() => { 31 | renderer.render() 32 | grid = renderer.getRenderOutput() 33 | }) 34 | 35 | it('should have negative left and right margins', () => { 36 | expect(grid.props.style.marginLeft).toEqual(-16) 37 | expect(grid.props.style.marginRight).toEqual(-16) 38 | }) 39 | }) 40 | 41 | context('with Cell children', () => { 42 | let c1, c2 43 | beforeEach(() => { 44 | renderer.render( 45 | 46 | 47 | 48 | 49 | ) 50 | grid = renderer.getRenderOutput() 51 | c1 = grid.props.children[0] 52 | c2 = grid.props.children[1] 53 | }) 54 | 55 | it('should render children Cells', () => { 56 | expect(grid.props.children.length).toEqual(2) 57 | }) 58 | 59 | it('should set Cells’ inline prop', () => { 60 | expect(c1.props.inline).toEqual(true) 61 | expect(c2.props.inline).toEqual(true) 62 | }) 63 | 64 | it('should set Cells’ widths as ratios of default width', () => { 65 | expect(c1.props.width).toEqual(256 / 768) 66 | expect(c2.props.width).toEqual(512 / 768) 67 | }) 68 | }) 69 | 70 | context('with gutter and Cell children', () => { 71 | let c1, c2 72 | beforeEach(() => { 73 | renderer.render( 74 | 75 | 76 | 77 | 78 | ) 79 | grid = renderer.getRenderOutput() 80 | c1 = grid.props.children[0] 81 | c2 = grid.props.children[1] 82 | }) 83 | 84 | it('should set negative left and right margins', () => { 85 | expect(grid.props.style.marginLeft).toEqual(-16) 86 | expect(grid.props.style.marginRight).toEqual(-16) 87 | }) 88 | 89 | it('should render children Cells', () => { 90 | expect(grid.props.children.length).toEqual(2) 91 | }) 92 | 93 | it('should set Cells’ inline prop', () => { 94 | expect(c1.props.inline).toEqual(true) 95 | expect(c2.props.inline).toEqual(true) 96 | }) 97 | 98 | it('should set Cells’ widths as ratios of default width', () => { 99 | expect(c1.props.width).toEqual(256 / 768) 100 | expect(c2.props.width).toEqual(512 / 768) 101 | }) 102 | 103 | it('should set Cells’ padding', () => { 104 | expect(c1.props.padding).toEqual(16) 105 | expect(c2.props.padding).toEqual(16) 106 | }) 107 | }) 108 | 109 | context('with gutter and Cell children with padding', () => { 110 | let c1, c2 111 | beforeEach(() => { 112 | renderer.render( 113 | 114 | 115 | 116 | 117 | ) 118 | grid = renderer.getRenderOutput() 119 | c1 = grid.props.children[0] 120 | c2 = grid.props.children[1] 121 | }) 122 | 123 | it('should set negative left and right margins', () => { 124 | expect(grid.props.style.marginLeft).toEqual(-16) 125 | expect(grid.props.style.marginRight).toEqual(-16) 126 | }) 127 | 128 | it('should render children Cells', () => { 129 | expect(grid.props.children.length).toEqual(2) 130 | }) 131 | 132 | it('should set Cells’ inline prop', () => { 133 | expect(c1.props.inline).toEqual(true) 134 | expect(c2.props.inline).toEqual(true) 135 | }) 136 | 137 | it('should set Cells’ widths as ratios of default width', () => { 138 | expect(c1.props.width).toEqual(256 / 768) 139 | expect(c2.props.width).toEqual(512 / 768) 140 | }) 141 | 142 | it('should not set first Cell’s padding', () => { 143 | expect(c1.props.padding).toEqual(32) 144 | }) 145 | 146 | it('should set second Cell’s padding', () => { 147 | expect(c2.props.padding).toEqual(16) 148 | }) 149 | }) 150 | }) 151 | 152 | 153 | describe('browser tests', () => { 154 | if (typeof document === 'undefined') { 155 | return false 156 | } 157 | 158 | let div = document.createElement('div'), 159 | c1, 160 | c2 161 | 162 | document.body.appendChild(div) 163 | 164 | context('at wider width', () => { 165 | beforeEach(() => { 166 | grid = ReactDOM.render( 167 | 168 | 169 | Cell 192 170 | 171 | 172 | Cell 576 173 | 174 | , 175 | div 176 | ) 177 | const cells = TestUtils.scryRenderedComponentsWithType(grid, Cell) 178 | c1 = cells[0] 179 | c2 = cells[1] 180 | }) 181 | 182 | afterEach(() => { 183 | ReactDOM.unmountComponentAtNode(div) 184 | }) 185 | 186 | it('should render', () => { 187 | expect(grid).toExist() 188 | }) 189 | 190 | it('should have negative margins', () => { 191 | const el = grid.refs.root 192 | expect(el.style.marginLeft).toEqual('-32px') 193 | expect(el.style.marginRight).toEqual('-32px') 194 | }) 195 | 196 | it('should render children', () => { 197 | expect(c1).toExist() 198 | expect(c2).toExist() 199 | }) 200 | 201 | it('should set padding on children', () => { 202 | expect(c1.props.padding).toEqual(32) 203 | expect(c2.props.padding).toEqual(32) 204 | }) 205 | 206 | it('should style padding on children', () => { 207 | expect(c1.refs.cell.style.paddingLeft).toEqual('32px') 208 | expect(c1.refs.cell.style.paddingRight).toEqual('32px') 209 | expect(c2.refs.cell.style.paddingLeft).toEqual('32px') 210 | expect(c2.refs.cell.style.paddingRight).toEqual('32px') 211 | }) 212 | 213 | it('should set children inline', () => { 214 | expect(c1.props.inline).toEqual(true) 215 | expect(c2.props.inline).toEqual(true) 216 | }) 217 | 218 | it('should set correct widths for children', () => { 219 | expect(c1.props.width).toEqual(192/768) 220 | expect(c2.props.width).toEqual(576/768) 221 | }) 222 | 223 | it('should style widths on children', () => { 224 | expect(c1.refs.cell.style.width).toEqual('25%') 225 | expect(c2.refs.cell.style.width).toEqual('75%') 226 | }) 227 | }) 228 | 229 | context('when in a smaller width container', () => { 230 | beforeEach((done) => { 231 | div.style.width = '512px' 232 | grid = ReactDOM.render( 233 | 234 | 235 | Cell 192 236 | 237 | 238 | Cell 576 239 | 240 | , 241 | div 242 | ) 243 | window.setTimeout(() => { 244 | const cells = TestUtils.scryRenderedComponentsWithType(grid, Cell) 245 | c1 = cells[0] 246 | c2 = cells[1] 247 | done() 248 | }, 100, this) 249 | }) 250 | 251 | afterEach(() => { 252 | ReactDOM.unmountComponentAtNode(div) 253 | }) 254 | 255 | it('should set padding on children', () => { 256 | expect(c1.props.padding).toEqual(32) 257 | expect(c2.props.padding).toEqual(32) 258 | }) 259 | 260 | it('should not set children inline', () => { 261 | expect(c1.props.inline).toEqual(false) 262 | expect(c2.props.inline).toEqual(false) 263 | }) 264 | 265 | it('should style children at full-width', () => { 266 | expect(c1.refs.cell.style.width).toEqual('100%') 267 | expect(c2.refs.cell.style.width).toEqual('100%') 268 | }) 269 | 270 | it('should have the Cells stacked', () => { 271 | const y1 = c1.refs.cell.getBoundingClientRect().top 272 | const y2 = c2.refs.cell.getBoundingClientRect().top 273 | expect(y1 < y2).toEqual(true) 274 | }) 275 | }) 276 | }) 277 | }) 278 | 279 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | import './Grid.spec' 3 | import './Cell.spec' 4 | 5 | // var context = require.context('.', true, /.+\.spec\.jsx?$/) 6 | // context.keys().forEach(context) 7 | // module.exports = context 8 | 9 | -------------------------------------------------------------------------------- /test/karma.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (config) { 3 | config.set({ 4 | browsers: [ 5 | // 'Chrome', 6 | 'Firefox' 7 | ], 8 | 9 | files: [ 10 | 'index.js' 11 | ], 12 | 13 | frameworks: [ 'chai', 'mocha' ], 14 | 15 | plugins: [ 16 | 'karma-chrome-launcher', 17 | 'karma-firefox-launcher', 18 | 'karma-chai', 19 | 'karma-mocha', 20 | 'karma-mocha-reporter', 21 | 'karma-webpack', 22 | ], 23 | 24 | preprocessors: { 25 | 'index.js': [ 26 | 'webpack' 27 | ] 28 | }, 29 | reporters: [ 30 | 'mocha' 31 | ], 32 | singleRun: true, 33 | 34 | webpack: { 35 | module: { 36 | loaders: [ 37 | { 38 | test: /\.jsx?$/, 39 | exclude: /node_modules/, 40 | loader: 'babel' 41 | } 42 | ], 43 | }, 44 | resolve: { 45 | extensions: ['', '.js', '.jsx'] 46 | } 47 | }, 48 | 49 | webpackMiddleware: { 50 | noInfo: true, 51 | } 52 | 53 | }) 54 | } 55 | 56 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | 6 | entry: [ 7 | './docs/entry.js' 8 | ], 9 | 10 | output: { 11 | filename: 'bundle.js', 12 | publicPath: '/docs/', 13 | path: __dirname + '/docs' 14 | }, 15 | 16 | module: { 17 | loaders: [ 18 | { 19 | test: /(\.js$|\.jsx?$)/, 20 | exclude: /node_modules/, 21 | loaders: [ 22 | 'react-hot', 23 | 'babel' 24 | ] 25 | }, 26 | { test: /\.json/, loader: 'json' }, 27 | { test: /\.md/, loader: 'html!markdown-loader' }, 28 | { test: /\.css$/, loader: 'style!css!cssnext' } 29 | ] 30 | }, 31 | 32 | resolve: { 33 | extensions: ['', '.js', '.jsx'] 34 | }, 35 | 36 | cssnext: { 37 | features: { 38 | customProperties: { 39 | variables: { 40 | 'font-family': '"SF UI Text", "Helvetica Neue", sans-serif', 41 | 'bold-font-weight': 500, 42 | 'heading-font-weight': 500, 43 | } 44 | } 45 | } 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | 2 | require('babel-register')() 3 | 4 | var _ = require('lodash') 5 | var config = require('./webpack.config.js') 6 | var webpack = require('webpack') 7 | 8 | module.exports = _.extend(config, { 9 | 10 | entry: [ 11 | 'webpack-dev-server/client?http://localhost:8080/', 12 | 'webpack/hot/only-dev-server', 13 | './docs/entry.js' 14 | ], 15 | 16 | plugins: [ 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | 20 | devServer: { 21 | hot: true 22 | } 23 | 24 | }) 25 | 26 | --------------------------------------------------------------------------------