├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── create-horror ├── cli.js └── package.json ├── docs ├── index.html ├── main.js └── src │ └── index.js ├── emotion.js ├── examples ├── basic │ ├── LICENSE.md │ ├── README.md │ ├── package.json │ └── pages │ │ └── index.js ├── index.js └── system.js ├── index.js ├── package.json ├── src ├── cap.js ├── create.js ├── emotion.js └── index.js ├── test.js ├── test.js.md └── test.js.snap /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-0", 5 | "react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples 3 | test.js* 4 | coverage 5 | .nyc_output 6 | create-horror 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # The MIT License (MIT) 3 | Copyright (c) 2018 Compositor, Inc. 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Horror 3 | 4 | :scream: React HTML elements with CSS-in-JS 5 | 6 | https://jxnblk.com/horror 7 | 8 | ```sh 9 | npm i horror styled-components 10 | ``` 11 | 12 | ## Quick Start 13 | 14 | To create a Horror starter project, run: 15 | 16 | ```sh 17 | npm init horror my-horror-project 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```jsx 23 | import React from 'react' 24 | import { Div, H1 } from 'horror' 25 | 26 | export default props => 27 |
30 |

31 | Horror 32 |

33 |
34 | ``` 35 | 36 | - Includes all HTML elements 37 | - Use object literal or CSS syntax 38 | - Style any component 39 | - Support for [styled-components][sc] or [emotion][emotion] 40 | 41 | ## HTML Tags 42 | 43 | Changing the underlying HTML tag can be done in any of the following ways: 44 | 45 | - Importing the tag directly: `import { Header } from 'horror'` 46 | - Using a key on the default import: `` 47 | - Using the `is` prop: `` 48 | - Using the styled-components API: `const H1 = Horror.withComponent('h1')` 49 | 50 | ## Using Custom Components 51 | 52 | To use a custom component, pass it to the `is` prop: 53 | 54 | ```jsx 55 | import React from 'react' 56 | import { Link } from 'react-router-dom' 57 | import H from 'horror' 58 | 59 | const RedLink = props => 60 | 67 | ``` 68 | 69 | ## Creating Style Components 70 | 71 | Horror can be used to create component primitive abstractions, similar to using [styled-components][sc] or [react-emotion][emotion], 72 | but with a more React-like syntax. 73 | 74 | ```jsx 75 | import React from 'react' 76 | import H from 'horror' 77 | 78 | const Button = ({ 79 | primary, 80 | ...props 81 | }) => 82 | 107 | 108 | Button.displayName = 'Button' 109 | 110 | export default Button 111 | ``` 112 | 113 | ## Emotion 114 | 115 | Horror also works with [emotion][emotion]: 116 | 117 | ```sh 118 | npm i emotion react-emotion 119 | ``` 120 | 121 | ```jsx 122 | import H from 'horror/emotion' 123 | ``` 124 | 125 | ## Related & Inspiration 126 | 127 | - [styled-components][sc] 128 | - [emotion][emotion] 129 | - [jsxstyle](https://github.com/smyte/jsxstyle) 130 | - [glamorous](https://github.com/paypal/glamorous) 131 | 132 | [emotion]: https://github.com/emotion-js/emotion 133 | [sc]: https://github.com/styled-components/styled-components/ 134 | 135 | [MIT License](LICENSE.md) 136 | | 137 | [Made by Compositor](https://compositor.io) 138 | -------------------------------------------------------------------------------- /create-horror/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const init = require('initit') 3 | 4 | const [ name ] = process.argv.slice(2) 5 | const template = 'jxnblk/horror/examples/basic' 6 | 7 | if (!name) { 8 | console.log(`name is required: 9 | 10 | $ npm init horror my-project 11 | `) 12 | process.exit(1) 13 | } 14 | 15 | init({ name, template }) 16 | .then(res => { 17 | console.log('project created:', name) 18 | process.exit(0) 19 | }) 20 | .catch(err => { 21 | console.log(err) 22 | process.exit(1) 23 | }) 24 | -------------------------------------------------------------------------------- /create-horror/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-horror", 3 | "version": "1.0.0-0", 4 | "description": "npm init horror", 5 | "bin": { 6 | "create-horror": "./cli.js" 7 | }, 8 | "keywords": [], 9 | "author": "Brent Jackson", 10 | "license": "MIT", 11 | "dependencies": { 12 | "initit": "^1.0.0-2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Horror 19 | 20 | 21 |

Horror

React HTML elements with CSS-in-JS

npm i horror
GitHub
<Div css={{
22 |   padding: '64px'
23 | }}>
24 |   <H1 css={{
25 |     margin: 0,
26 |     marginBottom: '32px',
27 |     display: 'inline-block',
28 |     fontSize: '48px',
29 |     paddingTop: '8px',
30 |     paddingBottom: '8px',
31 |     paddingLeft: '32px',
32 |     paddingRight: '32px',
33 |     color: 'white',
34 |     backgroundColor: 'red',
35 |     transform: 'skew(-6deg)',
36 |     '@media screen and (min-width: 48em)': {
37 |       fontSize: '64px'
38 |     }
39 |   }}>
40 |     Horror
41 |   </H1>
42 |   <P css={{
43 |     margin: 0,
44 |     marginBottom: '32px',
45 |     fontSize: '20px',
46 |     fontWeight: 'bold'
47 |   }}>
48 |     React HTML elements with CSS-in-JS
49 |   </P>
50 |   <Pre css={{
51 |     fontFamily: 'Menlo, monospace',
52 |     fontSize: '14px',
53 |     color: 'red'
54 |   }}>
55 |     npm i horror
56 |   </Pre>
57 |   <A href='https://github.com/jxnblk/horror'
58 |     css={{
59 |       textDecoration: 'none',
60 |       fontWeight: 'bold',
61 |       display: 'inline-block',
62 |       padding: '16px',
63 |       borderRadius: '4px',
64 |       color: 'white',
65 |       backgroundColor: 'red',
66 |       '&:hover': {
67 |         backgroundColor: '#900',
68 |       }
69 |     }}>
70 |     GitHub
71 |   </A>
72 | </Div>
73 | 74 | 75 | -------------------------------------------------------------------------------- /docs/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | LiveProvider, 4 | LivePreview, 5 | LiveEditor, 6 | LiveError 7 | } from 'react-live' 8 | import { Head } from 'mdx-go' 9 | import H, { 10 | Div, 11 | H1, 12 | } from '../..' 13 | 14 | const scope = Object.keys(H) 15 | .filter(key => /^[A-Z]/.test(key)) 16 | .reduce((a, key) => ({ ...a, [key]: H[key] }), {}) 17 | 18 | const code = `
21 |

