52 | Hello World, this is my first glamor styled component!
53 |
54 |
55 | `;
56 |
57 | exports[`enzyme.shallow 1`] = `
58 | .css-1otybxc,
59 | [data-css-1otybxc] {
60 | padding: 4em;
61 | background: papayawhip;
62 | }
63 |
64 |
67 |
68 | Hello World, this is my first glamor styled component!
69 |
70 |
71 | `;
72 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | ### [Simple Example](https://github.com/MicheleBertoli/css-in-js/blob/master/glamorous/button.js)
4 | This example includes both the object literal styles and prop based styles.
5 | Additionally, shows how to to psuedo selectors and a media query.
6 |
7 | ## Dynamic + Static Styles
8 |
9 | One of the nice bits of glamorous is that it allows you to make a clear
10 | separation between your dynamic and static styles by forcing you to choose
11 | between an object literal and a function. Here's an example of having both
12 | dynamic and static styles:
13 |
14 | ```javascript
15 | const MyLink = glamorous.a(
16 | {
17 | color: 'blue',
18 | textDecoration: 'none',
19 | },
20 | ({size = 'small'}) => ({
21 | fontSize: size === 'big' ? 24 : 16,
22 | })
23 | )
24 | ```
25 |
26 | You can see a live preview of this example here: https://codesandbox.io/s/mZkpo0lKA
27 |
28 | ## @supports + CSS Grid
29 |
30 | Want to use CSS Grid, but worried about browser support? Because `glamor`
31 | supports the `@supports` statement, you can use that with `glamorous` easily.
32 |
33 | Play with it [here](https://codesandbox.io/s/2k8yll8qj):
34 |
35 | ```javascript
36 | const MyGrid = glamorous.div({
37 | margin: 'auto',
38 | backgroundColor: '#fff',
39 | color: '#444',
40 | // You can use @supports with glamor!
41 | // So you can use @supports with glamorous as well!
42 | '@supports (display: grid)': {
43 | display: 'grid',
44 | gridGap: 10,
45 | gridTemplateAreas: `
46 | "....... header header"
47 | "sidebar content content"
48 | "footer footer footer"
49 | `,
50 | },
51 | })
52 | ```
53 |
54 | ## with Next.js
55 |
56 | Here's a [deployed example](https://with-glamorous-zrqwerosse.now.sh/) of using
57 | `glamorous` with `Next.js`. See the code [here][next].
58 |
59 | [next]: https://github.com/zeit/next.js/tree/master/examples/with-glamorous
60 |
61 | ## with create-react-app
62 |
63 | [Here](https://github.com/patitonar/create-react-app-glamorous) is an example of using
64 | `glamorous` with `create-react-app`.
65 |
--------------------------------------------------------------------------------
/dist-test/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is here to validate that the built version
3 | * of the library exposes the module in the way that we
4 | * want it to. Specifically that the ES6 module import can
5 | * get the glamorous function via default import. Also that
6 | * the CommonJS require returns the glamorous function
7 | * (rather than an object that has the glamorous as a
8 | * `default` property).
9 | *
10 | * This file is unable to validate the global export.
11 | */
12 | import assert from 'assert'
13 | import {oneLine} from 'common-tags'
14 |
15 | import esImport from '../dist/glamorous.es'
16 | import cjsImport from '../' // picks up the main from package.json
17 | import umdImport from '../dist/glamorous.umd'
18 |
19 | // intentionally left out because you shouldn't ever
20 | // try to require the ES file in CommonJS
21 | // const esRequire = require('../dist/glamorous.es')
22 | const cjsRequire = require('../') // picks up the main from package.json
23 | const umdRequire = require('../dist/glamorous.umd')
24 |
25 | assert(isGlamorousFunction(esImport), 'ES build has a problem with ES Modules')
26 |
27 | // intentionally left out ☝️
28 | // assert(isGlamorousFunction(esRequire), 'ES build has a problem with CJS')
29 |
30 | assert(
31 | isGlamorousFunction(cjsImport),
32 | 'CJS build has a problem with ES Modules',
33 | )
34 |
35 | assert(isGlamorousFunction(cjsRequire), 'CJS build has a problem with CJS')
36 |
37 | assert(
38 | isGlamorousFunction(umdImport),
39 | 'UMD build has a problem with ES Modules',
40 | )
41 |
42 | assert(isGlamorousFunction(umdRequire), 'UMD build has a problem with CJS')
43 |
44 | // TODO: how could we validate the global export?
45 |
46 | console.log('Built modules look good 👍')
47 |
48 | function isGlamorousFunction(thing) {
49 | if (typeof thing !== 'function') {
50 | console.error(
51 | oneLine`
52 | glamorous thing should be a function.
53 | It's a ${typeof thing} with the
54 | properties of: ${Object.keys(thing).join(', ')}
55 | `,
56 | )
57 | return false
58 | }
59 | return true
60 | }
61 |
62 | /*
63 | eslint
64 | no-console: 0,
65 | import/extensions: 0,
66 | import/no-unresolved: 0,
67 | import/no-duplicates: 0,
68 | */
69 |
--------------------------------------------------------------------------------
/src/theme-provider.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import brcast from 'brcast'
3 | import {PropTypes} from './react-compat'
4 |
5 | export const CHANNEL = '__glamorous__'
6 |
7 | /**
8 | * This is a component which will provide a theme to the entire tree
9 | * via context and event listener
10 | * (because pure components block context updates)
11 | * inspired by the styled-components implementation
12 | * https://github.com/styled-components/styled-components
13 | * @param {Object} theme the theme object..
14 | */
15 | class ThemeProvider extends Component {
16 | broadcast = brcast(this.props.theme)
17 |
18 | // create theme, by merging with outer theme, if present
19 | getTheme(passedTheme) {
20 | const theme = passedTheme || this.props.theme
21 | return {...this.outerTheme, ...theme}
22 | }
23 |
24 | getChildContext() {
25 | return {
26 | [CHANNEL]: this.broadcast,
27 | }
28 | }
29 |
30 | setOuterTheme = theme => {
31 | this.outerTheme = theme
32 | }
33 |
34 | componentDidMount() {
35 | // create a new subscription for keeping track of outer theme, if present
36 | if (this.context[CHANNEL]) {
37 | this.unsubscribe = this.context[CHANNEL].subscribe(this.setOuterTheme)
38 | }
39 | }
40 |
41 | componentWillMount() {
42 | // set broadcast state by merging outer theme with own
43 | if (this.context[CHANNEL]) {
44 | this.setOuterTheme(this.context[CHANNEL].getState())
45 | this.broadcast.setState(this.getTheme())
46 | }
47 | }
48 |
49 | componentWillReceiveProps(nextProps) {
50 | if (this.props.theme !== nextProps.theme) {
51 | this.broadcast.setState(this.getTheme(nextProps.theme))
52 | }
53 | }
54 |
55 | componentWillUnmount() {
56 | this.unsubscribe && this.unsubscribe()
57 | }
58 |
59 | render() {
60 | return this.props.children ?
61 | React.Children.only(this.props.children) :
62 | null
63 | }
64 | }
65 |
66 | ThemeProvider.childContextTypes = {
67 | [CHANNEL]: PropTypes.object.isRequired,
68 | }
69 |
70 | ThemeProvider.contextTypes = {
71 | [CHANNEL]: PropTypes.object,
72 | }
73 |
74 | ThemeProvider.propTypes = {
75 | theme: PropTypes.object.isRequired,
76 | children: PropTypes.node,
77 | }
78 |
79 | export default ThemeProvider
80 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/theme-provider.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`merges nested themes 1`] = `
4 | .css-mhpxxj,
5 | [data-css-mhpxxj] {
6 | padding: 1px;
7 | margin: 1px;
8 | }
9 |
10 | .css-19cobm9,
11 | [data-css-19cobm9] {
12 | padding: 1px;
13 | margin: 2px;
14 | }
15 |
16 |
17 |
25 |
26 |
27 |
30 |
31 |
38 |
39 |
42 |
43 |
44 |
45 |
46 |
47 | `;
48 |
49 | exports[`renders a component with theme 1`] = `
50 | .css-1fyph4i,
51 | [data-css-1fyph4i] {
52 | color: red;
53 | padding: 10px;
54 | }
55 |
56 |
59 | `;
60 |
61 | exports[`renders if children are null 1`] = `
62 |
69 | `;
70 |
71 | exports[`with theme prop of margin 2px 1`] = `
72 |
79 | `;
80 |
81 | exports[`with theme prop of margin 2px 2`] = `
82 |
89 | `;
90 |
91 | exports[`with theme prop of padding 10px 1`] = `
92 | .css-1p841yy,
93 | [data-css-1p841yy] {
94 | color: red;
95 | padding: 10px;
96 | }
97 |
98 |
99 |
106 |
107 |
110 |
111 |
112 |
113 | `;
114 |
115 | exports[`with theme prop of padding 20px 1`] = `
116 | .css-1e41o48,
117 | [data-css-1e41o48] {
118 | color: red;
119 | padding: 20px;
120 | }
121 |
122 |
123 |
130 |
131 |
134 |
135 |
136 |
137 | `;
138 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for being willing to contribute!
4 |
5 | **Working on your first Pull Request?** You can learn how from this *free* series
6 | [How to Contribute to an Open Source Project on GitHub][egghead]
7 |
8 | ## Project setup
9 |
10 | 1. Fork and clone the repo
11 | 2. `$ npm install` to install dependencies
12 | 3. `$ npm start validate` to validate you've got it working
13 | 4. Create a branch for your PR
14 |
15 | This project uses [`nps`][nps] and you can run `npm start` to see what scripts are available.
16 |
17 | ## Add yourself as a contributor
18 |
19 | This project follows the [all contributors][all-contributors] specification. To add yourself to the table of
20 | contributors on the README.md, please use the automated script as part of your PR:
21 |
22 | ```console
23 | npm start "contributors.add"
24 | ```
25 |
26 | Follow the prompt. If you've already added yourself to the list and are making a new type of contribution, you can run
27 | it again and select the added contribution type.
28 |
29 | ## Committing and Pushing changes
30 |
31 | This project uses [`semantic-release`][semantic-release] to do automatic releases and generate a changelog based on the
32 | commit history. So we follow [a convention][convention] for commit messages. Please follow this convention for your
33 | commit messages.
34 |
35 | You can use `commitizen` to help you to follow [the convention][convention]
36 |
37 | Once you are ready to commit the changes, please use the below commands
38 |
39 | 1. `git add `
40 | 2. `$ npm start commit`
41 |
42 | ... and follow the instruction of the interactive prompt.
43 |
44 | ### opt into git hooks
45 |
46 | There are git hooks set up with this project that are automatically installed when you install dependencies. They're
47 | really handy, but are turned off by default (so as to not hinder new contributors). You can opt into these by creating
48 | a file called `.opt-in` at the root of the project and putting this inside:
49 |
50 | ```
51 | commit-msg
52 | pre-commit
53 | ```
54 |
55 | ## Help needed
56 |
57 | Please checkout the [ROADMAP.md][ROADMAP] and raise an issue to discuss
58 | any of the items in the want to do or might do list.
59 |
60 | Also, please watch the repo and respond to questions/bug reports/feature requests! Thanks!
61 |
62 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
63 | [semantic-release]: https://npmjs.com/package/semantic-release
64 | [convention]: https://github.com/conventional-changelog/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md
65 | [all-contributors]: https://github.com/kentcdodds/all-contributors
66 | [ROADMAP]: ./other/ROADMAP.md
67 | [nps]: https://npmjs.com/package/nps
68 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/index.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`allows you to specify the tag rendered by a component 1`] = `
4 | .css-110n7f9,
5 | [data-css-110n7f9] {
6 | height: 1px;
7 | width: 2px;
8 | }
9 |
10 |
14 | `;
15 |
16 | exports[`can use pre-glamorous components with css attributes 1`] = `
17 | .css-1t62idy,
18 | [data-css-1t62idy] {
19 | -webkit-flex-direction: column;
20 | -ms-flex-direction: column;
21 | -webkit-box-orient: vertical;
22 | -webkit-box-direction: normal;
23 | display: -webkit-box;
24 | display: -moz-box;
25 | display: -ms-flexbox;
26 | display: -webkit-flex;
27 | display: flex;
28 | flex-direction: column;
29 | }
30 |
31 |
36 | `;
37 |
38 | exports[`forwards props when the GlamorousComponent.rootEl is known 1`] = `
39 | .css-mhpxxj,
40 | [data-css-mhpxxj] {
41 | padding: 1px;
42 | margin: 1px;
43 | }
44 |
45 |
,
82 | ),
83 | ).toMatchSnapshotWithGlamor()
84 | })
85 |
86 | test('renders if children are null', () => {
87 | expect(
88 | mount(
89 |
90 | {false && }
91 | ,
92 | ),
93 | ).toMatchSnapshotWithGlamor()
94 | })
95 |
96 | test('cleans up outer theme subscription when unmounts', () => {
97 | const unsubscribe = jest.fn()
98 | const context = getMockedContext(unsubscribe)
99 | const wrapper = mount(, {context})
100 | wrapper.unmount()
101 | expect(unsubscribe).toHaveBeenCalled()
102 | })
103 |
104 | test('does nothing when receive same theme via props', () => {
105 | const theme = {margin: 2}
106 | const wrapper = mount()
107 | expect(wrapper).toMatchSnapshotWithGlamor(`with theme prop of margin 2px`)
108 | wrapper.setProps({theme})
109 | expect(wrapper).toMatchSnapshotWithGlamor(`with theme prop of margin 2px`)
110 | })
111 |
--------------------------------------------------------------------------------
/package-scripts.js:
--------------------------------------------------------------------------------
1 | const npsUtils = require('nps-utils')
2 |
3 | const series = npsUtils.series
4 | const concurrent = npsUtils.concurrent
5 | const rimraf = npsUtils.rimraf
6 |
7 | module.exports = {
8 | scripts: {
9 | contributors: {
10 | add: {
11 | description: 'When new people contribute to the project, run this',
12 | script: 'all-contributors add',
13 | },
14 | generate: {
15 | description: 'Update the badge and contributors table',
16 | script: 'all-contributors generate',
17 | },
18 | },
19 | commit: {
20 | description: 'This uses commitizen to help us generate well formatted commit messages',
21 | script: 'git-cz',
22 | },
23 | test: {
24 | default: 'jest --coverage',
25 | watch: 'jest --watch',
26 | build: {
27 | description: 'validates the built files',
28 | script: 'babel-node dist-test/index.js',
29 | },
30 | },
31 | build: {
32 | description: 'delete the dist directory and run all builds',
33 | default: series(
34 | rimraf('dist'),
35 | concurrent.nps('build.es', 'build.umd.main', 'build.umd.min')
36 | ),
37 | es: {
38 | description: 'run the build with rollup (uses rollup.config.js)',
39 | script: 'rollup --config --environment FORMAT:es',
40 | },
41 | umd: {
42 | min: {
43 | description: 'run the rollup build with sourcemaps',
44 | script: 'rollup --config --sourcemap --environment MINIFY,FORMAT:umd',
45 | },
46 | main: {
47 | description: 'builds the cjs and umd files',
48 | script: 'rollup --config --sourcemap --environment FORMAT:umd',
49 | },
50 | },
51 | andTest: series.nps('build', 'test.build'),
52 | },
53 | lint: {
54 | description: 'lint the entire project',
55 | script: 'eslint .',
56 | },
57 | reportCoverage: {
58 | description: 'Report coverage stats to codecov. This should be run after the `test` script',
59 | script: 'codecov',
60 | },
61 | release: {
62 | description: 'We automate releases with semantic-release. This should only be run on travis',
63 | script: series(
64 | 'semantic-release pre',
65 | 'npm publish',
66 | 'semantic-release post'
67 | ),
68 | },
69 | examples: {
70 | withJest: {
71 | description: 'This jumpstarts and validates the with-jest example.',
72 | script: series(
73 | 'cd examples/with-jest',
74 | 'yarn install',
75 | 'yarn run test',
76 | 'cd ../../'
77 | ),
78 | },
79 | },
80 | validate: {
81 | description: 'This runs several scripts to make sure things look good before committing or on clean install',
82 | default: concurrent.nps('lint', 'build.andTest', 'test'),
83 | examples: {
84 | description: 'Validates the examples folder',
85 | script: 'nps examples.withJest',
86 | },
87 | },
88 | },
89 | options: {
90 | silent: false,
91 | },
92 | }
93 |
94 | // this is not transpiled
95 | /*
96 | eslint
97 | max-len: 0,
98 | comma-dangle: [
99 | 2,
100 | {
101 | arrays: 'always-multiline',
102 | objects: 'always-multiline',
103 | functions: 'never'
104 | }
105 | ]
106 | */
107 |
--------------------------------------------------------------------------------
/other/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at kent+coc@doddsfamily.us. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glamorous",
3 | "version": "0.0.0-semantically-released",
4 | "description": "React component styling solved",
5 | "main": "dist/glamorous.cjs.js",
6 | "jsnext:main": "dist/glamorous.es.js",
7 | "module": "dist/glamorous.es.js",
8 | "scripts": {
9 | "start": "nps",
10 | "test": "nps test",
11 | "commitmsg": "opt --in commit-msg --exec \"validate-commit-msg\"",
12 | "precommit": "lint-staged && opt --in pre-commit --exec \"npm start validate\""
13 | },
14 | "files": [
15 | "dist"
16 | ],
17 | "keywords": [
18 | "react",
19 | "css",
20 | "css-in-js",
21 | "styled-components",
22 | "glamor",
23 | "jsxstyle"
24 | ],
25 | "author": "Kent C. Dodds (http://kentcdodds.com/)",
26 | "license": "MIT",
27 | "dependencies": {
28 | "brcast": "^1.1.6",
29 | "html-tag-names": "^1.1.1",
30 | "svg-tag-names": "^1.1.0"
31 | },
32 | "peerDependencies": {
33 | "glamor": ">=2",
34 | "react": ">=14"
35 | },
36 | "devDependencies": {
37 | "all-contributors-cli": "^4.1.0",
38 | "babel-cli": "^6.24.1",
39 | "babel-jest": "^19.0.0",
40 | "babel-plugin-external-helpers": "^6.22.0",
41 | "babel-preset-env": "^1.3.3",
42 | "babel-preset-react": "^6.24.1",
43 | "babel-preset-stage-2": "^6.24.1",
44 | "babel-register": "^6.24.1",
45 | "codecov": "^2.1.0",
46 | "commitizen": "^2",
47 | "common-tags": "^1.4.0",
48 | "cz-conventional-changelog": "^2.0.0",
49 | "enzyme": "^2.8.0",
50 | "enzyme-to-json": "^1.5.0",
51 | "eslint": "^3.17.0",
52 | "eslint-config-kentcdodds": "^12.0.0",
53 | "glamor": "^2.20.24",
54 | "husky": "^0.13.2",
55 | "jest-cli": "^19.0.2",
56 | "jest-glamor-react": "^1.2.0",
57 | "lint-staged": "^3.3.1",
58 | "nps": "^5.0.3",
59 | "nps-utils": "^1.1.2",
60 | "opt-cli": "^1.5.1",
61 | "prettier-eslint-cli": "^3.1.2",
62 | "prop-types": "^15.5.6",
63 | "react": "^15.5.3",
64 | "react-addons-test-utils": "^15.5.1",
65 | "react-dom": "^15.5.3",
66 | "rollup": "^0.41.6",
67 | "rollup-plugin-babel": "^2.7.1",
68 | "rollup-plugin-commonjs": "^8.0.2",
69 | "rollup-plugin-json": "^2.1.1",
70 | "rollup-plugin-node-resolve": "^3.0.0",
71 | "rollup-plugin-uglify": "^1.0.1",
72 | "semantic-release": "^6.3.6",
73 | "validate-commit-msg": "^2.12.1"
74 | },
75 | "eslintConfig": {
76 | "extends": [
77 | "kentcdodds",
78 | "kentcdodds/jest",
79 | "kentcdodds/react",
80 | "kentcdodds/prettier"
81 | ]
82 | },
83 | "lint-staged": {
84 | "*.js": [
85 | "prettier-eslint --write",
86 | "git add"
87 | ]
88 | },
89 | "jest": {
90 | "testEnvironment": "jsdom",
91 | "coverageThreshold": {
92 | "global": {
93 | "branches": 100,
94 | "functions": 100,
95 | "lines": 100,
96 | "statements": 100
97 | }
98 | },
99 | "snapshotSerializers": [
100 | "enzyme-to-json/serializer"
101 | ],
102 | "roots": [
103 | "src"
104 | ]
105 | },
106 | "config": {
107 | "commitizen": {
108 | "path": "node_modules/cz-conventional-changelog"
109 | }
110 | },
111 | "repository": {
112 | "type": "git",
113 | "url": "https://github.com/paypal/glamorous.git"
114 | },
115 | "bugs": {
116 | "url": "https://github.com/paypal/glamorous/issues"
117 | },
118 | "homepage": "https://github.com/paypal/glamorous#readme"
119 | }
120 |
--------------------------------------------------------------------------------
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "glamorous",
3 | "projectOwner": "paypal",
4 | "files": [
5 | "README.md"
6 | ],
7 | "imageSize": 100,
8 | "commit": false,
9 | "contributors": [
10 | {
11 | "login": "kentcdodds",
12 | "name": "Kent C. Dodds",
13 | "avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3",
14 | "profile": "https://kentcdodds.com",
15 | "contributions": [
16 | "code",
17 | "doc",
18 | "infra",
19 | "test",
20 | "review"
21 | ]
22 | },
23 | {
24 | "login": "CompuIves",
25 | "name": "Ives van Hoorne",
26 | "avatar_url": "https://avatars0.githubusercontent.com/u/587016?v=3",
27 | "profile": "http://ivesvh.com",
28 | "contributions": [
29 | "example"
30 | ]
31 | },
32 | {
33 | "login": "patitonar",
34 | "name": "Gerardo Nardelli",
35 | "avatar_url": "https://avatars3.githubusercontent.com/u/4614574?v=3",
36 | "profile": "https://gnardelli.com",
37 | "contributions": [
38 | "doc"
39 | ]
40 | },
41 | {
42 | "login": "crowchirp",
43 | "name": "Chandan Rai",
44 | "avatar_url": "https://avatars0.githubusercontent.com/u/14236753?v=3",
45 | "profile": "https://github.com/crowchirp",
46 | "contributions": [
47 | "doc"
48 | ]
49 | },
50 | {
51 | "login": "binhonglee",
52 | "name": "BinHong Lee",
53 | "avatar_url": "https://avatars3.githubusercontent.com/u/16726210?v=3",
54 | "profile": "https://binhonglee.github.io",
55 | "contributions": [
56 | "doc"
57 | ]
58 | },
59 | {
60 | "login": "paulmolluzzo",
61 | "name": "Paul Molluzzo",
62 | "avatar_url": "https://avatars2.githubusercontent.com/u/737065?v=3",
63 | "profile": "https://paul.molluzzo.com",
64 | "contributions": [
65 | "doc",
66 | "example"
67 | ]
68 | },
69 | {
70 | "login": "tsriram",
71 | "name": "Sriram Thiagarajan",
72 | "avatar_url": "https://avatars0.githubusercontent.com/u/450559?v=3",
73 | "profile": "http://tsriram.in",
74 | "contributions": [
75 | "code"
76 | ]
77 | },
78 | {
79 | "login": "pksjce",
80 | "name": "Pavithra Kodmad",
81 | "avatar_url": "https://avatars1.githubusercontent.com/u/417268?v=3",
82 | "profile": "https://github.com/pksjce",
83 | "contributions": [
84 | "example"
85 | ]
86 | },
87 | {
88 | "login": "vesparny",
89 | "name": "Alessandro Arnodo",
90 | "avatar_url": "https://avatars0.githubusercontent.com/u/82070?v=3",
91 | "profile": "http://alessandro.arnodo.net",
92 | "contributions": [
93 | "code",
94 | "doc",
95 | "test"
96 | ]
97 | },
98 | {
99 | "login": "developit",
100 | "name": "Jason Miller",
101 | "avatar_url": "https://avatars1.githubusercontent.com/u/105127?v=3",
102 | "profile": "https://jasonformat.com",
103 | "contributions": [
104 | "review"
105 | ]
106 | },
107 | {
108 | "login": "kwelch",
109 | "name": "Kyle Welch",
110 | "avatar_url": "https://avatars0.githubusercontent.com/u/1295580?v=3",
111 | "profile": "http://www.krwelch.com",
112 | "contributions": [
113 | "review",
114 | "example"
115 | ]
116 | },
117 | {
118 | "login": "javivelasco",
119 | "name": "Javi Velasco",
120 | "avatar_url": "https://avatars0.githubusercontent.com/u/1634922?v=3",
121 | "profile": "http://javivelasco.com",
122 | "contributions": [
123 | "review"
124 | ]
125 | },
126 | {
127 | "login": "aweary",
128 | "name": "Brandon Dail",
129 | "avatar_url": "https://avatars1.githubusercontent.com/u/6886061?v=3",
130 | "profile": "https://twitter.com/aweary",
131 | "contributions": [
132 | "review"
133 | ]
134 | },
135 | {
136 | "login": "browniefed",
137 | "name": "Jason Brown",
138 | "avatar_url": "https://avatars2.githubusercontent.com/u/1714673?v=3",
139 | "profile": "http://browniefed.com",
140 | "contributions": [
141 | "review"
142 | ]
143 | }
144 | ]
145 | }
146 |
--------------------------------------------------------------------------------
/src/__tests__/index.js:
--------------------------------------------------------------------------------
1 | /* eslint func-style:0 */
2 | import React from 'react'
3 | import * as glamor from 'glamor'
4 | import {render, mount} from 'enzyme'
5 | import * as jestGlamorReact from 'jest-glamor-react'
6 | import {oneLine} from 'common-tags'
7 | import glamorous from '../'
8 | import {CHANNEL} from '../theme-provider'
9 |
10 | expect.extend(jestGlamorReact.matcher)
11 | expect.addSnapshotSerializer(jestGlamorReact.serializer)
12 |
13 | const getMockedContext = unsubscribe => ({
14 | [CHANNEL]: {
15 | getState: () => {},
16 | setState: () => {},
17 | subscribe: () => unsubscribe,
18 | },
19 | })
20 |
21 | test('sanity test', () => {
22 | const Div = glamorous.div({marginLeft: 24})
23 | expect(render()).toMatchSnapshotWithGlamor()
24 | })
25 |
26 | test('can use pre-glamorous components with css attributes', () => {
27 | expect(
28 | render(
29 | ,
36 | ),
37 | ).toMatchSnapshotWithGlamor()
38 | })
39 |
40 | test('merges composed component styles for reasonable overrides', () => {
41 | const Parent = glamorous.div({
42 | marginTop: 1,
43 | marginRight: 1,
44 | paddingTop: 1,
45 | paddingRight: 1,
46 | })
47 | const Child = glamorous(Parent)({
48 | marginRight: 2,
49 | marginBottom: 2,
50 | paddingTop: 2,
51 | paddingRight: 2,
52 | })
53 | const Grandchild = glamorous(Child)({
54 | marginBottom: 3,
55 | marginLeft: 3,
56 | })
57 | const otherGlamorStyles1 = glamor.css({
58 | marginLeft: 4,
59 | paddingTop: 4,
60 | })
61 | const otherGlamorStyles2 = glamor.css({
62 | paddingTop: 5,
63 | paddingRight: 5,
64 | })
65 | const wrapper = render(
66 | ,
80 | )
81 | const el = wrapper.children()[0]
82 | // being explicit
83 | const included = ['other', 'classes', 'are', 'not', 'removed']
84 | included.forEach(className => {
85 | expect(el.attribs.class).toContain(className)
86 | })
87 | // still using a snapshot though for good measure
88 | expect(wrapper).toMatchSnapshotWithGlamor()
89 | })
90 |
91 | test('styles can be functions that accept props', () => {
92 | const MyDiv = glamorous.div({marginTop: 1}, ({margin}) => ({
93 | marginTop: margin,
94 | }))
95 | expect(render()).toMatchSnapshotWithGlamor()
96 | })
97 |
98 | test('falls back to `name` if displayName cannot be inferred', () => {
99 | const MyDiv = props =>
100 | const MyComp = glamorous(MyDiv)()
101 | expect(MyComp.displayName).toBe('glamorous(MyDiv)')
102 | })
103 |
104 | test('falls back to `unknown` if name cannot be inferred', () => {
105 | const MyComp = glamorous(props => )()
106 | expect(MyComp.displayName).toBe('glamorous(unknown)')
107 | })
108 |
109 | test('allows you to specify a displayName', () => {
110 | const MyComp = glamorous(props => , {
111 | displayName: 'HiThere',
112 | })()
113 | expect(MyComp.displayName).toBe('HiThere')
114 | })
115 |
116 | test('will not forward `color` to a div', () => {
117 | expect(render()).toMatchSnapshotWithGlamor()
118 | })
119 |
120 | test('will forward `color` to an svg', () => {
121 | expect(render()).toMatchSnapshotWithGlamor()
122 | })
123 |
124 | test('allows you to specify the tag rendered by a component', () => {
125 | const MySvgComponent = props =>
126 | const MyStyledSvgComponent = glamorous(MySvgComponent, {rootEl: 'svg'})({
127 | height: 1,
128 | width: 1,
129 | })
130 | expect(
131 | render(
132 | ,
133 | ),
134 | ).toMatchSnapshotWithGlamor()
135 | })
136 |
137 | test('forwards props when the GlamorousComponent.rootEl is known', () => {
138 | // this test demonstrates how to prevent glamorous from forwarding
139 | // props all the way down to components which shouldn't be getting them
140 | // (components you have no control over).
141 |
142 | // here's a component you can't change, it renders all props to it's
143 | // `rootEl` which is a `div` in this case. They probably shouldn't be doing
144 | // this, but there are use cases for libraries to do this:
145 | const SomeComponentIHaveNoControlOver = jest.fn(props => )
146 |
147 | // to prevent glamorous from forwarding non-div attributes to this
148 | // component, you can make a glamorous version out of it and specify the
149 | // `rootEl` as `div` (doesn't matter a whole lot, except in the case of
150 | // `svg`, if it's an `svg`, then definitely put `svg` otherwise, put
151 | // something else...
152 | const MyWrappedVersion = glamorous(SomeComponentIHaveNoControlOver, {
153 | rootEl: 'div',
154 | })()
155 | // no need to pass anything. This will just create be a no-op class,
156 | // no problem
157 | const MyWrappedVersionMock = jest.fn(props => (
158 |
159 | ))
160 |
161 | // from there we can use our wrapped version and it will function the
162 | // same as the original
163 | const MyMyWrappedVersion = jest.fn(props => (
164 |
165 | ))
166 |
167 | // then if we make a parent glamorous, it will forward props down until
168 | // it hits our wrapper at which time it will check whether the prop is
169 | // valid for `rootEl === 'div'`, and if it's not then it wont forward the
170 | // prop along to `SomeComponentIHaveNoControlOver` and we wont have the
171 | // warning from react about noForward being an invalid property for a
172 | // `div`. Yay!
173 | const MyStyledMyMyWrappedVersion = glamorous(MyMyWrappedVersion)({
174 | padding: 1,
175 | margin: 1,
176 | })
177 | const secretMessage = 'He likes it! Hey Mikey!'
178 | const ui = render(
179 |
180 | {secretMessage}
181 | ,
182 | )
183 | const {calls: [[calledProps]]} = SomeComponentIHaveNoControlOver.mock
184 | expect(calledProps.noForward).toBe(undefined)
185 | expect(MyWrappedVersionMock).toHaveBeenCalledWith(
186 | {
187 | className: expect.anything(),
188 | children: secretMessage,
189 | noForward: 42,
190 | },
191 | expect.anything(),
192 | expect.anything(),
193 | )
194 | expect(MyMyWrappedVersion).toHaveBeenCalledWith(
195 | {
196 | children: secretMessage,
197 | className: expect.anything(),
198 | noForward: 42,
199 | },
200 | expect.anything(),
201 | expect.anything(),
202 | )
203 | expect(ui).toMatchSnapshotWithGlamor()
204 | })
205 |
206 | test('renders a component with theme properties', () => {
207 | const Comp = glamorous.div(
208 | {
209 | color: 'red',
210 | },
211 | (props, theme) => ({padding: theme.padding}),
212 | )
213 | expect(
214 | render(),
215 | ).toMatchSnapshotWithGlamor()
216 | })
217 |
218 | test('passes an updated theme when theme prop changes', () => {
219 | const Comp = glamorous.div(
220 | {
221 | color: 'red',
222 | },
223 | (props, theme) => ({padding: theme.padding}),
224 | )
225 | const wrapper = mount()
226 | expect(wrapper).toMatchSnapshotWithGlamor(`with theme prop of padding 10px`)
227 | wrapper.setProps({theme: {padding: 20}})
228 | expect(wrapper).toMatchSnapshotWithGlamor(`with theme prop of padding 20px`)
229 | })
230 |
231 | test('cleans up theme subscription when unmounts', () => {
232 | const unsubscribe = jest.fn()
233 | const context = getMockedContext(unsubscribe)
234 | const Comp = glamorous.div()
235 | const wrapper = mount(, {context})
236 | wrapper.unmount()
237 | expect(unsubscribe).toHaveBeenCalled()
238 | })
239 |
240 | test('ignores context if a theme props is passed', () => {
241 | const unsubscribe = jest.fn()
242 | const context = getMockedContext(unsubscribe)
243 | const Comp = glamorous.div()
244 | const wrapper = mount(, {context})
245 | wrapper.unmount()
246 | expect(unsubscribe).toHaveBeenCalledTimes(0)
247 | })
248 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a relatively small abstraction that's ripe for open sourcing.
3 | * Documentation is in the README.md
4 | */
5 | import React, {Component} from 'react'
6 | import {css, styleSheet} from 'glamor'
7 | import shouldForwardProperty from './should-forward-property'
8 | import domElements from './dom-elements'
9 | import {PropTypes} from './react-compat'
10 | import ThemeProvider, {CHANNEL} from './theme-provider'
11 |
12 | /**
13 | * This is the main export and the function that people
14 | * interact with most directly.
15 | *
16 | * It accepts a component which can be a string or a React Component and returns
17 | * a "glamorousComponentFactory"
18 | * @param {String|ReactComponent} comp the component to render
19 | * @param {Object} options helpful info for the GlamorousComponents
20 | * @return {Function} the glamorousComponentFactory
21 | */
22 | function glamorous(comp, {rootEl, displayName} = {}) {
23 | return glamorousComponentFactory
24 |
25 | /**
26 | * This returns a React Component that renders the comp (closure)
27 | * with a className based on the given glamor styles object(s)
28 | * @param {...Object|Function} styles the styles to create with glamor.
29 | * If any of these are functions, they are invoked with the component
30 | * props and the return value is used.
31 | * @return {ReactComponent} the ReactComponent function
32 | */
33 | function glamorousComponentFactory(...styles) {
34 | /**
35 | * This is a component which will render the comp (closure)
36 | * with the glamorous styles (closure). Forwards any valid
37 | * props to the underlying component.
38 | * @param {Object} theme the theme object
39 | * @return {ReactElement} React.createElement
40 | */
41 | class GlamorousComponent extends Component {
42 | state = {theme: null}
43 | setTheme = theme => this.setState({theme})
44 |
45 | componentWillMount() {
46 | const {theme} = this.props
47 | if (this.context[CHANNEL]) {
48 | // if a theme is provided via props, it takes precedence over context
49 | this.setTheme(theme ? theme : this.context[CHANNEL].getState())
50 | } else {
51 | this.setTheme(theme || {})
52 | }
53 | }
54 |
55 | componentWillReceiveProps(nextProps) {
56 | if (this.props.theme !== nextProps.theme) {
57 | this.setTheme(nextProps.theme)
58 | }
59 | }
60 |
61 | componentDidMount() {
62 | if (this.context[CHANNEL] && !this.props.theme) {
63 | // subscribe to future theme changes
64 | this.unsubscribe = this.context[CHANNEL].subscribe(this.setTheme)
65 | }
66 | }
67 |
68 | componentWillUnmount() {
69 | // cleanup subscription
70 | this.unsubscribe && this.unsubscribe()
71 | }
72 |
73 | render() {
74 | const {className, ...rest} = this.props
75 | const {toForward, cssOverrides} = splitProps(rest, GlamorousComponent)
76 | // create className to apply
77 | const mappedArgs = GlamorousComponent.styles.map(glamorRules => {
78 | if (typeof glamorRules === 'function') {
79 | return glamorRules(this.props, {...this.state.theme})
80 | }
81 | return glamorRules
82 | })
83 | const {
84 | glamorStyles: parentGlamorStyles,
85 | glamorlessClassName,
86 | } = extractGlamorStyles(className)
87 | const glamorClassName = css(
88 | ...mappedArgs,
89 | ...parentGlamorStyles,
90 | cssOverrides,
91 | ).toString()
92 | const fullClassName = joinClasses(glamorlessClassName, glamorClassName)
93 |
94 | return React.createElement(GlamorousComponent.comp, {
95 | className: fullClassName,
96 | ...toForward,
97 | })
98 | }
99 | }
100 |
101 | GlamorousComponent.propTypes = {
102 | className: PropTypes.string,
103 | cssOverrides: PropTypes.object,
104 | theme: PropTypes.object,
105 | }
106 |
107 | GlamorousComponent.contextTypes = {
108 | [CHANNEL]: PropTypes.object,
109 | }
110 |
111 | Object.assign(
112 | GlamorousComponent,
113 | getGlamorousComponentMetadata({comp, styles, rootEl, displayName}),
114 | )
115 | return GlamorousComponent
116 | }
117 | }
118 |
119 | function getGlamorousComponentMetadata({comp, styles, rootEl, displayName}) {
120 | const componentsComp = comp.comp ? comp.comp : comp
121 | return {
122 | // join styles together (for anyone doing: glamorous(glamorous.a({}), {}))
123 | styles: comp.styles ? comp.styles.concat(styles) : styles,
124 | // keep track of the ultimate rootEl to render (we never
125 | // actually render anything but
126 | // the base component, even when people wrap a glamorous
127 | // component in glamorous
128 | comp: componentsComp,
129 | rootEl: rootEl || componentsComp,
130 | // set the displayName to something that's slightly more
131 | // helpful than `GlamorousComponent` :)
132 | displayName: displayName || `glamorous(${getDisplayName(comp)})`,
133 | }
134 | }
135 |
136 | function getDisplayName(comp) {
137 | return typeof comp === 'string' ?
138 | comp :
139 | comp.displayName || comp.name || 'unknown'
140 | }
141 |
142 | /*
143 | * This creates a glamorousComponentFactory for every DOM element so you can
144 | * simply do:
145 | * const GreenButton = glamorous.button({
146 | * backgroundColor: 'green',
147 | * padding: 20,
148 | * })
149 | * Click Me!
150 | */
151 | Object.assign(
152 | glamorous,
153 | domElements.reduce(
154 | (getters, tag) => {
155 | getters[tag] = glamorous(tag)
156 | return getters
157 | },
158 | {},
159 | ),
160 | )
161 |
162 | /*
163 | * This creates a glamorous component for each DOM element so you can
164 | * simply do:
165 | *
169 | * I'm green!
170 | *
171 | */
172 | Object.assign(
173 | glamorous,
174 | domElements.reduce(
175 | (comps, tag) => {
176 | const capitalTag = capitalize(tag)
177 | comps[capitalTag] = glamorous[tag]()
178 | comps[capitalTag].displayName = `glamorous.${capitalTag}`
179 | comps[capitalTag].propsAreCssOverrides = true
180 | return comps
181 | },
182 | {},
183 | ),
184 | )
185 |
186 | /**
187 | * This function takes a className string and gets all the
188 | * associated glamor styles. It's used to merge glamor styles
189 | * from a className to make sure that specificity is not
190 | * a problem when passing a className to a component.
191 | * @param {String} [className=''] the className string
192 | * @return {Object} { glamorStyles, glamorlessClassName }
193 | * - glamorStyles is an array of all the glamor styles objects
194 | * - glamorlessClassName is the rest of the className string
195 | * without the glamor classNames
196 | */
197 | function extractGlamorStyles(className = '') {
198 | return className.toString().split(' ').reduce((groups, name) => {
199 | if (name.indexOf('css-') === 0) {
200 | const id = name.slice('css-'.length)
201 | const {style} = styleSheet.registered[id]
202 | groups.glamorStyles.push(style)
203 | } else {
204 | groups.glamorlessClassName = joinClasses(
205 | groups.glamorlessClassName,
206 | name,
207 | )
208 | }
209 | return groups
210 | }, {glamorlessClassName: '', glamorStyles: []})
211 | }
212 |
213 | function splitProps(
214 | {css: cssOverrides = {}, ...rest},
215 | {propsAreCssOverrides, rootEl},
216 | ) {
217 | const returnValue = {toForward: {}, cssOverrides: {}}
218 | if (!propsAreCssOverrides) {
219 | returnValue.cssOverrides = cssOverrides
220 | if (typeof rootEl !== 'string') {
221 | // if it's not a string, then we can forward everything
222 | // (because it's a component)
223 | returnValue.toForward = rest
224 | return returnValue
225 | }
226 | }
227 | return Object.keys(rest).reduce(
228 | (split, propName) => {
229 | if (shouldForwardProperty(rootEl, propName)) {
230 | split.toForward[propName] = rest[propName]
231 | } else if (propsAreCssOverrides) {
232 | split.cssOverrides[propName] = rest[propName]
233 | }
234 | return split
235 | },
236 | returnValue,
237 | )
238 | }
239 |
240 | function capitalize(s) {
241 | return s.slice(0, 1).toUpperCase() + s.slice(1)
242 | }
243 |
244 | function joinClasses(...classes) {
245 | return classes.filter(Boolean).join(' ')
246 | }
247 |
248 | export default glamorous
249 | export {ThemeProvider}
250 |
--------------------------------------------------------------------------------
/src/should-forward-property.js:
--------------------------------------------------------------------------------
1 | /* eslint max-lines:0, func-style:0 */
2 | // copied from:
3 | // https://github.com/styled-components/styled-components/tree/
4 | // 956e8210b6277860c89404f9cb08735f97eaa7e1/src/utils/validAttr.js
5 | /* Trying to avoid the unknown-prop errors on glamorous components
6 | by filtering by React's attribute whitelist.
7 | */
8 |
9 | /* Logic copied from ReactDOMUnknownPropertyHook */
10 | const reactProps = [
11 | 'children',
12 | 'dangerouslySetInnerHTML',
13 | 'key',
14 | 'ref',
15 | 'autoFocus',
16 | 'defaultValue',
17 | 'valueLink',
18 | 'defaultChecked',
19 | 'checkedLink',
20 | 'innerHTML',
21 | 'suppressContentEditableWarning',
22 | 'onFocusIn',
23 | 'onFocusOut',
24 | 'className',
25 |
26 | /* List copied from https://facebook.github.io/react/docs/events.html */
27 | 'onCopy',
28 | 'onCut',
29 | 'onPaste',
30 | 'onCompositionEnd',
31 | 'onCompositionStart',
32 | 'onCompositionUpdate',
33 | 'onKeyDown',
34 | 'onKeyPress',
35 | 'onKeyUp',
36 | 'onFocus',
37 | 'onBlur',
38 | 'onChange',
39 | 'onInput',
40 | 'onSubmit',
41 | 'onClick',
42 | 'onContextMenu',
43 | 'onDoubleClick',
44 | 'onDrag',
45 | 'onDragEnd',
46 | 'onDragEnter',
47 | 'onDragExit',
48 | 'onDragLeave',
49 | 'onDragOver',
50 | 'onDragStart',
51 | 'onDrop',
52 | 'onMouseDown',
53 | 'onMouseEnter',
54 | 'onMouseLeave',
55 | 'onMouseMove',
56 | 'onMouseOut',
57 | 'onMouseOver',
58 | 'onMouseUp',
59 | 'onSelect',
60 | 'onTouchCancel',
61 | 'onTouchEnd',
62 | 'onTouchMove',
63 | 'onTouchStart',
64 | 'onScroll',
65 | 'onWheel',
66 | 'onAbort',
67 | 'onCanPlay',
68 | 'onCanPlayThrough',
69 | 'onDurationChange',
70 | 'onEmptied',
71 | 'onEncrypted',
72 | 'onEnded',
73 | 'onError',
74 | 'onLoadedData',
75 | 'onLoadedMetadata',
76 | 'onLoadStart',
77 | 'onPause',
78 | 'onPlay',
79 | 'onPlaying',
80 | 'onProgress',
81 | 'onRateChange',
82 | 'onSeeked',
83 | 'onSeeking',
84 | 'onStalled',
85 | 'onSuspend',
86 | 'onTimeUpdate',
87 | 'onVolumeChange',
88 | 'onWaiting',
89 | 'onLoad',
90 | 'onAnimationStart',
91 | 'onAnimationEnd',
92 | 'onAnimationIteration',
93 | 'onTransitionEnd',
94 |
95 | 'onCopyCapture',
96 | 'onCutCapture',
97 | 'onPasteCapture',
98 | 'onCompositionEndCapture',
99 | 'onCompositionStartCapture',
100 | 'onCompositionUpdateCapture',
101 | 'onKeyDownCapture',
102 | 'onKeyPressCapture',
103 | 'onKeyUpCapture',
104 | 'onFocusCapture',
105 | 'onBlurCapture',
106 | 'onChangeCapture',
107 | 'onInputCapture',
108 | 'onSubmitCapture',
109 | 'onClickCapture',
110 | 'onContextMenuCapture',
111 | 'onDoubleClickCapture',
112 | 'onDragCapture',
113 | 'onDragEndCapture',
114 | 'onDragEnterCapture',
115 | 'onDragExitCapture',
116 | 'onDragLeaveCapture',
117 | 'onDragOverCapture',
118 | 'onDragStartCapture',
119 | 'onDropCapture',
120 | 'onMouseDownCapture',
121 | 'onMouseEnterCapture',
122 | 'onMouseLeaveCapture',
123 | 'onMouseMoveCapture',
124 | 'onMouseOutCapture',
125 | 'onMouseOverCapture',
126 | 'onMouseUpCapture',
127 | 'onSelectCapture',
128 | 'onTouchCancelCapture',
129 | 'onTouchEndCapture',
130 | 'onTouchMoveCapture',
131 | 'onTouchStartCapture',
132 | 'onScrollCapture',
133 | 'onWheelCapture',
134 | 'onAbortCapture',
135 | 'onCanPlayCapture',
136 | 'onCanPlayThroughCapture',
137 | 'onDurationChangeCapture',
138 | 'onEmptiedCapture',
139 | 'onEncryptedCapture',
140 | 'onEndedCapture',
141 | 'onErrorCapture',
142 | 'onLoadedDataCapture',
143 | 'onLoadedMetadataCapture',
144 | 'onLoadStartCapture',
145 | 'onPauseCapture',
146 | 'onPlayCapture',
147 | 'onPlayingCapture',
148 | 'onProgressCapture',
149 | 'onRateChangeCapture',
150 | 'onSeekedCapture',
151 | 'onSeekingCapture',
152 | 'onStalledCapture',
153 | 'onSuspendCapture',
154 | 'onTimeUpdateCapture',
155 | 'onVolumeChangeCapture',
156 | 'onWaitingCapture',
157 | 'onLoadCapture',
158 | 'onAnimationStartCapture',
159 | 'onAnimationEndCapture',
160 | 'onAnimationIterationCapture',
161 | 'onTransitionEndCapture',
162 | ]
163 |
164 | /* From HTMLDOMPropertyConfig */
165 | const htmlProps = [
166 | /**
167 | * Standard Properties
168 | */
169 | 'accept',
170 | 'acceptCharset',
171 | 'accessKey',
172 | 'action',
173 | 'allowFullScreen',
174 | 'allowTransparency',
175 | 'alt',
176 | // specifies target context for links with `preload` type
177 | 'as',
178 | 'async',
179 | 'autoComplete',
180 | // autoFocus is polyfilled/normalized by AutoFocusUtils
181 | '// autoFocus',
182 | 'autoPlay',
183 | 'capture',
184 | 'cellPadding',
185 | 'cellSpacing',
186 | 'charSet',
187 | 'challenge',
188 | 'checked',
189 | 'cite',
190 | 'classID',
191 | 'className',
192 | 'cols',
193 | 'colSpan',
194 | 'content',
195 | 'contentEditable',
196 | 'contextMenu',
197 | 'controls',
198 | 'coords',
199 | 'crossOrigin',
200 | 'data', // For `` acts as `src`.
201 | 'dateTime',
202 | 'default',
203 | 'defer',
204 | 'dir',
205 | 'disabled',
206 | 'download',
207 | 'draggable',
208 | 'encType',
209 | 'form',
210 | 'formAction',
211 | 'formEncType',
212 | 'formMethod',
213 | 'formNoValidate',
214 | 'formTarget',
215 | 'frameBorder',
216 | 'headers',
217 | 'height',
218 | 'hidden',
219 | 'high',
220 | 'href',
221 | 'hrefLang',
222 | 'htmlFor',
223 | 'httpEquiv',
224 | 'icon',
225 | 'id',
226 | 'inputMode',
227 | 'integrity',
228 | 'is',
229 | 'keyParams',
230 | 'keyType',
231 | 'kind',
232 | 'label',
233 | 'lang',
234 | 'list',
235 | 'loop',
236 | 'low',
237 | 'manifest',
238 | 'marginHeight',
239 | 'marginWidth',
240 | 'max',
241 | 'maxLength',
242 | 'media',
243 | 'mediaGroup',
244 | 'method',
245 | 'min',
246 | 'minLength',
247 | // Caution; `option.selected` is not updated if `select.multiple` is
248 | // disabled with `removeAttribute`.
249 | 'multiple',
250 | 'muted',
251 | 'name',
252 | 'nonce',
253 | 'noValidate',
254 | 'open',
255 | 'optimum',
256 | 'pattern',
257 | 'placeholder',
258 | 'playsInline',
259 | 'poster',
260 | 'preload',
261 | 'profile',
262 | 'radioGroup',
263 | 'readOnly',
264 | 'referrerPolicy',
265 | 'rel',
266 | 'required',
267 | 'reversed',
268 | 'role',
269 | 'rows',
270 | 'rowSpan',
271 | 'sandbox',
272 | 'scope',
273 | 'scoped',
274 | 'scrolling',
275 | 'seamless',
276 | 'selected',
277 | 'shape',
278 | 'size',
279 | 'sizes',
280 | 'span',
281 | 'spellCheck',
282 | 'src',
283 | 'srcDoc',
284 | 'srcLang',
285 | 'srcSet',
286 | 'start',
287 | 'step',
288 | 'style',
289 | 'summary',
290 | 'tabIndex',
291 | 'target',
292 | 'title',
293 | // Setting .type throws on non- tags
294 | 'type',
295 | 'useMap',
296 | 'value',
297 | 'width',
298 | 'wmode',
299 | 'wrap',
300 |
301 | /**
302 | * RDFa Properties
303 | */
304 | 'about',
305 | 'datatype',
306 | 'inlist',
307 | 'prefix',
308 | // property is also supported for OpenGraph in meta tags.
309 | 'property',
310 | 'resource',
311 | 'typeof',
312 | 'vocab',
313 |
314 | /**
315 | * Non-standard Properties
316 | */
317 | // autoCapitalize and autoCorrect are supported in Mobile Safari for
318 | // keyboard hints.
319 | 'autoCapitalize',
320 | 'autoCorrect',
321 | // autoSave allows WebKit/Blink to persist values of
322 | // input fields on page reloads
323 | 'autoSave',
324 | // color is for Safari mask-icon link
325 | 'color',
326 | // itemProp, itemScope, itemType are for
327 | // Microdata support. See http://schema.org/docs/gs.html
328 | 'itemProp',
329 | 'itemScope',
330 | 'itemType',
331 | // itemID and itemRef are for Microdata support as well but
332 | // only specified in the WHATWG spec document. See
333 | // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api
334 | 'itemID',
335 | 'itemRef',
336 | // results show looking glass icon and recent searches on input
337 | // search fields in WebKit/Blink
338 | 'results',
339 | // IE-only attribute that specifies security restrictions on an iframe
340 | // as an alternative to the sandbox attribute on IE<10
341 | 'security',
342 | // IE-only attribute that controls focus behavior
343 | 'unselectable',
344 | ]
345 |
346 | const svgProps = [
347 | 'accentHeight',
348 | 'accumulate',
349 | 'additive',
350 | 'alignmentBaseline',
351 | 'allowReorder',
352 | 'alphabetic',
353 | 'amplitude',
354 | 'arabicForm',
355 | 'ascent',
356 | 'attributeName',
357 | 'attributeType',
358 | 'autoReverse',
359 | 'azimuth',
360 | 'baseFrequency',
361 | 'baseProfile',
362 | 'baselineShift',
363 | 'bbox',
364 | 'begin',
365 | 'bias',
366 | 'by',
367 | 'calcMode',
368 | 'capHeight',
369 | 'clip',
370 | 'clipPath',
371 | 'clipRule',
372 | 'clipPathUnits',
373 | 'colorInterpolation',
374 | 'colorInterpolationFilters',
375 | 'colorProfile',
376 | 'colorRendering',
377 | 'contentScriptType',
378 | 'contentStyleType',
379 | 'cursor',
380 | 'cx',
381 | 'cy',
382 | 'd',
383 | 'decelerate',
384 | 'descent',
385 | 'diffuseConstant',
386 | 'direction',
387 | 'display',
388 | 'divisor',
389 | 'dominantBaseline',
390 | 'dur',
391 | 'dx',
392 | 'dy',
393 | 'edgeMode',
394 | 'elevation',
395 | 'enableBackground',
396 | 'end',
397 | 'exponent',
398 | 'externalResourcesRequired',
399 | 'fill',
400 | 'fillOpacity',
401 | 'fillRule',
402 | 'filter',
403 | 'filterRes',
404 | 'filterUnits',
405 | 'floodColor',
406 | 'floodOpacity',
407 | 'focusable',
408 | 'fontFamily',
409 | 'fontSize',
410 | 'fontSizeAdjust',
411 | 'fontStretch',
412 | 'fontStyle',
413 | 'fontVariant',
414 | 'fontWeight',
415 | 'format',
416 | 'from',
417 | 'fx',
418 | 'fy',
419 | 'g1',
420 | 'g2',
421 | 'glyphName',
422 | 'glyphOrientationHorizontal',
423 | 'glyphOrientationVertical',
424 | 'glyphRef',
425 | 'gradientTransform',
426 | 'gradientUnits',
427 | 'hanging',
428 | 'horizAdvX',
429 | 'horizOriginX',
430 | 'ideographic',
431 | 'imageRendering',
432 | 'in',
433 | 'in2',
434 | 'intercept',
435 | 'k',
436 | 'k1',
437 | 'k2',
438 | 'k3',
439 | 'k4',
440 | 'kernelMatrix',
441 | 'kernelUnitLength',
442 | 'kerning',
443 | 'keyPoints',
444 | 'keySplines',
445 | 'keyTimes',
446 | 'lengthAdjust',
447 | 'letterSpacing',
448 | 'lightingColor',
449 | 'limitingConeAngle',
450 | 'local',
451 | 'markerEnd',
452 | 'markerMid',
453 | 'markerStart',
454 | 'markerHeight',
455 | 'markerUnits',
456 | 'markerWidth',
457 | 'mask',
458 | 'maskContentUnits',
459 | 'maskUnits',
460 | 'mathematical',
461 | 'mode',
462 | 'numOctaves',
463 | 'offset',
464 | 'opacity',
465 | 'operator',
466 | 'order',
467 | 'orient',
468 | 'orientation',
469 | 'origin',
470 | 'overflow',
471 | 'overlinePosition',
472 | 'overlineThickness',
473 | 'paintOrder',
474 | 'panose1',
475 | 'pathLength',
476 | 'patternContentUnits',
477 | 'patternTransform',
478 | 'patternUnits',
479 | 'pointerEvents',
480 | 'points',
481 | 'pointsAtX',
482 | 'pointsAtY',
483 | 'pointsAtZ',
484 | 'preserveAlpha',
485 | 'preserveAspectRatio',
486 | 'primitiveUnits',
487 | 'r',
488 | 'radius',
489 | 'refX',
490 | 'refY',
491 | 'renderingIntent',
492 | 'repeatCount',
493 | 'repeatDur',
494 | 'requiredExtensions',
495 | 'requiredFeatures',
496 | 'restart',
497 | 'result',
498 | 'rotate',
499 | 'rx',
500 | 'ry',
501 | 'scale',
502 | 'seed',
503 | 'shapeRendering',
504 | 'slope',
505 | 'spacing',
506 | 'specularConstant',
507 | 'specularExponent',
508 | 'speed',
509 | 'spreadMethod',
510 | 'startOffset',
511 | 'stdDeviation',
512 | 'stemh',
513 | 'stemv',
514 | 'stitchTiles',
515 | 'stopColor',
516 | 'stopOpacity',
517 | 'strikethroughPosition',
518 | 'strikethroughThickness',
519 | 'string',
520 | 'stroke',
521 | 'strokeDasharray',
522 | 'strokeDashoffset',
523 | 'strokeLinecap',
524 | 'strokeLinejoin',
525 | 'strokeMiterlimit',
526 | 'strokeOpacity',
527 | 'strokeWidth',
528 | 'surfaceScale',
529 | 'systemLanguage',
530 | 'tableValues',
531 | 'targetX',
532 | 'targetY',
533 | 'textAnchor',
534 | 'textDecoration',
535 | 'textRendering',
536 | 'textLength',
537 | 'to',
538 | 'transform',
539 | 'u1',
540 | 'u2',
541 | 'underlinePosition',
542 | 'underlineThickness',
543 | 'unicode',
544 | 'unicodeBidi',
545 | 'unicodeRange',
546 | 'unitsPerEm',
547 | 'vAlphabetic',
548 | 'vHanging',
549 | 'vIdeographic',
550 | 'vMathematical',
551 | 'values',
552 | 'vectorEffect',
553 | 'version',
554 | 'vertAdvY',
555 | 'vertOriginX',
556 | 'vertOriginY',
557 | 'viewBox',
558 | 'viewTarget',
559 | 'visibility',
560 | 'widths',
561 | 'wordSpacing',
562 | 'writingMode',
563 | 'x',
564 | 'xHeight',
565 | 'x1',
566 | 'x2',
567 | 'xChannelSelector',
568 | 'xlinkActuate',
569 | 'xlinkArcrole',
570 | 'xlinkHref',
571 | 'xlinkRole',
572 | 'xlinkShow',
573 | 'xlinkTitle',
574 | 'xlinkType',
575 | 'xmlBase',
576 | 'xmlns',
577 | 'xmlnsXlink',
578 | 'xmlLang',
579 | 'xmlSpace',
580 | 'y',
581 | 'y1',
582 | 'y2',
583 | 'yChannelSelector',
584 | 'z',
585 | 'zoomAndPan',
586 | ]
587 |
588 | // these are valid attributes that have the
589 | // same name as CSS properties, and is used
590 | // for css overrides API
591 | const cssProps = ['color', 'height', 'width']
592 |
593 | /* From DOMProperty */
594 | // eslint-disable-next-line max-len
595 | const ATTRIBUTE_NAME_START_CHAR = ':A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD'
596 | // eslint-disable-next-line max-len
597 | const ATTRIBUTE_NAME_CHAR = `${ATTRIBUTE_NAME_START_CHAR}\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040`
598 | const isCustomAttribute = RegExp.prototype.test.bind(
599 | new RegExp(`^(data|aria)-[${ATTRIBUTE_NAME_CHAR}]*$`),
600 | )
601 |
602 | const hasItem = (list, name) => list.indexOf(name) !== -1
603 | const isHtmlProp = name => hasItem(htmlProps, name)
604 | const isCssProp = name => hasItem(cssProps, name)
605 | const isSvgProp = (tagName, name) =>
606 | tagName === 'svg' && hasItem(svgProps, name)
607 | const isReactProp = name => hasItem(reactProps, name)
608 |
609 | // eslint-disable-next-line complexity
610 | const shouldForwardProperty = (tagName, name) =>
611 | typeof tagName !== 'string' ||
612 | ((isHtmlProp(name) ||
613 | isSvgProp(tagName, name) ||
614 | isCustomAttribute(name.toLowerCase()) ||
615 | isReactProp(name)) &&
616 | (tagName === 'svg' || !isCssProp(name)))
617 |
618 | export default shouldForwardProperty
619 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # glamorous
4 |
5 | React component styling solved with an elegant ([inspired](#inspiration)) API,
6 | small footprint (~6kb gzipped), and great performance (via [`glamor`][glamor]).
7 |
8 | > Read [the intro blogpost][intro-blogpost]
9 |
10 | [![Build Status][build-badge]][build]
11 | [![Code Coverage][coverage-badge]][coverage]
12 | [![Dependencies][dependencyci-badge]][dependencyci]
13 | [![version][version-badge]][package]
14 | [![downloads][downloads-badge]][npm-stat]
15 | [![MIT License][license-badge]][LICENSE]
16 |
17 | [](#contributors)
18 | [![PRs Welcome][prs-badge]][prs]
19 | [![Chat][chat-badge]][chat]
20 | [![Donate][donate-badge]][donate]
21 | [![Code of Conduct][coc-badge]][coc]
22 | [![Roadmap][roadmap-badge]][roadmap]
23 | [![Examples][examples-badge]][examples]
24 |
25 | [![gzip size][gzip-badge]][unpkg-dist]
26 | [![size][size-badge]][unpkg-dist]
27 | [![module formats: umd, cjs, and es][module-formats-badge]][unpkg-dist]
28 | [![Watch on GitHub][github-watch-badge]][github-watch]
29 | [![Star on GitHub][github-star-badge]][github-star]
30 | [![Tweet][twitter-badge]][twitter]
31 |
32 | ## The problem
33 |
34 | You like CSS in JS, but you don't like having to create entire component
35 | functions just for styling purposes. You don't want to give a name to something
36 | that's purely style-related. And it's just kind of annoying to do the
37 | style-creating, `className` assigning, and props-forwarding song and dance.
38 |
39 | For example, this is what you have to do with raw `glamor` (or `aphrodite` or
40 | similar for that matter):
41 |
42 | ```jsx
43 | const styles = glamor.css({
44 | fontSize: 20,
45 | textAlign: 'center',
46 | })
47 | function MyStyledDiv({className = '', ...rest}) {
48 | return (
49 |
53 | )
54 | }
55 | ```
56 |
57 | ## This solution
58 |
59 | With `glamorous`, that example above looks as simple as this:
60 |
61 | ```javascript
62 | const MyStyledDiv = glamorous.div({
63 | fontSize: 20,
64 | textAlign: 'center',
65 | })
66 | ```
67 |
68 | In fact, it's even better, because there are a bunch of features that make
69 | composing these components together really nice!
70 |
71 | Oh, and what if you didn't care to give `MyStyledDiv` a name? If you just want
72 | a div that's styled using glamor? You can do that too:
73 |
74 | ```jsx
75 | const { Div } = glamorous
76 |
77 | function App() {
78 | return (
79 |
83 | Hello world!
84 |
85 | )
86 | }
87 | ```
88 |
89 | So that's the basics of this solution... Let's get to the details!
90 |
91 | ## Installation
92 |
93 | This module is distributed via [npm][npm] which is bundled with [node][node] and
94 | should be installed as one of your project's `dependencies`:
95 |
96 | ```
97 | npm install --save glamorous
98 | ```
99 |
100 | This also depends on `react` and `glamor` so you'll need those in your project
101 | as well (if you don't already have them):
102 |
103 | ```
104 | npm install --save react glamor
105 | ```
106 |
107 | > NOTE: If you're using React v15.5 or greater, you'll also need to have
108 | > `prop-types` installed: `npm install --save prop-types`
109 |
110 | You can then use one of the module formats:
111 |
112 | - `main`: `dist/glamorous.cjs.js` - exports itself as a CommonJS module
113 | - `global`: `dist/glamorous.umd.js` and `dist/glamorous.umd.min.js` - exports
114 | itself as a [umd][umd] module which is consumable in several environments, the
115 | most notable as a global.
116 | - `jsnext:main` and `module`: `dist/glamorous.es.js` - exports itself using the
117 | ES modules specification, you'll need to configure webpack to make use of this
118 | file do this using the [resolve.mainFields][mainFields] property.
119 |
120 | The most common use-case is consuming this module via CommonJS:
121 |
122 | ```javascript
123 | const glamorous = require('glamorous')
124 | const {ThemeProvider} = glamorous
125 | // etc.
126 | ```
127 |
128 | If you're transpiling (and/or using the `jsnext:main`):
129 |
130 | ```javascript
131 | import glamorous, {ThemeProvider} from 'glamorous'
132 | ```
133 |
134 | If you want to use the global:
135 |
136 | ```html
137 |
138 |
139 |
140 |
141 |
142 |
143 |
148 | ```
149 |
150 | ## Terms and concepts
151 |
152 | ### glamorous
153 |
154 | The `glamorous` function is the main (only) export. It allows you to create
155 | glamorous components that render the styles to the component you give it. This
156 | is done by forwarding a `className` prop to the component you tell it to render.
157 | But before we get into how you wrap custom components, let's talk about the
158 | built-in DOM components.
159 |
160 | #### built-in DOM component factories
161 |
162 | For every DOM element, there is an associated `glamorous` component factory
163 | attached to the `glamorous` function. As above, you can access these factories
164 | like so: `glamorous.div`, `glamorous.a`, `glamorous.article`, etc.
165 |
166 | #### glamorousComponentFactory
167 |
168 | Whether you create one yourself or use one of the built-in ones mentioned above,
169 | each `glamorousComponentFactory` allows you to invoke it with styles and it
170 | returns you a new component which will have those styles applied when it's
171 | rendered. This is accomplished by generating a `className` for the styles you
172 | give and forwarding that `className` onto the rendered element. So if you're
173 | wrapping a component you intend to style, you'll need to make sure you accept
174 | the `className` as a prop and apply it to where you want the styles applied in
175 | your custom component (normally the root element).
176 |
177 | ##### ...styles
178 |
179 | The `glamorousComponentFactory` accepts any number of style object arguments.
180 | These can be style objects or functions which are invoked with `props` on every
181 | render and return style objects. To learn more about what these style objects
182 | can look like, please take a look at the [`glamor`][glamor] documentation.
183 |
184 | #### GlamorousComponent
185 |
186 | The `GlamorousComponent` is what is returned from the
187 | `glamorousComponentFactory`. Its job is to get all the styles together get a
188 | `className` (from [`glamor`][glamor]) and forward that on to your component.
189 |
190 | For examples below, we'll use this as our GlamorousComponent:
191 |
192 | ```javascript
193 | const MyStyledDiv = glamorous.div({margin: 1, fontSize: 1, padding: 1})
194 | ```
195 |
196 | It does a few interesting things based on the props you pass it:
197 |
198 | ##### `className`
199 |
200 | For each `className` you provide, the `GlamorousComponent` will check to see
201 | whether it is a [`glamor`][glamor] generated `className` (can be from raw glamor
202 | or from `glamorous`, doesn't matter). If it is, it will get the original styles
203 | that were used to generate that `className` and merge those with the styles for
204 | the element that's rendered in a way that the provided `className`'s styles win
205 | in the event of a conflict.
206 |
207 | If the `className` is not generated by `glamor`, then it will simply be
208 | forwarded along with the `GlamorousComponent`-generated `className`.
209 |
210 | ```jsx
211 | const myCustomGlamorStyles = glamor.css({fontSize: 2})
212 |
213 | // styles applied:
214 | // {margin: 1, fontSize: 2, padding: 1}
215 | // as well as any styles custom-class applies
216 | ```
217 |
218 | ##### `cssOverrides`
219 |
220 | This is an object and if provided, it will be merged with this component's and
221 | take highest priority over the component's predefined styles.
222 |
223 | ```jsx
224 | const myCustomGlamorStyles = glamor.css({fontSize: 2, padding: 2})
225 |
229 | // styles applied:
230 | // {margin: 1, fontSize: 2, padding: 3}
231 | // as well as any styles custom-class applies
232 | ```
233 |
234 | ##### other props
235 |
236 | Only props that are safe to forward to the specific `element` that will
237 | ultimately be rendered will be forwarded. So this is totally legit:
238 |
239 | ```jsx
240 |
241 | ```
242 |
243 | A use case for doing something like this would be for dynamic styles:
244 |
245 | ```javascript
246 | const staticStyles = {color: 'green'}
247 | const dynamicStyles = props => {fontSize: props.size === 'big' ? 32 : 24}
248 | const MyDynamicallyStyledDiv = glamorous.div(staticStyles, dynamicStyles)
249 | ```
250 |
251 | > The exception to this prop forwarding is the pre-created `GlamorousComponent`s
252 | > (see below).
253 |
254 | #### built-in GlamorousComponents
255 |
256 | Often you want to style something without actually giving it a name (because
257 | naming things is hard). So glamorous also exposes a pre-created
258 | `GlamorousComponent` for each DOM node type which make this reasonable to do:
259 |
260 | ```jsx
261 | const { Div, Span, A, Img } = glamorous
262 |
263 | function MyUserInterface({name, tagline, imageUrl, homepage, size}) {
264 | const nameSize = size
265 | const taglineSize = size * 0.5
266 | return (
267 |
276 | )
277 | }
278 | ```
279 |
280 | Having to name all of that stuff could be tedious, so having these pre-built
281 | components is handy. The other handy bit here is that the props _are_ the styles
282 | for these components. Notice that glamorous can distinguish between props that
283 | are for styling and those that are have semantic meaning (like with the `Img`
284 | and `A` components which make use of `src` and `href` props).
285 |
286 | One other tip... This totally works:
287 |
288 | ```jsx
289 |
290 | JSX is pretty wild!
291 |
292 | ```
293 |
294 | ### Theming
295 |
296 | `glamorous` fully supports theming using a special `` component.
297 |
298 | It provides the `theme` to all glamorous components down the tree.
299 |
300 | > Try this out in your browser [here](https://codesandbox.io/s/o2yq9MkQk)!
301 |
302 | ```jsx
303 | import glamorous, {ThemeProvider} from glamorous
304 |
305 | // our main theme object
306 | const theme = {
307 | main: {color: 'red'}
308 | }
309 |
310 | // our secondary theme object
311 | const secondaryTheme = {
312 | main: {color: 'blue'}
313 | }
314 |
315 | // a themed component
316 | const Title = glamorous.h1({
317 | fontSize: '10px'
318 | }, (props, theme) => ({
319 | color: theme.main.color
320 | }))
321 |
322 | // use to pass theme down the tree
323 |
324 | Hello!
325 |
326 |
327 | // it is possible to nest themes
328 | // inner themes will be merged with outers
329 |
330 |
331 | Hello!
332 |
333 | {/* this will be blue */}
334 | Hello from here!
335 |
336 |
337 |
338 |
339 | // to override a theme, just pass a theme prop to a glamorous component
340 | // the component will ignore any surrounding theme, applying the one passed directly via props
341 |
342 | {/* this will be yellow */}
343 | Hello!
344 |
345 | ```
346 |
347 | ### Server Side Rendering (SSR)
348 |
349 | Because both `glamor` and `react` support SSR, `glamorous` does too! I actually
350 | do this on [my personal site](https://github.com/kentcdodds/kentcdodds.com)
351 | which is generated at build-time on the server. Learn about rendering
352 | [`react` on the server][react-ssr] and [`glamor` too][glamor-ssr].
353 |
354 | ### Example Style Objects
355 |
356 | Style objects can affect pseudo-classes and pseduo-elements, complex CSS
357 | selectors, introduce keyframe animations, and use media queries:
358 |
359 |
360 | pseudo-class
361 |
362 | ```javascript
363 | const MyLink = glamorous.a({
364 | ':hover': {
365 | color: 'red'
366 | }
367 | })
368 |
369 | // Use in a render function
370 | GitHub
371 | ```
372 |
373 |
374 |
375 | pseudo-element
376 |
377 | ```jsx
378 | const MyListItem = glamorous.li({
379 | listStyleType: 'none',
380 | position: 'relative',
381 | '&::before': {
382 | content: `'#'`, // be sure the quotes are included in the passed string
383 | display: 'block',
384 | position: 'absolute',
385 | left: '-20px',
386 | width: '20px',
387 | height: '20px'
388 | }
389 | })
390 | // Use in a render function
391 |