├── .prettierignore ├── .flowconfig ├── .gitignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .travis.yml ├── src ├── Functions │ ├── floatDirection.js │ ├── stripUnit.js │ ├── floatOppositeDirection.js │ ├── parseMedia.js │ ├── stripUnit.test.js │ ├── parseUnit.js │ ├── index.js │ ├── parseMedia.test.js │ ├── columnWidth.test.js │ ├── floatDirection.test.js │ ├── floatOppositeDirection.test.js │ ├── columnWidth.js │ └── parseUnit.test.js ├── Mixins │ ├── gridContainer.js │ ├── gridMedia.js │ ├── gridContainer.test.js │ ├── gridVisual.js │ ├── gridColumn.js │ ├── gridColumn.test.js │ ├── gridShift.js │ ├── gridPush.test.js │ ├── gridCollapse.js │ ├── gridPush.js │ ├── gridCollapse.test.js │ ├── gridMedia.test.js │ ├── gridShift.test.js │ └── gridVisual.test.js ├── index.js ├── Theme │ ├── NeatTheme.js │ ├── Neat.js │ └── Neat.test.js ├── test.js └── Hooks │ └── useTheme.js ├── .babelrc ├── LICENSE ├── rollup.config.js ├── stories └── index.js ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | coverage/ 3 | docs/ 4 | node_modules/ 5 | bundle.js -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-storysource/register' 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | before_script: 5 | - npm i 6 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addDecorator, configure } from '@storybook/react' 2 | 3 | const loadStories = () => { 4 | require('../stories') 5 | } 6 | 7 | configure(loadStories, module) 8 | -------------------------------------------------------------------------------- /src/Functions/floatDirection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const floatDirection: Function = (direction: string = 'ltr'): string => { 3 | return direction === 'rtl' ? 'right' : 'left' 4 | } 5 | 6 | export default floatDirection 7 | -------------------------------------------------------------------------------- /src/Functions/stripUnit.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const stripUnit: Function = (value: string): number | string => { 3 | const unitlessValue = parseFloat(value) 4 | if (isNaN(unitlessValue)) return value 5 | return unitlessValue 6 | } 7 | 8 | export default stripUnit 9 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | exclude: /node_modules/, 6 | test: /\.js$/, 7 | loaders: [require.resolve('@storybook/addon-storysource/loader')], 8 | enforce: 'pre' 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Functions/floatOppositeDirection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const floatOppositeDirection: Function = ( 3 | direction: string = 'ltr' 4 | ): ?string => { 5 | if (direction === 'ltr') { 6 | return 'right' 7 | } 8 | if (direction === 'rtl') { 9 | return 'left' 10 | } 11 | } 12 | 13 | export default floatOppositeDirection 14 | -------------------------------------------------------------------------------- /src/Mixins/gridContainer.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const gridContainer: Function = (parent: string = '&'): Object => { 3 | const psuedoSelector = `${parent}::after` 4 | return { 5 | [psuedoSelector]: { 6 | clear: 'both', 7 | content: '""', 8 | display: 'block' 9 | } 10 | } 11 | } 12 | 13 | export default gridContainer 14 | -------------------------------------------------------------------------------- /src/Functions/parseMedia.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const parseMedia: Function = (media: string | number): string | boolean => { 3 | if (typeof media === 'number') { 4 | return `only screen and (min-width: ${media}px)` 5 | } 6 | if (typeof media === 'string') { 7 | return media 8 | } 9 | return false 10 | } 11 | 12 | export default parseMedia 13 | -------------------------------------------------------------------------------- /src/Functions/stripUnit.test.js: -------------------------------------------------------------------------------- 1 | import stripUnit from './stripUnit' 2 | 3 | describe('stripUnit()', () => { 4 | it('should return 12 (int)', () => { 5 | let result = stripUnit('12px') 6 | expect(result).toBe(12) 7 | }) 8 | it('should return px', () => { 9 | let result = stripUnit('px') 10 | expect(result).toBe('px') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /src/Functions/parseUnit.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const parseUnit: Function = (value: string): string => { 3 | let parsedValue = parseFloat(value) 4 | if (parsedValue) { 5 | let splitValue = value.split(parsedValue.toString(10)) 6 | return splitValue[splitValue.length - 1].trim() 7 | } else { 8 | return '' 9 | } 10 | } 11 | 12 | export default parseUnit 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Neat from './Theme/Neat' 2 | export gridCollapse from './Mixins/gridCollapse' 3 | export gridColumn from './Mixins/gridColumn' 4 | export gridContainer from './Mixins/gridContainer' 5 | export gridMedia from './Mixins/gridMedia' 6 | export gridPush from './Mixins/gridPush' 7 | export gridShift from './Mixins/gridShift' 8 | export gridVisual from './Mixins/gridVisual' 9 | 10 | export default Neat 11 | -------------------------------------------------------------------------------- /src/Functions/index.js: -------------------------------------------------------------------------------- 1 | import columnWidth from './columnWidth' 2 | import floatDirection from './floatDirection' 3 | import floatOppositeDirection from './floatOppositeDirection' 4 | import parseMedia from './parseMedia' 5 | import parseUnit from './parseUnit' 6 | import stripUnit from './stripUnit' 7 | 8 | export { 9 | columnWidth, 10 | floatDirection, 11 | floatOppositeDirection, 12 | parseMedia, 13 | parseUnit, 14 | stripUnit 15 | } 16 | -------------------------------------------------------------------------------- /src/Mixins/gridMedia.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { parseMedia } from '../Functions' 3 | import { css } from 'styled-components' 4 | import { NeatTheme } from '../Theme/NeatTheme' 5 | 6 | let gridMedia: Function = ( 7 | theme: typeof NeatTheme, 8 | ...args: Array 9 | ): Array => { 10 | const { media } = theme 11 | if (!media) return [] 12 | return css` 13 | @media ${parseMedia(media)} { 14 | ${css(...args)} 15 | } 16 | ` 17 | } 18 | 19 | export default gridMedia 20 | -------------------------------------------------------------------------------- /src/Theme/NeatTheme.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const Directions: { [key: string]: string } = { 3 | LTR: 'LTR', 4 | RTL: 'RTL' 5 | } 6 | 7 | export type Direction = $Values 8 | 9 | export type Theme = { 10 | color: ?string, 11 | columns: number, 12 | direction: Direction, 13 | gutter: string, 14 | media: ?string | ?number 15 | } 16 | export const NeatTheme: Theme = { 17 | color: null, 18 | columns: 12, 19 | direction: Directions.LTR, 20 | gutter: '20px', 21 | media: null 22 | } 23 | -------------------------------------------------------------------------------- /src/Theme/Neat.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { Directions, NeatTheme } from './NeatTheme' 3 | import type { Theme } from './NeatTheme' 4 | 5 | const Neat: Function = (theme: Theme = NeatTheme): Theme => ({ 6 | color: theme.color || NeatTheme.color, 7 | columns: theme.columns || NeatTheme.columns, 8 | direction: 9 | theme.direction === Directions.LTR || theme.direction === Directions.RTL 10 | ? theme.direction 11 | : NeatTheme.direction, 12 | gutter: theme.gutter || NeatTheme.gutter, 13 | media: theme.media || NeatTheme.media 14 | }) 15 | 16 | export default Neat 17 | -------------------------------------------------------------------------------- /src/Functions/parseMedia.test.js: -------------------------------------------------------------------------------- 1 | import parseMedia from './parseMedia' 2 | 3 | describe('parseMedia()', () => { 4 | it('should handle no argument', () => { 5 | let result = parseMedia() 6 | expect(result).toBe(false) 7 | }) 8 | it('should handle numbers', () => { 9 | let result = parseMedia(12) 10 | expect(result).toBe('only screen and (min-width: 12px)') 11 | }) 12 | it('should handle strings', () => { 13 | let media = 'only screen and handle (max-width: 300px)' 14 | let result = parseMedia(media) 15 | expect(result).toBe(media) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/Functions/columnWidth.test.js: -------------------------------------------------------------------------------- 1 | import columnWidth from './columnWidth' 2 | 3 | describe('columnWidth()', () => { 4 | it('should handle a gutter of 0', () => { 5 | let result = columnWidth({ columns: 10, gutter: 0 }, 2) 6 | expect(result).toBe('20%') 7 | }) 8 | 9 | it('should handle a non-zero gutter', () => { 10 | let result = columnWidth({ columns: 10, gutter: '10px' }, 2) 11 | expect(result).toBe('20% - 12px') 12 | }) 13 | 14 | it('should handle a malformed grid', () => { 15 | let result = columnWidth({}, 2) 16 | expect(result).toBe('0') 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/Mixins/gridContainer.test.js: -------------------------------------------------------------------------------- 1 | import gridContainer from './gridContainer' 2 | 3 | describe('gridContainer()', () => { 4 | it('should return the correct object', () => { 5 | const result = gridContainer() 6 | expect('&::after' in result).toBe(true) 7 | expect(result).toEqual({ 8 | '&::after': { 9 | clear: 'both', 10 | content: '""', 11 | display: 'block' 12 | } 13 | }) 14 | }) 15 | it('should return the correct object', () => { 16 | const result = gridContainer('test') 17 | expect('test::after' in result).toBe(true) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/Functions/floatDirection.test.js: -------------------------------------------------------------------------------- 1 | import floatDirection from './floatDirection' 2 | 3 | describe('floatDirection()', () => { 4 | it('should handle no data', () => { 5 | let result = floatDirection() 6 | expect(result).toBe('left') 7 | }) 8 | 9 | it('should handle ltr', () => { 10 | let result = floatDirection('ltr') 11 | expect(result).toBe('left') 12 | }) 13 | 14 | it('should handle rtl', () => { 15 | let result = floatDirection('rtl') 16 | expect(result).toBe('right') 17 | }) 18 | 19 | it('should handle unexpected data', () => { 20 | let result = floatDirection('test') 21 | expect(result).toBe('left') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/Mixins/gridVisual.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { columnWidth } from '../Functions' 3 | import { NeatTheme } from '../Theme/NeatTheme' 4 | 5 | type Styles = { 6 | 'background-image'?: string 7 | } 8 | 9 | let gridVisual: Function = (theme: typeof NeatTheme): Styles => { 10 | let { color, gutter } = theme 11 | if (!gutter) return {} 12 | color = color || '' 13 | return { 14 | 'background-image': ` 15 | repeating-linear-gradient( 16 | to right, transparent, transparent ${gutter}, 17 | ${color} ${gutter}, 18 | ${color} calc(${columnWidth(theme, 1)} + ${gutter}) 19 | ) 20 | ` 21 | } 22 | } 23 | 24 | export default gridVisual 25 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | import Neat, { 2 | gridCollapse, 3 | gridColumn, 4 | gridContainer, 5 | gridMedia, 6 | gridPush, 7 | gridShift, 8 | gridVisual 9 | } from './' 10 | 11 | describe('API', () => { 12 | it('should export the API correctly', () => { 13 | expect(typeof Neat).toBe('function') 14 | expect(typeof gridCollapse).toBe('function') 15 | expect(typeof gridColumn).toBe('function') 16 | expect(typeof gridContainer).toBe('function') 17 | expect(typeof gridMedia).toBe('function') 18 | expect(typeof gridPush).toBe('function') 19 | expect(typeof gridShift).toBe('function') 20 | expect(typeof gridVisual).toBe('function') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/Mixins/gridColumn.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { columnWidth, floatDirection } from '../Functions' 3 | import type { Theme } from '../Theme/NeatTheme' 4 | 5 | type Styles = { 6 | width: string, 7 | float: string, 8 | 'margin-left'?: string, 9 | 'margin-right'?: string 10 | } 11 | 12 | let gridColumn: Function = (theme: Theme, span: number): Styles => { 13 | const { columns, direction, gutter } = theme 14 | span = Math.floor(span) 15 | if (span > columns) { 16 | span = columns 17 | } 18 | return { 19 | width: `calc(${columnWidth(theme, span)})`, 20 | float: `${floatDirection(direction)}`, 21 | [`margin-${floatDirection(direction)}`]: gutter 22 | } 23 | } 24 | 25 | export default gridColumn 26 | -------------------------------------------------------------------------------- /src/Functions/floatOppositeDirection.test.js: -------------------------------------------------------------------------------- 1 | import floatOppositeDirection from './floatOppositeDirection' 2 | 3 | describe('floatOppositeDirection', () => { 4 | it('should handle no data', () => { 5 | let result = floatOppositeDirection() 6 | expect(result).toBe('right') 7 | }) 8 | 9 | it('should handle ltr', () => { 10 | let result = floatOppositeDirection('ltr') 11 | expect(result).toBe('right') 12 | }) 13 | 14 | it('should handle rtl', () => { 15 | let result = floatOppositeDirection('rtl') 16 | expect(result).toBe('left') 17 | }) 18 | 19 | it('should handle unexpected data', () => { 20 | let result = floatOppositeDirection('test') 21 | expect(result).toBe(undefined) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/Functions/columnWidth.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import parseUnit from './parseUnit' 3 | import stripUnit from './stripUnit' 4 | import { NeatTheme } from '../Theme/NeatTheme' 5 | 6 | const columnWidth: Function = ( 7 | theme: typeof NeatTheme, 8 | span: number 9 | ): string => { 10 | const { columns, gutter } = theme 11 | if (!columns || gutter === undefined) return '0' 12 | let ratio = span / columns 13 | let gutterValue = stripUnit(gutter) 14 | let gutterUnit = parseUnit(gutter) 15 | if (gutterValue === 0) { 16 | return `${ratio * 100}%` 17 | } else { 18 | let affordance = `${gutterValue + gutterValue * ratio}${gutterUnit}` 19 | return `${ratio * 100}% - ${affordance}` 20 | } 21 | } 22 | 23 | export default columnWidth 24 | -------------------------------------------------------------------------------- /src/Functions/parseUnit.test.js: -------------------------------------------------------------------------------- 1 | import parseUnit from './parseUnit' 2 | 3 | describe('parseUnit()', () => { 4 | it('should handle the typical use case', () => { 5 | let result = parseUnit('12px') 6 | expect(result).toBe('px') 7 | }) 8 | 9 | it('should handle a space between the number and unit', () => { 10 | let result = parseUnit('12 px') 11 | expect(result).toBe('px') 12 | }) 13 | 14 | it('should handle no unit', () => { 15 | let result = parseUnit('12') 16 | expect(result).toBe('') 17 | }) 18 | 19 | it('should handle no data', () => { 20 | let result = parseUnit() 21 | expect(result).toBe('') 22 | }) 23 | }) 24 | 25 | test('should handle % signs', () => { 26 | let result = parseUnit('5%') 27 | expect(result).toBe('%') 28 | }) 29 | -------------------------------------------------------------------------------- /src/Mixins/gridColumn.test.js: -------------------------------------------------------------------------------- 1 | import { columnWidth } from '../Functions' 2 | import gridColumn from './gridColumn' 3 | import Neat from '../Theme/Neat' 4 | 5 | describe('gridColumn()', () => { 6 | it('should return the correct object', () => { 7 | const result = gridColumn(Neat(), 2) 8 | const calculatedWidth = columnWidth(Neat(), 2) 9 | expect(result).toEqual({ 10 | width: `calc(${calculatedWidth})`, 11 | float: 'left', 12 | 'margin-left': '20px' 13 | }) 14 | }) 15 | it('should return the correct object', () => { 16 | const result = gridColumn(Neat(), 14) 17 | const calculatedWidth = columnWidth(Neat(), 12) 18 | expect(result).toEqual({ 19 | width: `calc(${calculatedWidth})`, 20 | float: 'left', 21 | 'margin-left': '20px' 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/Mixins/gridShift.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { columnWidth, floatDirection } from '../Functions' 3 | import { NeatTheme } from '../Theme/NeatTheme' 4 | 5 | type Styles = { 6 | left?: string, 7 | right?: string, 8 | position?: string 9 | } 10 | 11 | const gridShift: Function = ( 12 | theme: typeof NeatTheme, 13 | shift: number = 0 14 | ): Styles => { 15 | const { direction, gutter } = theme 16 | if (!direction || gutter === undefined) return {} 17 | if (shift > 0) { 18 | let width = columnWidth(theme, shift) 19 | return { 20 | [`${floatDirection(direction)}`]: ` 21 | calc(${width} + ${gutter}) 22 | `, 23 | position: 'relative' 24 | } 25 | } else { 26 | return { 27 | [`${floatDirection(direction)}`]: gutter 28 | } 29 | } 30 | } 31 | 32 | export default gridShift 33 | -------------------------------------------------------------------------------- /src/Mixins/gridPush.test.js: -------------------------------------------------------------------------------- 1 | import gridPush from './gridPush' 2 | import Neat from '../Theme/Neat' 3 | import { columnWidth } from '../Functions' 4 | 5 | describe('gridPush()', () => { 6 | it('should should handle a push of 0', () => { 7 | let result = gridPush(Neat(), 0) 8 | expect('margin-left' in result).toBe(true) 9 | expect(result['margin-left']).toBe('20px') 10 | }) 11 | it('should should handle no push', () => { 12 | let result = gridPush(Neat()) 13 | expect('margin-left' in result).toBe(true) 14 | expect(result['margin-left']).toBe('20px') 15 | }) 16 | it('should handle a push greater than 0', () => { 17 | let result = gridPush(Neat(), 2) 18 | let calculatedWidth = columnWidth(Neat(), 2) 19 | expect(result['margin-left'].trim()).toEqual( 20 | `calc(${calculatedWidth} + 40px)` 21 | ) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/Mixins/gridCollapse.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { 3 | floatDirection, 4 | floatOppositeDirection, 5 | parseUnit, 6 | stripUnit 7 | } from '../Functions' 8 | import type { Theme } from '../Theme/NeatTheme' 9 | 10 | type Styles = { 11 | 'margin-left'?: string, 12 | 'margin-right'?: string, 13 | width?: string 14 | } 15 | 16 | const gridCollapse: Function = (theme: Theme): Styles => { 17 | const { direction, gutter } = theme 18 | if (!direction || !gutter) return {} 19 | let gutterUnit = parseUnit(gutter) 20 | if (gutterUnit === '%') return {} 21 | let gutterValue = stripUnit(gutter) 22 | return { 23 | [`margin-${floatDirection(direction)}`]: `-${gutter}`, 24 | [`margin-${floatOppositeDirection(direction)}`]: `-${gutter}`, 25 | width: `calc(100% + ${gutterValue * 2}${gutterUnit})` 26 | } 27 | } 28 | 29 | export default gridCollapse 30 | -------------------------------------------------------------------------------- /src/Mixins/gridPush.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { columnWidth, floatDirection, parseUnit, stripUnit } from '../Functions' 3 | import { NeatTheme } from '../Theme/NeatTheme' 4 | 5 | type Styles = { 6 | 'margin-left'?: string, 7 | 'margin-right'?: string 8 | } 9 | 10 | let gridPush: Function = ( 11 | theme: typeof NeatTheme, 12 | push: number = 0 13 | ): Styles => { 14 | const { direction, gutter } = theme 15 | if (push > 0) { 16 | let gutterValue = stripUnit(gutter) 17 | let gutterUnit = parseUnit(gutter) 18 | let affordance = `${gutterValue * 2}${gutterUnit}` 19 | return { 20 | [`margin-${floatDirection(direction)}`]: ` 21 | calc(${columnWidth(theme, push)} + ${affordance}) 22 | ` 23 | } 24 | } else { 25 | return { 26 | [`margin-${floatDirection(direction)}`]: gutter 27 | } 28 | } 29 | } 30 | 31 | export default gridPush 32 | -------------------------------------------------------------------------------- /src/Hooks/useTheme.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NeatTheme } from '../Theme/NeatTheme' 3 | 4 | /** 5 | * Eventually this will replace the Theme provider. 6 | * @param props 7 | */ 8 | export const useTheme = props => { 9 | const [color, setColor] = React.useState(props.color ?? NeatTheme.color) 10 | const [columns, setColumns] = React.useState( 11 | props.columns ?? NeatTheme.columns 12 | ) 13 | const [direction, setDirection] = React.useState( 14 | props.direction ?? NeatTheme.direction 15 | ) 16 | const [gutter, setGutter] = React.useState(props.gutter ?? NeatTheme.gutter) 17 | const [media, setMedia] = React.useState(props.media ?? NeatTheme.media) 18 | return { 19 | color, 20 | columns, 21 | direction, 22 | gutter, 23 | media, 24 | setColor, 25 | setColumns, 26 | setDirection, 27 | setGutter, 28 | setMedia 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "forceAllTransforms": true 8 | } 9 | ], 10 | "@babel/preset-flow", 11 | "@babel/preset-react" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-syntax-dynamic-import", 15 | "@babel/plugin-transform-modules-commonjs", 16 | "@babel/plugin-transform-runtime", 17 | "@babel/plugin-proposal-export-default-from", 18 | "@babel/plugin-proposal-object-rest-spread", 19 | "@babel/plugin-transform-arrow-functions", 20 | "@babel/plugin-transform-spread", 21 | "@babel/plugin-transform-destructuring", 22 | "@babel/plugin-transform-parameters", 23 | "@babel/plugin-transform-block-scoping", 24 | "@babel/plugin-transform-computed-properties", 25 | "@babel/plugin-transform-template-literals", 26 | "@babel/plugin-proposal-nullish-coalescing-operator", 27 | "@babel/plugin-proposal-optional-chaining" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/Mixins/gridCollapse.test.js: -------------------------------------------------------------------------------- 1 | import gridCollapse from './gridCollapse' 2 | 3 | describe('gridCollapse()', () => { 4 | it('should return the correct object', () => { 5 | let result = gridCollapse({ 6 | direction: 'ltr', 7 | gutter: '20px' 8 | }) 9 | expect(result).toEqual({ 10 | 'margin-left': '-20px', 11 | 'margin-right': '-20px', 12 | width: `calc(100% + 40px)` 13 | }) 14 | }) 15 | it('should return an empty object if % is passed into the gutter', () => { 16 | let result = gridCollapse({ 17 | direction: 'ltr', 18 | gutter: '20%' 19 | }) 20 | expect(result).toEqual({}) 21 | }) 22 | it('should return an empty object when direction or columns are undefined', () => { 23 | let result = gridCollapse({ 24 | direction: undefined, 25 | gutter: '20px' 26 | }) 27 | expect(result).toEqual({}) 28 | result = gridCollapse({ 29 | driection: 'ltr', 30 | gutter: undefined 31 | }) 32 | expect(result).toEqual({}) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Brandon Tom 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Mixins/gridMedia.test.js: -------------------------------------------------------------------------------- 1 | import gridMedia from './gridMedia' 2 | import Neat from '../Theme/Neat' 3 | 4 | describe('gridMedia()', () => { 5 | it('should handle numeric input', () => { 6 | let result = gridMedia( 7 | Neat({ 8 | media: 500 9 | }), 10 | [{ backgroundColor: 'blue' }] 11 | ) 12 | expect(Array.isArray(result)).toBe(true) 13 | expect(result.join('').replace(/\s+/g, ' ').trim()).toEqual( 14 | ` 15 | @media only screen and (min-width: 500px) { background-color: blue; } 16 | `.trim() 17 | ) 18 | }) 19 | it('should handle string input', () => { 20 | let result = gridMedia( 21 | Neat({ 22 | media: 'only screen and (max-width: 100px)' 23 | }), 24 | [{ backgroundColor: 'red' }] 25 | ) 26 | expect(Array.isArray(result)).toBe(true) 27 | expect(result.join('').replace(/\s+/g, ' ').trim()).toEqual( 28 | ` 29 | @media only screen and (max-width: 100px) { background-color: red; } 30 | `.trim() 31 | ) 32 | }) 33 | it('should return an empty array when no media is passed', () => { 34 | let result = gridMedia(Neat({}), [{ backgroundColor: 'red' }]) 35 | expect(Array.isArray(result)).toBe(true) 36 | expect(result).toEqual([]) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/Mixins/gridShift.test.js: -------------------------------------------------------------------------------- 1 | import gridShift from './gridShift' 2 | import Neat from '../Theme/Neat' 3 | import { columnWidth } from '../Functions' 4 | 5 | describe('gridShift()', () => { 6 | it('should handle a shift of 0', () => { 7 | let result = gridShift(Neat(), 0) 8 | expect('left' in result).toBe(true) 9 | expect(result.left).toBe('20px') 10 | }) 11 | it('should handle no shift', () => { 12 | let result = gridShift(Neat()) 13 | expect('left' in result).toBe(true) 14 | expect(result.left).toBe('20px') 15 | }) 16 | it('should handle a push greater than 0', () => { 17 | let result = gridShift(Neat(), 2) 18 | let calculatedWidth = columnWidth(Neat(), 2) 19 | expect(result['left'].trim()).toEqual(`calc(${calculatedWidth} + 20px)`) 20 | }) 21 | it('should handle no direction', () => { 22 | let result = gridShift({ 23 | color: null, 24 | columns: 12, 25 | direction: null, 26 | gutter: '20px', 27 | media: null 28 | }) 29 | expect(result).toEqual({}) 30 | }) 31 | it('should handle no direction', () => { 32 | let result = gridShift({ 33 | color: null, 34 | columns: 12, 35 | direction: 'ltr', 36 | gutter: undefined, 37 | media: null 38 | }) 39 | expect(result).toEqual({}) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/Mixins/gridVisual.test.js: -------------------------------------------------------------------------------- 1 | import gridVisual from './gridVisual' 2 | import { columnWidth } from '../Functions' 3 | import Neat from '../Theme/Neat' 4 | 5 | describe('gridVisual()', () => { 6 | it('should return the correct object', () => { 7 | let color = 'blue' 8 | let result = gridVisual(Neat({ color })) 9 | expect(result.hasOwnProperty('background-image')).toBe(true) 10 | expect(result['background-image'].replace(/\s+/g, ' ').trim()).toEqual( 11 | ` 12 | repeating-linear-gradient( 13 | to right, transparent, transparent 20px, ${color} 20px, 14 | ${color} calc(${columnWidth(Neat(), 1)} + 20px) 15 | )` 16 | .replace(/\s+/g, ' ') 17 | .trim() 18 | ) 19 | }) 20 | it('should return an empty object if there is no gutter', () => { 21 | let result = gridVisual({ 22 | color: null, 23 | columns: 12, 24 | direction: 'ltr', 25 | gutter: null, 26 | media: null 27 | }) 28 | expect(result).toEqual({}) 29 | }) 30 | it('should return the correct object', () => { 31 | let result = gridVisual(Neat()) 32 | expect(result.hasOwnProperty('background-image')).toBe(true) 33 | expect(result['background-image'].replace(/\s+/g, ' ').trim()).toEqual( 34 | ` 35 | repeating-linear-gradient( 36 | to right, transparent, transparent 20px, 20px, 37 | calc(${columnWidth(Neat(), 1)} + 20px) 38 | )` 39 | .replace(/\s+/g, ' ') 40 | .trim() 41 | ) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel' 2 | import flow from 'rollup-plugin-flow' 3 | import path from 'path' 4 | import resolve from 'rollup-plugin-node-resolve' 5 | import { terser } from 'rollup-plugin-terser' 6 | 7 | export default [ 8 | { 9 | external: ['react', 'styled-components'], 10 | input: `${path.resolve(__dirname, 'src', 'index.js')}`, 11 | output: { 12 | exports: 'named', 13 | file: `${path.resolve(__dirname, 'bundle.js')}`, 14 | format: 'cjs' 15 | }, 16 | plugins: [ 17 | babel({ 18 | babelrc: false, 19 | exclude: 'node_modules/**', 20 | presets: ['@babel/preset-flow', '@babel/preset-react'], 21 | plugins: [ 22 | '@babel/plugin-syntax-dynamic-import', 23 | '@babel/plugin-proposal-export-default-from', 24 | '@babel/plugin-proposal-object-rest-spread', 25 | '@babel/plugin-transform-arrow-functions', 26 | '@babel/plugin-transform-spread', 27 | '@babel/plugin-transform-destructuring', 28 | '@babel/plugin-transform-parameters', 29 | '@babel/plugin-transform-block-scoping', 30 | '@babel/plugin-transform-computed-properties', 31 | '@babel/plugin-transform-template-literals', 32 | '@babel/plugin-proposal-nullish-coalescing-operator', 33 | '@babel/plugin-proposal-optional-chaining' 34 | ], 35 | babelHelpers: 'bundled' 36 | }), 37 | flow({ all: true }), 38 | resolve(), 39 | terser() 40 | ] 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /src/Theme/Neat.test.js: -------------------------------------------------------------------------------- 1 | import Neat from './Neat' 2 | import { Directions } from './NeatTheme' 3 | 4 | describe('Neat', () => { 5 | describe('constructor', () => { 6 | it('should return the default values', () => { 7 | const result = { 8 | ...new Neat() 9 | } 10 | expect(result.color).toBe(null) 11 | expect(result.columns).toBe(12) 12 | expect(result.direction).toBe(Directions.LTR) 13 | expect(result.gutter).toBe('20px') 14 | expect(result.media).toBe(null) 15 | }) 16 | it('should return a mix of default values with custom values', () => { 17 | const result = { 18 | ...new Neat({ 19 | color: 'blue', 20 | media: 1000, 21 | gutter: '10px' 22 | }) 23 | } 24 | expect(result.color).toBe('blue') 25 | expect(result.columns).toBe(12) 26 | expect(result.direction).toBe(Directions.LTR) 27 | expect(result.gutter).toBe('10px') 28 | expect(result.media).toBe(1000) 29 | }) 30 | it('should handle a direction of "ltr" correctly', () => { 31 | const result = { 32 | ...new Neat({ 33 | direction: Directions.LTR 34 | }) 35 | } 36 | expect(result.direction).toBe(Directions.LTR) 37 | }) 38 | it('should handle a direction of "rtl" correctly', () => { 39 | const result = { 40 | ...new Neat({ 41 | direction: Directions.RTL 42 | }) 43 | } 44 | expect(result.direction).toBe(Directions.RTL) 45 | }) 46 | it('should handle a number direction correctly', () => { 47 | const result = { 48 | ...new Neat({ 49 | direction: 1 50 | }) 51 | } 52 | expect(result.direction).toBe(Directions.LTR) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import Neat, { gridColumn, gridContainer, gridMedia } from '../bundle' 2 | import styled, { ThemeProvider } from 'styled-components' 3 | import React from 'react' 4 | import { storiesOf } from '@storybook/react' 5 | 6 | storiesOf('Neat Components', module) 7 | .add('12 Column Grid', () => { 8 | let constants = () => { 9 | return ` 10 | background-color: #00d4ff; 11 | height: 300px; 12 | margin-top: 1em; 13 | ` 14 | } 15 | 16 | let Container = styled.div` 17 | ${gridContainer()}; 18 | ` 19 | let Column = styled.div` 20 | ${constants()} ${props => gridColumn(props.theme, 1)}; 21 | ` 22 | return ( 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 | ) 42 | }) 43 | .add('3 Column Grid', () => { 44 | let constants = () => { 45 | return ` 46 | background-color: #00d4ff; 47 | height: 300px; 48 | margin-top: 1em; 49 | ` 50 | } 51 | 52 | let Container = styled.div` 53 | ${gridContainer()}; 54 | ` 55 | let Column = styled.div` 56 | ${constants()} ${props => gridColumn(props.theme, 4)}; 57 | ` 58 | return ( 59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 | ) 69 | }) 70 | .add('A Responsive Grid', () => { 71 | const mobileGrid = Neat({ 72 | columns: 12, 73 | gutter: '10px', 74 | media: 'only screen and (max-width: 600px)' 75 | }) 76 | 77 | const desktopGrid = Neat({ 78 | columns: 12, 79 | gutter: '20px', 80 | media: 'only screen and (min-width: 601px)' 81 | }) 82 | 83 | const desktopMediaQuery = gridMedia(desktopGrid, [ 84 | { 85 | ...gridColumn(desktopGrid, 1), 86 | 'background-color': 'red' 87 | } 88 | ]) 89 | 90 | const Column = styled.div` 91 | height: 50px; 92 | background-color: yellow; 93 | margin-top: 1rem; 94 | ${gridColumn(mobileGrid, 2)} 95 | ${desktopMediaQuery} 96 | ` 97 | 98 | const Container = styled.div` 99 | ${gridContainer()} 100 | ` 101 | return ( 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | ) 119 | }) 120 | .add('9/3 Column Grid', () => { 121 | let constants = () => { 122 | return ` 123 | background-color: #00d4ff; 124 | height: 300px; 125 | margin-top: 1em; 126 | ` 127 | } 128 | 129 | let Container = styled.div` 130 | ${gridContainer()}; 131 | ` 132 | let ContentArea = styled.div` 133 | ${constants()} ${props => gridColumn(props.theme, 9)}; 134 | ` 135 | let Sidebar = styled.div` 136 | ${constants()} ${props => gridColumn(props.theme, 3)}}; 137 | ` 138 | return ( 139 |
140 | 141 |
142 | 143 | 144 | 145 | 146 |
147 |
148 | 149 |
150 | 151 | 152 | 153 | 154 |
155 |
156 |
157 | ) 158 | }) 159 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neat-components", 3 | "version": "1.0.33", 4 | "description": "Combining Thoughtbot's Neat Grid system with Styled Components", 5 | "keywords": [ 6 | "components", 7 | "grid", 8 | "neat", 9 | "styled", 10 | "styled-components", 11 | "thoughtbot" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/magicink/neat-components" 16 | }, 17 | "license": "MIT", 18 | "author": { 19 | "name": "Brandon Tom", 20 | "email": "brandon@brandontom.com" 21 | }, 22 | "main": "bundle.js", 23 | "files": [ 24 | "bundle.js", 25 | "README.md" 26 | ], 27 | "scripts": { 28 | "build": "rollup --config rollup.config.js", 29 | "clean": "npx rimraf bundle.js && npx rimraf ./docs", 30 | "format": "prettier --write \"**/*.{js,json,md}\"", 31 | "lint": "standard --parser babel-eslint \"./src/**.js\"", 32 | "patch": "npm version patch --no-git-tag-version", 33 | "prepare": "npx rimraf ./docs && npm run build", 34 | "storybook": "npm run clean && npm run build && start-storybook -c .storybook -p 9001", 35 | "storybook:build": "npm run clean && npm run build && build-storybook -c .storybook -o docs", 36 | "test": "jest --coverage", 37 | "test:watch": "jest --watch" 38 | }, 39 | "husky": { 40 | "hooks": { 41 | "pre-commit": "npm run format && npm run clean && npm run storybook:build && npm test" 42 | } 43 | }, 44 | "prettier": { 45 | "arrowParens": "avoid", 46 | "bracketSpacing": true, 47 | "quoteProps": "as-needed", 48 | "jsxBracketSameLine": false, 49 | "jsxSingleQuote": true, 50 | "semi": false, 51 | "singleQuote": true, 52 | "tabWidth": 2, 53 | "trailingComma": "none" 54 | }, 55 | "jest": { 56 | "roots": [ 57 | "/src/" 58 | ], 59 | "testEnvironment": "jest-environment-jsdom-global" 60 | }, 61 | "dependencies": {}, 62 | "devDependencies": { 63 | "@babel/cli": "^7.12.10", 64 | "@babel/core": "^7.10.5", 65 | "@babel/plugin-proposal-decorators": "^7.10.5", 66 | "@babel/plugin-proposal-export-default-from": "^7.10.4", 67 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", 68 | "@babel/plugin-proposal-object-rest-spread": "^7.10.4", 69 | "@babel/plugin-proposal-optional-chaining": "^7.10.4", 70 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 71 | "@babel/plugin-transform-arrow-functions": "^7.10.4", 72 | "@babel/plugin-transform-block-scoping": "^7.10.5", 73 | "@babel/plugin-transform-computed-properties": "^7.10.4", 74 | "@babel/plugin-transform-destructuring": "^7.10.4", 75 | "@babel/plugin-transform-modules-commonjs": "^7.10.4", 76 | "@babel/plugin-transform-parameters": "^7.10.5", 77 | "@babel/plugin-transform-runtime": "^7.10.5", 78 | "@babel/plugin-transform-spread": "^7.10.4", 79 | "@babel/plugin-transform-template-literals": "^7.10.5", 80 | "@babel/preset-env": "^7.10.4", 81 | "@babel/preset-flow": "^7.10.4", 82 | "@babel/preset-react": "^7.10.4", 83 | "@babel/register": "^7.10.5", 84 | "@babel/runtime": "^7.10.5", 85 | "@rollup/plugin-babel": "^5.1.0", 86 | "@storybook/addon-storysource": "^5.3.19", 87 | "@storybook/cli": "^5.3.19", 88 | "@storybook/react": "^7.0.6", 89 | "@testing-library/react": "^10.4.7", 90 | "@testing-library/react-hooks": "^3.4.1", 91 | "babel-eslint": "^10.0.1", 92 | "babel-jest": "^24.8.0", 93 | "babel-loader": "^8.0.6", 94 | "eslint-plugin-flowtype": "^3.10.3", 95 | "flow-bin": "^0.100.0", 96 | "husky": "^4.3.8", 97 | "jest": "^26.1.0", 98 | "jest-environment-jsdom": "^26.1.0", 99 | "jest-environment-jsdom-global": "^2.0.4", 100 | "marked": "^0.6.2", 101 | "prettier": "^2.2.1", 102 | "react": "^16.13.1", 103 | "react-dom": "^16.13.1", 104 | "regenerator-runtime": "^0.13.2", 105 | "rollup": "^2.23.0", 106 | "rollup-plugin-babel": "^4.4.0", 107 | "rollup-plugin-flow": "^1.1.1", 108 | "rollup-plugin-node-resolve": "^5.2.0", 109 | "rollup-plugin-terser": "^6.1.0", 110 | "standard": "^12.0.1", 111 | "styled-components": "^4.3.1" 112 | }, 113 | "peerDependencies": { 114 | "styled-components": "^5.1.1" 115 | }, 116 | "standard": { 117 | "parser": "babel-eslint", 118 | "plugins": [ 119 | "flowtype" 120 | ], 121 | "rules": { 122 | "flowtype/define-flow-type": 1, 123 | "flowtype/use-flow-type": 1 124 | }, 125 | "globals": [ 126 | "describe", 127 | "expect", 128 | "it", 129 | "test", 130 | "$Values" 131 | ] 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/magicink/neat-components.svg?branch=develop)](https://travis-ci.org/magicink/neat-components) 2 | 3 | # Neat Components 4 | 5 | A Styled Components implementation of Thoughtbot's Neat 2.0. 6 | 7 | View Storybook examples here: https://magicink.github.io/neat-components/ 8 | 9 | ### Usage 10 | 11 | ```javascript 12 | import React, { Component } from 'react' 13 | import styled, { ThemeProvider } from 'styled-components' 14 | import Neat, { gridContainer, gridColumn, gridShift } from 'neat-components' 15 | 16 | let constants = () => { 17 | return ` 18 | background-color: blue; 19 | height: 50px; 20 | margin-top: 1em; 21 | ` 22 | } 23 | 24 | let Container = styled.div` 25 | ${gridContainer()}; 26 | ` 27 | let Column = styled.div` 28 | ${constants()} ${props => gridColumn(props.theme, 1)}; 29 | ` 30 | 31 | class App extends Component { 32 | render() { 33 | return ( 34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | ) 45 | } 46 | } 47 | 48 | export default App 49 | ``` 50 | 51 | ## API 52 | 53 | ### Neat([settings]) 54 | 55 | Returns a theme object. You can pass this theme to a `ThemeProvider` component. For example 56 | 57 | ```javascript 58 | ... 59 | ``` 60 | 61 | You can pass in a `settings` object to create a custom grid. The `settings` object has the following properties: 62 | 63 | **`color`**: Used by `gridVisual()` to determine the color the grid (default: `null`). 64 | 65 | **`columns`**: The number of columns the grid supports (default: `12`). 66 | 67 | **`direction`**: The direction columns float. Accepts `ltr` (default) or `rtl`. 68 | 69 | **`gutter`**: The spacing between columns (default: `20px`) 70 | 71 | **`media`**: Used by `gridMedia()` to specify the media the grid should be applied. It can accept a string (i.e. `only screen and (max-width: 800px)`) or a number (i.e `800`). The later would produce `only screen and (min-width: 800px)`. Defaults to `null`. 72 | 73 | ```javascript 74 | const CustomGrid = Neat({ columns: 3, gutter: '60px' }) 75 | const Container = styled.div` 76 | margin-top: 1rem; 77 | ${gridContainer()}; 78 | ` 79 | const WideColumn = styled.div` 80 | background-image: linear-gradient(to bottom right, #21e9f4, #00d4ff); 81 | border-radius: 5px; 82 | height: 20rem; 83 | ${props => gridColumn(props.theme, 1)}; 84 | ` 85 | 86 | class App extends Component { 87 | render() { 88 | return ( 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ) 97 | } 98 | } 99 | ``` 100 | 101 | ### gridCollapse(theme) 102 | 103 | Used to create grids within grids. 104 | 105 | ```javascript 106 | const SidebarGrid = Neat({ 107 | columns: 3, 108 | gutter: '40px' 109 | }) 110 | 111 | const GalleryGrid = Neat({ 112 | columns: 4, 113 | gutter: '40px' 114 | }) 115 | 116 | const Container = styled.div` 117 | ${gridContainer()}; 118 | ` 119 | 120 | const SideBar = styled.div` 121 | height: 19rem; 122 | ${props => gridColumn(SidebarGrid, 1)}; 123 | ` 124 | 125 | const Gallery = styled.div` 126 | ${props => gridColumn(GalleryGrid, 2)}; 127 | ` 128 | 129 | const GalleryContainer = styled.div` 130 | ${gridCollapse(GalleryGrid)} ${gridContainer()}; 131 | ` 132 | 133 | const GalleryItem = styled.div` 134 | background-color: #496278; 135 | height: 4rem; 136 | margin-bottom: 1rem; 137 | ${gridColumn(GalleryGrid, 1)}; 138 | ` 139 | 140 | class App extends Component { 141 | render() { 142 | return ( 143 | 144 | 145 | 146 | 147 | 148 | 149 | ... 150 | 151 | 152 | 153 | 154 | ) 155 | } 156 | } 157 | ``` 158 | 159 | ### gridColumn(theme, span) 160 | 161 | Creates a component that occupies `span` number of a given theme's columns 162 | 163 | ```javascript 164 | let Column = styled.div` 165 | ${props => gridColumn(props.theme, 1)}; 166 | ` 167 | 168 | class App extends Component { 169 | render() { 170 | return ( 171 | 172 |
173 | 174 | 175 | 176 | 177 | 178 | 179 |
180 |
181 | ) 182 | } 183 | } 184 | ``` 185 | 186 | ### gridContainer() 187 | 188 | Injects a clearfix solution into the component. 189 | 190 | ```javascript 191 | const Container = styled.div` 192 | ${gridContainer()}; 193 | ` 194 | 195 | class App extends Component { 196 | render() { 197 | return ( 198 | 199 | ... 200 | 201 | ) 202 | } 203 | } 204 | ``` 205 | 206 | ### gridMedia(theme) 207 | 208 | Used to inject media queries into the component. 209 | 210 | ```javascript 211 | const mobileGrid = Neat({ 212 | columns: 12, 213 | gutter: '10px', 214 | media: 'only screen and (max-width: 600px)' 215 | }) 216 | 217 | const desktopGrid = Neat({ 218 | columns: 12, 219 | gutter: '20px', 220 | media: 'only screen and (min-width: 601px)' 221 | }) 222 | 223 | const Column = styled.div` 224 | height: 50px; 225 | background-color: yellow; 226 | margin-top: 1rem; 227 | ${gridColumn(mobileGrid, 2)} 228 | ${gridMedia(desktopGrid, [ 229 | { 230 | ...gridColumn(desktopGrid, 1), 231 | 'background-color': 'red' 232 | } 233 | ])} 234 | ` 235 | 236 | const Container = styled.div` 237 | ${gridContainer()} 238 | ` 239 | 240 | export class GridMedia extends React.Component { 241 | render() { 242 | return [ 243 |

244 | gridMedia 245 |

, 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | ] 263 | } 264 | } 265 | ``` 266 | 267 | ### gridShift(theme, shift) 268 | 269 | Shifts a component `shift` number of columns. This reorganizes the components.. 270 | 271 | ```javascript 272 | let constants = () => { 273 | return ` 274 | background-color: blue; 275 | height: 50px; 276 | margin-top: 1em; 277 | ` 278 | } 279 | 280 | let Container = styled.div` 281 | ${gridContainer()}; 282 | ` 283 | let Column = styled.div` 284 | ${constants()} ${props => gridColumn(props.theme, 1)}; 285 | ` 286 | 287 | let Shifted = styled.div` 288 | ${constants()} 289 | ${props => gridColumn(props.theme, 1)} 290 | ${props => gridShift(props.theme, 2)} 291 | ` 292 | 293 | class App extends Component { 294 | render() { 295 | return ( 296 | 297 |
298 | 299 | 300 | // Shifted one column to the right. 301 | 302 |
303 |
304 | ) 305 | } 306 | } 307 | ``` 308 | 309 | ### gridPush(theme, push) 310 | 311 | Pushes the component `push` number of columns. It's similar to `gridShift()` but does not rearrange the components. 312 | 313 | ```javascript 314 | let constants = () => { 315 | return ` 316 | background-color: blue; 317 | height: 50px; 318 | margin-top: 1em; 319 | ` 320 | } 321 | 322 | let Container = styled.div` 323 | ${gridContainer()}; 324 | ` 325 | let Column = styled.div` 326 | ${constants()} ${props => gridColumn(props.theme, 1)}; 327 | ` 328 | 329 | let Pushed = styled.div` 330 | ${constants()} 331 | ${props => gridColumn(props.theme, 1)} 332 | ${props => gridPush(props.theme, 2)} 333 | ` 334 | 335 | class App extends Component { 336 | render() { 337 | return ( 338 | 339 |
340 | 341 | 342 | // Pushed one column to the right. 343 | 344 |
345 |
346 | ) 347 | } 348 | } 349 | ``` 350 | 351 | ### gridVisual(theme) 352 | 353 | Creates series of visual guidelines based on the grid system. 354 | 355 | ## References 356 | 357 | - [Neat 2.x](http://neat.bourbon.io) 358 | - [styled-components](https://www.styled-components.com/) 359 | --------------------------------------------------------------------------------