37 | Horror 38 |

39 |

45 | React HTML elements with CSS-in-JS 46 |

47 |
 52 |     npm i horror
 53 |   
54 | 67 | GitHub 68 | 69 |
` 70 | 71 | const transform = src => ` 72 | ${src} 73 | ` 74 | 75 | const breakpoint = '@media screen and (min-width: 48em)' 76 | 77 | const Editor = props => 78 | 91 | 92 | const Err = props => 93 | 105 | 106 | const Flex = props => 107 |
116 | 117 | export const Root = props => props.children 118 | 119 | export default class extends React.Component { 120 | render () { 121 | return ( 122 | 123 | 124 | Horror 125 | 126 | 127 | 132 | 133 | 134 |
141 | 142 |
143 | 150 |
151 |
152 |
153 | ) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /emotion.js: -------------------------------------------------------------------------------- 1 | const tags = require('html-tags') 2 | const H = require('./dist/emotion').default 3 | const cap = require('./dist/cap').default 4 | module.exports = H 5 | tags.forEach(key => { 6 | module.exports[cap(key)] = H[cap(key)] 7 | }) 8 | -------------------------------------------------------------------------------- /examples/basic/LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # The MIT License (MIT) 3 | Copyright (c) 2018 Compositor, Inc. 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 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | 2 | # create-horror 3 | 4 | This project was created with [create-horror][horror] 5 | 6 | ## Getting Started 7 | 8 | Install dependencies 9 | 10 | ```sh 11 | npm install 12 | ``` 13 | 14 | Run the development server 15 | 16 | ```sh 17 | npm start 18 | ``` 19 | 20 | Edit the source code in [`pages/index.js`](pages/index.js) to get started 21 | 22 | To export this project to static HTML and JS, run: 23 | 24 | ```sh 25 | npm run build 26 | ``` 27 | 28 | [MIT License](LICENSE.md) 29 | | 30 | [Made by Compositor](https://compositor.io) 31 | 32 | [horror]: https://github.com/jxnblk/horror 33 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "horror-basic-example", 3 | "private": true, 4 | "scripts": { 5 | "start": "x0 pages --open", 6 | "build": "x0 build pages" 7 | }, 8 | "devDependencies": { 9 | "@compositor/x0": "^5.0.5", 10 | "horror": "^1.0.0-2", 11 | "react": "^16.4.0", 12 | "styled-components": "^3.3.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/basic/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Div, 4 | Header, 5 | H1, 6 | P, 7 | Code, 8 | } from 'horror' 9 | 10 | const Title = props => 11 |

22 | 23 | export default props => 24 |
28 |
29 | 30 | Hello Horror 31 | 32 |

33 | Edit the source code in pages/index.js to get started 34 |

35 |
36 |
37 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import tags from 'html-tags' 3 | import voids from 'html-tags/void' 4 | import H, { H2 } from '../index' 5 | 6 | const demoCSS = `color: red; 7 | font-size: 32px; 8 | text-transform: uppercase; 9 | letter-spacing: 0.1em; 10 | margin: 0; 11 | ` 12 | 13 | const Button = ({ 14 | primary, 15 | ...props 16 | }) => 17 | 42 | 43 | 44 | export default class extends React.Component { 45 | state = { 46 | css: demoCSS, 47 | type: 'h2', 48 | } 49 | 50 | update = (fn) => this.setState(fn) 51 | 52 | render () { 53 | const { 54 | type, 55 | css 56 | } = this.state 57 | 58 | return ( 59 | 60 | 61 | Horror 62 | 63 | React HTML elements with CSS-in-JS 64 | 65 | npm i horror 66 | 67 | 68 | 72 |

73 | Demo 74 |

75 | 76 | 77 | {React.createElement(H, { 78 | is: type, 79 | css, 80 | children: voids.includes(type) ? undefined : 'Demo' 81 | })} 82 | 83 | 84 | { 89 | this.update({ type: e.target.value }) 90 | }}> 91 | {tags.map(t => ( 92 | 99 | { 103 | this.update({ css: e.target.value }) 104 | }} 105 | /> 106 | 107 | 108 |
109 |
110 | ) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /examples/system.js: -------------------------------------------------------------------------------- 1 | // example with styled-system 2 | 3 | import React from 'react' 4 | import { 5 | width, 6 | fontSize, 7 | space, 8 | color, 9 | } from 'styled-system' 10 | import merge from 'deepmerge' 11 | import H from '../src' 12 | 13 | const compose = (...funcs) => props => 14 | funcs.reduce((a = {}, fn) => merge(a, fn(props) || {}), {}) 15 | 16 | const coreSystem = compose( 17 | width, 18 | fontSize, 19 | space, 20 | color 21 | ) 22 | 23 | const Box = props => 24 | 28 | 29 | export default props => 30 | 31 | 35 | Hello styled-system 36 | 37 | 38 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const H = require('./dist/index').default 2 | const tags = require('html-tags') 3 | const cap = require('./dist/cap').default 4 | module.exports = H 5 | tags.forEach(key => { 6 | module.exports[cap(key)] = H[cap(key)] 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "horror", 3 | "version": "1.0.0-4", 4 | "description": "React HTML elements with CSS-in-JS", 5 | "main": "index.js", 6 | "scripts": { 7 | "prepare": "babel src -d dist", 8 | "start": "mdx-go docs/src", 9 | "build": "mdx-go build docs/src -d docs --cssLibrary='styled-components'", 10 | "test": "nyc ava", 11 | "cover": "nyc report --reporter html" 12 | }, 13 | "keywords": [], 14 | "author": "Brent Jackson", 15 | "license": "MIT", 16 | "dependencies": { 17 | "ava": "^0.25.0", 18 | "html-tags": "^2.0.0", 19 | "nyc": "^12.0.2", 20 | "react-test-renderer": "^16.4.0", 21 | "styled-system": "^2.2.5" 22 | }, 23 | "devDependencies": { 24 | "babel-cli": "^6.26.0", 25 | "babel-core": "^6.26.3", 26 | "babel-preset-env": "^1.7.0", 27 | "babel-preset-react": "^6.24.1", 28 | "babel-preset-stage-0": "^6.24.1", 29 | "babel-register": "^6.26.0", 30 | "deepmerge": "^2.1.1", 31 | "emotion": "^9.2.3", 32 | "mdx-go": "^1.0.10", 33 | "react-emotion": "^9.2.3", 34 | "react-live": "^1.10.1", 35 | "styled-components": "^3.3.2" 36 | }, 37 | "ava": { 38 | "require": [ 39 | "babel-register" 40 | ], 41 | "babel": "inherit" 42 | }, 43 | "mdx-go": { 44 | "basename": "/horror" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/cap.js: -------------------------------------------------------------------------------- 1 | export default str => str.charAt(0).toUpperCase() + str.slice(1) 2 | -------------------------------------------------------------------------------- /src/create.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import tags from 'html-tags' 3 | import cap from './cap' 4 | 5 | const Tag = ({ 6 | is = 'div', 7 | css, 8 | theme, 9 | ...props 10 | }) => ( 11 | React.createElement(is, props) 12 | ) 13 | 14 | const create = styled => { 15 | const H = styled(Tag)([], ({ css }) => css) 16 | tags.forEach(tag => { 17 | const T = cap(tag) 18 | H[tag] = H.withComponent(tag) 19 | H[tag].displayName = 'H.' + tag 20 | 21 | H[T] = H.withComponent(tag) 22 | H[T].displayName = 'H.' + T 23 | }) 24 | return H 25 | } 26 | 27 | export default create 28 | -------------------------------------------------------------------------------- /src/emotion.js: -------------------------------------------------------------------------------- 1 | import styled from 'react-emotion' 2 | import create from './create' 3 | 4 | export default create(styled) 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import create from './create' 3 | 4 | export default create(styled) 5 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import React from 'react' 3 | import { create as render } from 'react-test-renderer' 4 | import tags from 'html-tags' 5 | import H from './src' 6 | import E from './src/emotion' 7 | import create from './src/create' 8 | 9 | const renderJSON = el => render(el).toJSON() 10 | 11 | test(`styled-components H renders`, t => { 12 | const json = renderJSON( 13 | React.createElement(H, { css: 'color: tomato;' }) 14 | ) 15 | t.snapshot(json) 16 | }) 17 | 18 | tags.forEach(tag => ( 19 | test(`styled-components H.${tag} renders`, t => { 20 | const json = renderJSON( 21 | React.createElement(H[tag], { css: 'color: tomato;' }) 22 | ) 23 | t.snapshot(json) 24 | }) 25 | )) 26 | 27 | test(`emotion H renders`, t => { 28 | const json = renderJSON( 29 | React.createElement(E, { css: 'color: tomato;' }) 30 | ) 31 | t.snapshot(json) 32 | }) 33 | 34 | tags.forEach(tag => ( 35 | test(`emotion H.${tag} renders`, t => { 36 | const json = renderJSON( 37 | React.createElement(E[tag], { css: 'color: tomato;' }) 38 | ) 39 | t.snapshot(json) 40 | }) 41 | )) 42 | 43 | // fixture 44 | const styled = Component => (...styles) => { 45 | class Styled extends React.Component { 46 | render () { 47 | const style = styles.reduce((a, b) => { 48 | const sx = typeof b === 'function' 49 | ? b(this.props) 50 | : b 51 | return { ...a, ...sx } 52 | }, {}) 53 | 54 | return 55 | } 56 | } 57 | Styled.withComponent = () => Styled 58 | return Styled 59 | } 60 | 61 | test('creates custom styled components', t => { 62 | const C = create(styled) 63 | const json = renderJSON( 64 | 65 | ) 66 | t.snapshot(json) 67 | }) 68 | -------------------------------------------------------------------------------- /test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test.js` 2 | 3 | The actual snapshot is saved in `test.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## H.A renders 8 | 9 | > Snapshot 1 10 | 11 | 14 | 15 | ## H.Abbr renders 16 | 17 | > Snapshot 1 18 | 19 | 22 | 23 | ## H.Address renders 24 | 25 | > Snapshot 1 26 | 27 |
30 | 31 | ## H.Area renders 32 | 33 | > Snapshot 1 34 | 35 | 38 | 39 | ## H.Article renders 40 | 41 | > Snapshot 1 42 | 43 |
46 | 47 | ## H.Aside renders 48 | 49 | > Snapshot 1 50 | 51 |