├── .eslintignore ├── examples ├── react-todo-mvc │ ├── .gitignore │ ├── .babelrc │ ├── scripts │ │ └── get-css-string.js │ ├── src │ │ ├── NewTodo.css │ │ ├── index.js │ │ ├── TodoEdit.css │ │ ├── Filters.css │ │ ├── Footer.js │ │ ├── NewTodo.js │ │ ├── Footer.css │ │ ├── index.css │ │ ├── Filters.js │ │ ├── TodoItem.js │ │ ├── App.css │ │ ├── TodoItem.css │ │ └── App.js │ ├── config │ │ ├── webpack.config.base.js │ │ ├── webpack.config.prod.js │ │ └── webpack.config.dev.js │ ├── public │ │ └── index.html │ └── package.json ├── webpack-react-dom │ ├── .gitignore │ ├── .babelrc │ ├── src │ │ ├── another.css │ │ ├── index.js │ │ ├── index.css │ │ └── App.js │ ├── public │ │ └── index.html │ ├── config │ │ ├── webpack.config.base.js │ │ ├── webpack.config.prod.js │ │ └── webpack.config.dev.js │ ├── dist │ │ ├── bundle.css │ │ └── bundle.js │ └── package.json └── webpack-vanilla-dom │ ├── src │ ├── another.css │ ├── index.css │ └── index.js │ ├── public │ ├── index.html │ └── dist │ │ ├── bundle.css │ │ └── dist │ │ └── bundle.css │ ├── config │ ├── webpack.config.base.js │ ├── webpack.config.prod.js │ └── webpack.config.dev.js │ ├── dist │ └── bundle.css │ └── package.json ├── loader ├── index.js ├── bindings.js ├── postcss.config.js ├── deep-merge.js ├── readme.md ├── pitch-loader.js ├── normal-loader.js └── index.old.js ├── .travis.yml ├── assets ├── dom.png ├── react.png ├── webpack.png ├── stylesheet.png ├── dom.svg ├── webpack.svg ├── react.svg └── stylesheet.svg ├── .gitignore ├── .npmignore ├── react-dom ├── map-to-object.js ├── src │ ├── utils.js │ └── create-css-component.js ├── hot │ └── index.js ├── dist │ ├── utils.js.map │ ├── utils.js │ ├── create-css-component.js.map │ └── create-css-component.js ├── index.js ├── readme.md └── validAttributes.js ├── __tests__ ├── dom │ ├── on-dom-load.js │ └── bind-attrs-to-cssom.js ├── react-dom │ ├── map-to-object.js │ ├── utils.js │ └── index.js ├── vanilla-dom │ ├── index.js │ └── create-component.js ├── postcss │ ├── attr-to-template.js │ └── index.js ├── loader │ └── deepMerge.js └── core │ ├── template.js │ └── match-attribute.js ├── vanilla-dom ├── index.js ├── dist │ ├── event.js │ ├── create-component.js.map │ └── create-component.js ├── readme.md └── src │ └── create-component.js ├── dom ├── src │ ├── on-dom-load.js │ ├── generate-class-name.js │ └── bind-attrs-to-cssom.js └── dist │ ├── dom-load.js.map │ ├── dom-load.js │ ├── on-dom-load.js │ ├── bind-attrs-to-cssom.js.map │ ├── generate-class-name.js │ └── bind-attrs-to-cssom.js ├── core ├── template.js └── match-attribute.js ├── .eslintrc ├── postcss ├── postfix-attr-value.js ├── parse-attribute-nodes.js ├── attr-to-template.js ├── append-attr.js └── index.js ├── .vscode └── settings.json ├── package.json └── readme.md /.eslintignore: -------------------------------------------------------------------------------- 1 | */dist/* 2 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /loader/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./pitch-loader'); 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | cache: yarn 4 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | */dist/ 3 | -------------------------------------------------------------------------------- /assets/dom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iddan/stylesheet/HEAD/assets/dom.png -------------------------------------------------------------------------------- /assets/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iddan/stylesheet/HEAD/assets/react.png -------------------------------------------------------------------------------- /assets/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iddan/stylesheet/HEAD/assets/webpack.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | coverage/ 3 | _site/ 4 | _assets/ 5 | .sass-cache/ 6 | search/ 7 | -------------------------------------------------------------------------------- /assets/stylesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iddan/stylesheet/HEAD/assets/stylesheet.png -------------------------------------------------------------------------------- /examples/react-todo-mvc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "stage-2", 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "stage-2", 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /loader/bindings.js: -------------------------------------------------------------------------------- 1 | exports['vanilla-dom'] = require('../vanilla-dom'); 2 | exports['react-dom'] = require('../react-dom'); 3 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/src/another.css: -------------------------------------------------------------------------------- 1 | Label[name="The White Screen"] { 2 | background: white; 3 | color: red; 4 | } 5 | -------------------------------------------------------------------------------- /loader/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ options }) => ({ 2 | plugins: { 3 | [require.resolve('../postcss')]: options 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | _assets/ 3 | .sass-cache 4 | search/ 5 | assets/ 6 | dom/src/ 7 | examples/ 8 | react-dom/src/ 9 | vanilla-dom/src/ 10 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/src/another.css: -------------------------------------------------------------------------------- 1 | Label[name="The White Screen"] { 2 | background: white; 3 | color: red; 4 | border: 1px solid gainsboro; 5 | } 6 | -------------------------------------------------------------------------------- /react-dom/map-to-object.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | 3 | const mapToObject = _.curry((iteratee, array) => _.zipObject(array, _.map(iteratee, array))); 4 | 5 | module.exports = mapToObject; 6 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/scripts/get-css-string.js: -------------------------------------------------------------------------------- 1 | // paste in the console 2 | [...document.styleSheets] 3 | .map(stylesheet => [...stylesheet.cssRules].map(cssRule => cssRule.cssText).join('\n')) 4 | .join('\n'); 5 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/NewTodo.css: -------------------------------------------------------------------------------- 1 | @import './TodoEdit.css'; 2 | 3 | NewTodo { 4 | padding: 16px 16px 16px 60px; 5 | border: none; 6 | background: rgba(0, 0, 0, 0.003); 7 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 8 | } 9 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | -------------------------------------------------------------------------------- /__tests__/dom/on-dom-load.js: -------------------------------------------------------------------------------- 1 | import onDOMLoad from '../../dom/dist/on-dom-load'; 2 | 3 | test('Callback is triggered only after document is loaded', () => { 4 | onDOMLoad(() => { 5 | expect(document.readyState).toBe('complete'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /react-dom/src/utils.js: -------------------------------------------------------------------------------- 1 | export const omitBy = (object, filter) => { 2 | const newObj = {}; 3 | for (const key in object) { 4 | const value = object[key]; 5 | if (!filter(value, key)) { 6 | newObj[key] = value; 7 | } 8 | } 9 | return newObj; 10 | }; 11 | -------------------------------------------------------------------------------- /vanilla-dom/index.js: -------------------------------------------------------------------------------- 1 | exports.createComponentPath = require.resolve('./dist/create-component'); 2 | 3 | exports.preprocess = ({ selector, className, attributes = [], attrs = [], base }) => ({ 4 | selector, 5 | className, 6 | attributes, 7 | attrs, 8 | base, 9 | }); 10 | -------------------------------------------------------------------------------- /__tests__/react-dom/map-to-object.js: -------------------------------------------------------------------------------- 1 | import mapToObject from '../../react-dom/map-to-object'; 2 | 3 | test('Maps array items to object', () => { 4 | expect(mapToObject(item => item.toLowerCase() + '-is-cool', ['Dan', 'Ron'])).toEqual({ 5 | Dan: 'dan-is-cool', 6 | Ron: 'ron-is-cool', 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/config/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [], 4 | }, 5 | externals: { 6 | 'react': 'React', 7 | 'react-dom': 'ReactDOM', 8 | }, 9 | entry: './src/index.js', 10 | output: { 11 | filename: 'dist/bundle.js', 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /dom/src/on-dom-load.js: -------------------------------------------------------------------------------- 1 | export default function onDOMLoad(callback) { 2 | if (document.readyState === 'complete') { 3 | callback(); 4 | } else { 5 | const handleDOMLoad = () => { 6 | removeEventListener('load', handleDOMLoad); 7 | callback(); 8 | }; 9 | addEventListener('load', handleDOMLoad); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import Stylesheet from 'stylesheet/react-dom/hot'; 5 | 6 | if (module.hot) { 7 | module.hot.accept('./App', Stylesheet.handleAccept); 8 | } 9 | 10 | ReactDOM.render(React.createElement(App), document.querySelector('#root')); 11 | -------------------------------------------------------------------------------- /core/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {string} Template 3 | */ 4 | 5 | /** 6 | * @param {Template} template 7 | * @param {Object} values 8 | */ 9 | export const format = (template, values) => 10 | template.replace( 11 | /\{\s*(.+?)(?:\s*=\s*"(.+?)")?\s*\}/g, 12 | (match, name, defaultValue) => values[name] || defaultValue || '' 13 | ); 14 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/config/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.js/, 6 | use: 'babel-loader', 7 | }, 8 | ], 9 | }, 10 | externals: { 11 | 'react': 'React', 12 | 'react-dom': 'ReactDOM', 13 | }, 14 | entry: './src/index.js', 15 | output: { 16 | filename: 'dist/bundle.js', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/config/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { 5 | test: /\.js/, 6 | use: 'babel-loader', 7 | }, 8 | ], 9 | }, 10 | externals: { 11 | 'react': 'React', 12 | 'react-dom': 'ReactDOM', 13 | }, 14 | entry: './src/index.js', 15 | output: { 16 | filename: 'dist/bundle.js', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | const initialTodos = JSON.parse(localStorage.getItem('todos')); 7 | 8 | const storeTodos = todos => localStorage.setItem('todos', JSON.stringify(todos)); 9 | 10 | ReactDOM.render( 11 | , 12 | document.querySelector('#root') 13 | ); 14 | -------------------------------------------------------------------------------- /__tests__/react-dom/utils.js: -------------------------------------------------------------------------------- 1 | import { omitBy } from '../../react-dom/dist/utils'; 2 | 3 | test('Omits every name starts with D', () => { 4 | expect( 5 | omitBy( 6 | { 7 | frontEndEngineer: 'Dan', 8 | architect: 'Doron', 9 | CEO: 'Ron', 10 | CTO: 'Lior', 11 | backEndEngineer: 'Tupac', 12 | }, 13 | value => value.startsWith('D') 14 | ) 15 | ).toEqual({ CEO: 'Ron', CTO: 'Lior', backEndEngineer: 'Tupac' }); 16 | }); 17 | -------------------------------------------------------------------------------- /dom/src/generate-class-name.js: -------------------------------------------------------------------------------- 1 | import matchAttribute from '../../core/match-attribute'; 2 | 3 | const getClassName = ({ className }) => className; 4 | 5 | const generateClassName = ({ className, attributes = [], attrs = [] }) => props => 6 | [ 7 | className, 8 | ...attrs.map(getClassName), 9 | ...attributes 10 | .filter(attribute => matchAttribute(attribute, props[attribute.name])) 11 | .map(getClassName), 12 | ].join(' '); 13 | 14 | export default generateClassName; 15 | -------------------------------------------------------------------------------- /react-dom/hot/index.js: -------------------------------------------------------------------------------- 1 | const Stylesheet = { 2 | instances: [], 3 | register(instance) { 4 | this.instances.push(instance); 5 | }, 6 | unregister(instance) { 7 | this.instances.splice(this.instances.indexOf(instance), 1); 8 | }, 9 | handleAccept() { 10 | setTimeout(() => { 11 | for (const instance of this.instances) { 12 | instance.init(); 13 | instance.forceUpdate(); 14 | } 15 | }, 1); 16 | }, 17 | }; 18 | 19 | export default Stylesheet; 20 | -------------------------------------------------------------------------------- /vanilla-dom/dist/event.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, '__esModule', { 2 | value: true 3 | }); 4 | exports.default = StylesheetEvent; 5 | function StylesheetEvent(type, props) { 6 | this.props = props; 7 | } 8 | 9 | StylesheetEvent.prototype = Object.create(Event.prototype); 10 | // constructor(type, props) { 11 | // const event = new Event(type, { 12 | // bubbles: true, 13 | // cancelable: false, 14 | // scoped: false, 15 | // }); 16 | // super(type, ); 17 | // this.props = props; 18 | // } 19 | // } 20 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/TodoEdit.css: -------------------------------------------------------------------------------- 1 | NewTodo, 2 | TodoEdit { 3 | @apply input; 4 | position: relative; 5 | margin: 0; 6 | width: 100%; 7 | font-size: 24px; 8 | font-family: inherit; 9 | font-weight: inherit; 10 | line-height: 1.4em; 11 | border: 0; 12 | outline: none; 13 | color: inherit; 14 | padding: 6px; 15 | border: 1px solid #999; 16 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 17 | box-sizing: border-box; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-font-smoothing: antialiased; 20 | font-smoothing: antialiased; 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "fbjs-opensource", 4 | "plugin:lodash-fp/recommended", 5 | "plugin:promise/recommended" 6 | ], 7 | "plugins": [ 8 | "lodash-fp", 9 | "promise" 10 | ], 11 | "rules": { 12 | "object-curly-spacing": ["warn", "always", { 13 | "objectsInObjects": false 14 | }], 15 | "comma-dangle": ["off"], 16 | "template-curly-spacing": ["warn", "always"], 17 | "no-unused-vars": ["warn", { 18 | "vars": "all", 19 | "args": "after-used", 20 | "ignoreRestSiblings": false 21 | }] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | Stylesheet Todo MVC 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |

Double-click to edit a todo

12 |

Created by iddan

13 |

Part of TodoMVC

14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/dist/bundle.css: -------------------------------------------------------------------------------- 1 | Label[name="The White Screen"] { 2 | background: white; 3 | color: red; 4 | } 5 | @media screen and (max-width: 250px) {} 6 | 7 | body { 8 | margin: 0; 9 | } 10 | 11 | .Label_Bk4BUSPrb { 12 | font-family: monospace; 13 | font-size: 14px; 14 | user-select: none; 15 | color: white; 16 | background: blue; 17 | padding: 1rem; 18 | margin: 1rem; 19 | } 20 | 21 | body .Label_Bk4BUSPrb.Label-highlighted__Bk4BUSPrb { 22 | color: white; 23 | } 24 | 25 | body .Label_Bk4BUSPrb.Label-name_TjNxB_Bk4BUSPrb { 26 | color: red; 27 | } 28 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/public/dist/bundle.css: -------------------------------------------------------------------------------- 1 | Label[name="The White Screen"] { 2 | background: white; 3 | color: red; 4 | } 5 | @media screen and (max-width: 250px) {} 6 | 7 | body { 8 | margin: 0; 9 | } 10 | 11 | .Label_HkhZLBvS- { 12 | font-family: monospace; 13 | font-size: 14px; 14 | user-select: none; 15 | color: white; 16 | background: blue; 17 | padding: 1rem; 18 | margin: 1rem; 19 | } 20 | 21 | body .Label_HkhZLBvS-.Label-highlighted__HkhZLBvS- { 22 | color: white; 23 | } 24 | 25 | body .Label_HkhZLBvS-.Label-name_TjNxB_HkhZLBvS- { 26 | color: red; 27 | } 28 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylesheet-example-webpack-vanilla-dom", 3 | "version": "0.6.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "webpack-dev-server --config config/webpack.config.dev.js", 7 | "build": "webpack --config config/webpack.config.prod.js && cp -r dist public/dist" 8 | }, 9 | "license": "MIT", 10 | "devDependencies": { 11 | "extract-text-webpack-plugin": "^3.0.0", 12 | "style-loader": "^0.18.1", 13 | "stylesheet": "^0.9.1", 14 | "webpack": "^3.2.0", 15 | "webpack-dev-server": "^3.1.11" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/public/dist/dist/bundle.css: -------------------------------------------------------------------------------- 1 | Label[name="The White Screen"] { 2 | background: white; 3 | color: red; 4 | } 5 | @media screen and (max-width: 250px) {} 6 | 7 | body { 8 | margin: 0; 9 | } 10 | 11 | .Label_Bk4BUSPrb { 12 | font-family: monospace; 13 | font-size: 14px; 14 | user-select: none; 15 | color: white; 16 | background: blue; 17 | padding: 1rem; 18 | margin: 1rem; 19 | } 20 | 21 | body .Label_Bk4BUSPrb.Label-highlighted__Bk4BUSPrb { 22 | color: white; 23 | } 24 | 25 | body .Label_Bk4BUSPrb.Label-name_TjNxB_Bk4BUSPrb { 26 | color: red; 27 | } 28 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/src/index.css: -------------------------------------------------------------------------------- 1 | @import './another.css'; 2 | @media screen and (max-width: 250px) {} 3 | 4 | body { 5 | margin: 0; 6 | } 7 | 8 | Label { 9 | @apply span; 10 | font-family: monospace; 11 | font-size: 14px; 12 | user-select: none; 13 | color: white; 14 | background: blue; 15 | padding: 1rem; 16 | margin: 1rem; 17 | } 18 | 19 | body Label[highlighted] { 20 | background: linear-gradient(to top, yellow, attr(color color, tomato)); 21 | font-size: calc(attr(fontSize px) + 12px); 22 | color: white; 23 | } 24 | 25 | body Label[name="Ryskin"] { 26 | color: red; 27 | } 28 | -------------------------------------------------------------------------------- /postcss/postfix-attr-value.js: -------------------------------------------------------------------------------- 1 | module.exports = function postfixAttrValue(value, type) { 2 | switch (type) { 3 | case 'em': 4 | case 'ex': 5 | case 'px': 6 | case 'rem': 7 | case 'vw': 8 | case 'vh': 9 | case 'vmin': 10 | case 'vmax': 11 | case 'mm': 12 | case 'cm': 13 | case 'in': 14 | case 'pt': 15 | case 'pc': 16 | case 'deg': 17 | case 'grad': 18 | case 'rad': 19 | case 's': 20 | case 'ms': 21 | case 'Hz': 22 | case 'kHz': 23 | case '%': 24 | return value + type; 25 | default: 26 | return value; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/Filters.css: -------------------------------------------------------------------------------- 1 | Filters { 2 | @apply ul; 3 | margin: 0; 4 | padding: 0; 5 | list-style: none; 6 | position: absolute; 7 | right: 0; 8 | left: 0; 9 | } 10 | 11 | Filters li { 12 | display: inline; 13 | } 14 | 15 | Filters li Link { 16 | @apply a; 17 | color: inherit; 18 | margin: 3px; 19 | padding: 3px 7px; 20 | text-decoration: none; 21 | border: 1px solid transparent; 22 | border-radius: 3px; 23 | } 24 | 25 | Filters li Link[selected], 26 | Filters li Link:hover { 27 | border-color: rgba(175, 47, 47, 0.1); 28 | } 29 | 30 | Filters li Link[selected] { 31 | border-color: rgba(175, 47, 47, 0.2); 32 | } 33 | -------------------------------------------------------------------------------- /react-dom/dist/utils.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/utils.js"],"names":["omitBy","object","filter","newObj","key","value"],"mappings":";;;;;AAAO,IAAMA,0BAAS,SAATA,MAAS,CAACC,MAAD,EAASC,MAAT,EAAoB;AACxC,MAAMC,SAAS,EAAf;AACA,OAAK,IAAMC,GAAX,IAAkBH,MAAlB,EAA0B;AACxB,QAAMI,QAAQJ,OAAOG,GAAP,CAAd;AACA,QAAI,CAACF,OAAOG,KAAP,EAAcD,GAAd,CAAL,EAAyB;AACvBD,aAAOC,GAAP,IAAcC,KAAd;AACD;AACF;AACD,SAAOF,MAAP;AACD,CATM","file":"utils.js","sourcesContent":["export const omitBy = (object, filter) => {\n const newObj = {};\n for (const key in object) {\n const value = object[key];\n if (!filter(value, key)) {\n newObj[key] = value; \n }\n }\n return newObj;\n};"]} -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.bracketSpacing": true, 3 | "prettier.jsxBracketSameLine": true, 4 | "prettier.printWidth": 100, 5 | "prettier.semi": true, 6 | "prettier.singleQuote": true, 7 | "prettier.tabWidth": 2, 8 | "prettier.trailingComma": "es5", 9 | "prettier.useTabs": false, 10 | "prettier.eslintIntegration": true, 11 | "editor.formatOnSave": true, 12 | "cSpell.enabled": true, 13 | "files.exclude": { 14 | "**/node_modules/": "explorerExcludedFiles", 15 | "coverage/": "explorerExcludedFiles", 16 | "_site/": "explorerExcludedFiles", 17 | "_assets/": "explorerExcludedFiles", 18 | ".sass-cache/": "explorerExcludedFiles", 19 | "search/": "explorerExcludedFiles" 20 | } 21 | } -------------------------------------------------------------------------------- /examples/webpack-react-dom/src/index.css: -------------------------------------------------------------------------------- 1 | @import './another.css'; 2 | @media screen and (max-width: 250px) {} 3 | 4 | body { 5 | margin: 0; 6 | } 7 | 8 | Label { 9 | @apply span; 10 | font-family: monospace; 11 | font-size: 14px; 12 | user-select: none; 13 | color: blue; 14 | background: navajowhite; 15 | padding: 1rem; 16 | margin: 1rem; 17 | } 18 | 19 | body Label[highlighted] { 20 | background: linear-gradient(to top, yellow, attr(color color, tomato)); 21 | font-size: calc(attr(fontSize px) + 12px); 22 | color: white; 23 | } 24 | 25 | body Label[name="Ryskin"] { 26 | color: red; 27 | } 28 | 29 | body Label:not([name="Ryskin"]):not([name="The White Screen"]) { 30 | color: blueviolet; 31 | } 32 | -------------------------------------------------------------------------------- /loader/deep-merge.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-object-spread/prefer-object-spread */ 2 | /** 3 | * @param {Object[]} objects array of plain objects 4 | */ 5 | const deepMerge = (...objects) => 6 | objects.reduce((acc, object) => { 7 | if (!acc || typeof object !== 'object') { 8 | return object; 9 | } 10 | if (!Array.isArray(acc) && Array.isArray(object)) { 11 | return object; 12 | } 13 | if (Array.isArray(acc) && Array.isArray(object)) { 14 | return [...acc, ...object]; 15 | } 16 | return Object.keys(object).reduce( 17 | (acc2, key) => Object.assign({}, acc2, { [key]: deepMerge(acc2[key], object[key]) }), 18 | acc || {} 19 | ); 20 | }, {}); 21 | 22 | module.exports = deepMerge; 23 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylesheet-react-todo-mvc", 3 | "scripts": { 4 | "start": "webpack-dev-server --config config/webpack.config.dev.js", 5 | "build": "webpack --config config/webpack.config.prod.js && cp -r dist public/dist" 6 | }, 7 | "devDependencies": { 8 | "babel-core": "^6.24.1", 9 | "babel-loader": "^7.0.0", 10 | "babel-preset-react": "^6.24.1", 11 | "babel-preset-stage-2": "^6.24.1", 12 | "extract-text-webpack-plugin": "^3.0.0", 13 | "style-loader": "^0.18.1", 14 | "stylesheet": "^0.9.1", 15 | "webpack": "^3.2.0", 16 | "webpack-dev-server": "^2.4.5" 17 | }, 18 | "dependencies": { 19 | "react": "^15.4.2", 20 | "react-dom": "^15.4.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/dist/bundle.css: -------------------------------------------------------------------------------- 1 | Label[name="The White Screen"] { 2 | background: white; 3 | color: red; 4 | border: 1px solid gainsboro; 5 | } 6 | @media screen and (max-width: 250px) {} 7 | 8 | body { 9 | margin: 0; 10 | } 11 | 12 | .Label_ryd7NBwBZ { 13 | font-family: monospace; 14 | font-size: 14px; 15 | user-select: none; 16 | color: blue; 17 | background: navajowhite; 18 | padding: 1rem; 19 | margin: 1rem; 20 | } 21 | 22 | body .Label_ryd7NBwBZ.Label-highlighted__ryd7NBwBZ { 23 | color: white; 24 | } 25 | 26 | body .Label_ryd7NBwBZ.Label-name_TjNxB_ryd7NBwBZ { 27 | color: red; 28 | } 29 | 30 | body .Label_ryd7NBwBZ:not([name="Ryskin"]):not([name="The White Screen"]) { 31 | color: blueviolet; 32 | } 33 | -------------------------------------------------------------------------------- /dom/dist/dom-load.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/dom-load.js"],"names":["DOMLoad","Promise","document","readyState","resolve","onDOMLoad","removeEventListener","addEventListener"],"mappings":";;;;;AAAA,IAAMA,UAAU,IAAIC,OAAJ,CAAY,mBAAW;AACrC,MAAIC,SAASC,UAAT,KAAwB,UAA5B,EAAwC;AACtCC;AACD,GAFD,MAEO;AACL,QAAMC,YAAY,SAAZA,SAAY,GAAM;AACtBC,0BAAoB,MAApB,EAA4BD,SAA5B;AACAD;AACD,KAHD;AAIAG,qBAAiB,MAAjB,EAAyBF,SAAzB;AACD;AACF,CAVe,CAAhB;;kBAYeL,O","file":"dom-load.js","sourcesContent":["const DOMLoad = new Promise(resolve => {\n if (document.readyState === 'complete') {\n resolve();\n } else {\n const onDOMLoad = () => {\n removeEventListener('load', onDOMLoad);\n resolve();\n };\n addEventListener('load', onDOMLoad);\n }\n});\n\nexport default DOMLoad;\n"]} -------------------------------------------------------------------------------- /postcss/parse-attribute-nodes.js: -------------------------------------------------------------------------------- 1 | const parser = require('postcss-selector-parser'); 2 | const { unique } = require('shorthash'); 3 | 4 | const parseAttributeNodes = (id, componentName, nodes) => 5 | nodes.map(node => { 6 | const { operator, attribute, raws: { unquoted, insensitive }} = node; 7 | const attributeId = operator ? unique(operator + unquoted) : ''; 8 | const attributeClassName = `${ componentName }-${ attribute }_${ attributeId }_${ id }`; 9 | node.replaceWith(parser.className({ value: attributeClassName })); 10 | return { 11 | operator, 12 | name: attribute, 13 | value: unquoted, 14 | insensitive, 15 | className: attributeClassName, 16 | }; 17 | }); 18 | 19 | module.exports = parseAttributeNodes; 20 | -------------------------------------------------------------------------------- /__tests__/vanilla-dom/index.js: -------------------------------------------------------------------------------- 1 | import { createComponentPath, preprocess } from '../../vanilla-dom'; 2 | 3 | test('createComponentPath navigates to a component constructor', () => { 4 | const createComponent = require(createComponentPath); 5 | const component = createComponent( 6 | preprocess({ 7 | selector: '.A', 8 | className: '.A', 9 | base: 'li', 10 | }) 11 | ); 12 | expect(component).toBeInstanceOf(Function); 13 | }); 14 | 15 | test('Preprocess normalizes types', () => { 16 | expect( 17 | preprocess({ 18 | selector: '.A', 19 | className: '.A', 20 | base: 'li', 21 | }) 22 | ).toEqual({ 23 | selector: '.A', 24 | className: '.A', 25 | attributes: [], 26 | attrs: [], 27 | base: 'li', 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylesheet-webpack-react-dom-example", 3 | "scripts": { 4 | "start": "webpack-dev-server --config config/webpack.config.dev.js", 5 | "build": "webpack --config config/webpack.config.prod.js && cp -r dist public/dist" 6 | }, 7 | "devDependencies": { 8 | "babel-core": "^6.25.0", 9 | "babel-loader": "^7.1.1", 10 | "babel-preset-react": "^6.24.1", 11 | "babel-preset-stage-2": "^6.24.1", 12 | "extract-text-webpack-plugin": "^3.0.0", 13 | "style-loader": "^0.18.2", 14 | "stylesheet": "^0.9.1", 15 | "webpack": "^3.2.0", 16 | "webpack-dev-server": "^3.1.11" 17 | }, 18 | "dependencies": { 19 | "react": "^15.6.1", 20 | "react-dom": "^15.6.1" 21 | }, 22 | "version": "0.1.0" 23 | } 24 | -------------------------------------------------------------------------------- /__tests__/postcss/attr-to-template.js: -------------------------------------------------------------------------------- 1 | import attrToTemplate from '../../postcss/attr-to-template'; 2 | 3 | test('Transforms basic attr() declaration', () => { 4 | expect(attrToTemplate('attr(color color)')).toEqual( 5 | expect.objectContaining({ 6 | template: '{ color }', 7 | attributes: ['color'], 8 | }) 9 | ); 10 | }); 11 | 12 | test('Transforms complex attr() declaration', () => { 13 | expect(attrToTemplate('1px solid attr(color color)')).toEqual( 14 | expect.objectContaining({ 15 | template: '1px solid { color }', 16 | attributes: ['color'], 17 | }) 18 | ); 19 | }); 20 | 21 | test('Transforms attr() with unit', () => { 22 | expect(attrToTemplate('attr(height %)')).toEqual( 23 | expect.objectContaining({ 24 | template: '{ height }%', 25 | attributes: ['height'], 26 | }) 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | const config = require('./webpack.config.base'); 3 | 4 | const { module: { rules = [] }, plugins = [] } = config; 5 | 6 | module.exports = Object.assign(config, { 7 | module: Object.assign(config.module, { 8 | rules: [ 9 | ...rules, 10 | { 11 | test: /\.css$/, 12 | use: [ 13 | { 14 | loader: 'stylesheet/loader', 15 | query: { 16 | bindings: 'react-dom', 17 | }, 18 | }, 19 | ...ExtractTextPlugin.extract({ 20 | fallback: 'style-loader', 21 | use: ['css-loader'], 22 | }), 23 | ], 24 | }, 25 | ], 26 | }), 27 | plugins: [...plugins, new ExtractTextPlugin('dist/bundle.css')], 28 | }); 29 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | const config = require('./webpack.config.base'); 3 | 4 | const { module: { rules = [] }, plugins = [] } = config; 5 | 6 | module.exports = Object.assign(config, { 7 | module: Object.assign(config.module, { 8 | rules: [ 9 | ...rules, 10 | { 11 | test: /\.css$/, 12 | use: [ 13 | { 14 | loader: 'stylesheet/loader', 15 | query: { 16 | bindings: 'react-dom', 17 | }, 18 | }, 19 | ...ExtractTextPlugin.extract({ 20 | fallback: 'style-loader', 21 | use: ['css-loader'], 22 | }), 23 | ], 24 | }, 25 | ], 26 | }), 27 | plugins: [...plugins, new ExtractTextPlugin('dist/bundle.css')], 28 | }); 29 | -------------------------------------------------------------------------------- /__tests__/loader/deepMerge.js: -------------------------------------------------------------------------------- 1 | import deepMerge from '../../loader/deep-merge'; 2 | 3 | test('Shallowly merges two objects', () => { 4 | expect(deepMerge({ numberOfStudentsInClassroom: 1 }, { numberOfStudents: 2 })).toEqual({ 5 | numberOfStudentsInClassroom: 1, 6 | numberOfStudents: 2, 7 | }); 8 | }); 9 | 10 | test('Shallowly merges two array', () => { 11 | expect(deepMerge(['Dan'], ['Ron'])).toEqual(['Dan', 'Ron']); 12 | }); 13 | 14 | test('Deeply merges two objects', () => { 15 | expect( 16 | deepMerge({ numberOfStudents: { inClassroom: 1 }}, { numberOfStudents: { inGeneral: 2 }}) 17 | ).toEqual({ numberOfStudents: { inClassroom: 1, inGeneral: 2 }}); 18 | }); 19 | 20 | test('Merges an array within an object', () => { 21 | expect(deepMerge({ students: ['Dan'] }, { students: ['Ron'] })).toEqual({ 22 | students: ['Dan', 'Ron'], 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 2 | const config = require('./webpack.config.base'); 3 | 4 | const { module: { rules = [] }, plugins = [] } = config; 5 | 6 | module.exports = Object.assign(config, { 7 | module: Object.assign(config.module, { 8 | rules: [ 9 | ...rules, 10 | { 11 | test: /\.css$/, 12 | use: [ 13 | { 14 | loader: 'stylesheet/loader', 15 | query: { 16 | bindings: 'vanilla-dom', 17 | }, 18 | }, 19 | ...ExtractTextPlugin.extract({ 20 | fallback: 'style-loader', 21 | use: ['css-loader'], 22 | }), 23 | ], 24 | }, 25 | ], 26 | }), 27 | plugins: [...plugins, new ExtractTextPlugin('dist/bundle.css')], 28 | }); 29 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Filters from './Filters'; 4 | import { Footer as StyledFooter, TodoCount, ClearCompleted } from './Footer.css'; 5 | 6 | const Footer = ({ onFilterSelect, onClearCompleted, filter, complete, incomplete }) => ( 7 | 8 | 9 | {incomplete} item{incomplete !== 1 && 's'} left 10 | 11 | 12 | {Boolean(complete) && } 13 | 14 | ); 15 | 16 | Footer.propTypes = { 17 | onFilterSelect: PropTypes.func.isRequired, 18 | filter: PropTypes.string.isRequired, 19 | complete: PropTypes.number.isRequired, 20 | incomplete: PropTypes.number.isRequired, 21 | onClearCompleted: PropTypes.func.isRequired, 22 | }; 23 | 24 | export default Footer; 25 | -------------------------------------------------------------------------------- /postcss/attr-to-template.js: -------------------------------------------------------------------------------- 1 | const valueParser = require('postcss-value-parser'); 2 | const _ = require('lodash/fp'); 3 | const postfixAttrValue = require('./postfix-attr-value'); 4 | 5 | const isAttrFunction = _.matches({ type: 'function', value: 'attr' }); 6 | 7 | const extractWordValues = _.flow([_.filter({ type: 'word' }), _.map('value')]); 8 | 9 | const attrToTemplate = value => { 10 | const attributes = []; 11 | const template = valueParser(value) 12 | .walk(node => { 13 | if (isAttrFunction(node)) { 14 | const [name, type, defaultValue] = extractWordValues(node.nodes); 15 | const attrValue = `{ ${ name } ${ defaultValue ? `= "${ defaultValue }"` : '' }}`; 16 | node.type = 'word'; 17 | node.value = postfixAttrValue(attrValue, type); 18 | attributes.push(name); 19 | } 20 | }) 21 | .toString(); 22 | return { template, attributes }; 23 | }; 24 | 25 | module.exports = attrToTemplate; 26 | -------------------------------------------------------------------------------- /assets/dom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | HTML5 Logo Badge 12 | 13 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/NewTodo.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { NewTodo as StyledNewTodo } from './NewTodo.css'; 4 | 5 | class NewTodo extends PureComponent { 6 | static propTypes = { 7 | add: PropTypes.func.isRequired, 8 | }; 9 | 10 | state = { 11 | title: '', 12 | }; 13 | 14 | handleKeyDown = e => { 15 | switch (e.keyCode) { 16 | case 13: { 17 | const { title } = this.state; 18 | const trimmedTitle = title.trim(); 19 | if (trimmedTitle) { 20 | this.setState({ title: '' }); 21 | this.props.add({ title: trimmedTitle }); 22 | } 23 | } 24 | } 25 | }; 26 | 27 | handleChange = e => { 28 | this.setState({ title: e.target.value }); 29 | }; 30 | 31 | render() { 32 | const { title } = this.state; 33 | return ( 34 | 40 | ); 41 | } 42 | } 43 | 44 | export default NewTodo; 45 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const config = require('./webpack.config.base'); 4 | 5 | const { module: { rules = [] }, plugins = [] } = config; 6 | 7 | module.exports = Object.assign(config, { 8 | module: Object.assign(config.module, { 9 | rules: [ 10 | ...rules, 11 | { 12 | test: /\.css/, 13 | use: [ 14 | { 15 | loader: 'stylesheet/loader', 16 | query: { 17 | sourceMap: true, 18 | bindings: 'react-dom', 19 | }, 20 | }, 21 | 'style-loader', 22 | 'css-loader', 23 | ], 24 | }, 25 | ], 26 | }), 27 | devtool: 'sourcemaps', 28 | devServer: { 29 | contentBase: path.resolve(__dirname, '../public'), 30 | port: 8080, 31 | hotOnly: true, 32 | historyApiFallback: true, 33 | inline: true, 34 | }, 35 | plugins: [ 36 | ...plugins, 37 | new webpack.HotModuleReplacementPlugin(), 38 | new webpack.NamedModulesPlugin(), 39 | new webpack.NoEmitOnErrorsPlugin(), 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const config = require('./webpack.config.base'); 4 | 5 | const { module: { rules = [] }, plugins = [] } = config; 6 | 7 | module.exports = Object.assign(config, { 8 | module: Object.assign(config.module, { 9 | rules: [ 10 | ...rules, 11 | { 12 | test: /\.css/, 13 | use: [ 14 | { 15 | loader: 'stylesheet/loader', 16 | query: { 17 | sourceMap: true, 18 | bindings: 'react-dom', 19 | }, 20 | }, 21 | 'style-loader', 22 | 'css-loader', 23 | ], 24 | }, 25 | ], 26 | }), 27 | devtool: 'sourcemaps', 28 | devServer: { 29 | contentBase: path.resolve(__dirname, '../public'), 30 | port: 8080, 31 | hotOnly: true, 32 | historyApiFallback: true, 33 | inline: true, 34 | }, 35 | plugins: [ 36 | ...plugins, 37 | new webpack.HotModuleReplacementPlugin(), 38 | new webpack.NamedModulesPlugin(), 39 | new webpack.NoEmitOnErrorsPlugin(), 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/src/index.js: -------------------------------------------------------------------------------- 1 | import { Label } from './index.css'; 2 | 3 | const randomInt = max => (Math.random() * max).toFixed(0); 4 | 5 | const randomColor = () => `rgb(${ [randomInt(255), randomInt(255), randomInt(255)].join() })`; 6 | 7 | const App = () => { 8 | const container = document.createElement('div'); 9 | container.setAttribute('role', 'container'); 10 | 11 | const ryskin = Label.create({ 12 | fontSize: Math.random() * 100, 13 | highlighted: true, 14 | name: 'Ryskin', 15 | color: randomColor(), 16 | }); 17 | 18 | ryskin.appendChild(document.createTextNode('Ryskinder, please click me!')); 19 | 20 | ryskin.addEventListener('click', () => { 21 | ryskin.fontSize = Math.random() * 100; 22 | ryskin.color = randomColor(); 23 | }); 24 | 25 | const theWhiteScreen = Label.create({ 26 | name: 'The White Screen', 27 | }); 28 | 29 | theWhiteScreen.appendChild(document.createTextNode('The White Screen')); 30 | 31 | container.appendChild(ryskin); 32 | container.appendChild(theWhiteScreen); 33 | 34 | return container; 35 | }; 36 | 37 | document.querySelector('#root').appendChild(App()); 38 | -------------------------------------------------------------------------------- /examples/webpack-vanilla-dom/config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const config = require('./webpack.config.base'); 4 | 5 | const { module: { rules = [] }, plugins = [] } = config; 6 | 7 | module.exports = Object.assign(config, { 8 | module: Object.assign(config.module, { 9 | rules: [ 10 | ...rules, 11 | { 12 | test: /\.css/, 13 | use: [ 14 | { 15 | loader: 'stylesheet/loader', 16 | query: { 17 | sourceMap: true, 18 | bindings: 'vanilla-dom', 19 | }, 20 | }, 21 | 'style-loader', 22 | 'css-loader', 23 | ], 24 | }, 25 | ], 26 | }), 27 | devtool: 'sourcemaps', 28 | devServer: { 29 | contentBase: path.resolve(__dirname, '../public'), 30 | port: 8080, 31 | hotOnly: true, 32 | historyApiFallback: true, 33 | inline: true, 34 | }, 35 | plugins: [ 36 | ...plugins, 37 | new webpack.HotModuleReplacementPlugin(), 38 | new webpack.NamedModulesPlugin(), 39 | new webpack.NoEmitOnErrorsPlugin(), 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/Footer.css: -------------------------------------------------------------------------------- 1 | 2 | Footer { 3 | @apply footer; 4 | color: #777; 5 | padding: 10px 15px; 6 | height: 20px; 7 | text-align: center; 8 | border-top: 1px solid #e6e6e6; 9 | } 10 | 11 | Footer::before { 12 | content: ''; 13 | position: absolute; 14 | right: 0; 15 | bottom: 0; 16 | left: 0; 17 | height: 50px; 18 | overflow: hidden; 19 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 20 | 0 8px 0 -3px #f6f6f6, 21 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 22 | 0 16px 0 -6px #f6f6f6, 23 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 24 | } 25 | 26 | TodoCount { 27 | @apply span; 28 | float: left; 29 | text-align: left; 30 | } 31 | 32 | TodoCount strong { 33 | font-weight: 300; 34 | } 35 | 36 | ClearCompleted { 37 | @apply button; 38 | } 39 | 40 | ClearCompleted, 41 | html ClearCompleted:active { 42 | float: right; 43 | position: relative; 44 | line-height: 20px; 45 | text-decoration: none; 46 | cursor: pointer; 47 | position: relative; 48 | } 49 | 50 | ClearCompleted:hover { 51 | text-decoration: underline; 52 | } 53 | 54 | ClearCompleted::after { 55 | content: 'Clear completed'; 56 | } 57 | -------------------------------------------------------------------------------- /assets/webpack.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /examples/webpack-react-dom/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { Label } from './index.css'; 3 | 4 | class Counter extends PureComponent { 5 | state = { count: 0 }; 6 | 7 | handleClick = () => { 8 | this.setState(state => ({ ...state, count: state.count + 1 })); 9 | }; 10 | 11 | render() { 12 | return ( 13 |
14 | {this.state.count} 15 |
16 | ); 17 | } 18 | } 19 | 20 | class App extends PureComponent { 21 | state = {}; 22 | 23 | handleClick = () => { 24 | this.setState({ 25 | color: `rgb(${ (Math.random() * 255).toFixed(0) }, ${ (Math.random() * 255).toFixed(0) }, ${ (Math.random() * 26 | 255).toFixed(0) })`, 27 | fontSize: Math.random() * 100, 28 | }); 29 | }; 30 | 31 | render() { 32 | return ( 33 |
34 | 37 | 38 | 39 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /__tests__/dom/bind-attrs-to-cssom.js: -------------------------------------------------------------------------------- 1 | import onDOMLoad from '../../dom/dist/on-dom-load'; 2 | import bindAttrsToCSSOM from '../../dom/dist/bind-attrs-to-cssom'; 3 | 4 | const style = Object.assign(document.createElement('style'), { 5 | type: 'text/css', 6 | innerHTML: ` 7 | .A .B { 8 | color: attr(color color); 9 | } 10 | `, 11 | }); 12 | 13 | document.head.appendChild(style); 14 | 15 | const boundAttrs = bindAttrsToCSSOM([ 16 | { 17 | prop: 'color', 18 | selector: '.A .B', 19 | template: '{ color }', 20 | attributes: ['color'], 21 | }, 22 | ]); 23 | 24 | const [{ cssRule: boundCSSRule }] = boundAttrs; 25 | 26 | test('Binds attrs to CSSOM rules', () => { 27 | expect(boundAttrs).toEqual([ 28 | expect.objectContaining({ 29 | prop: 'color', 30 | selector: '.A .B', 31 | template: '{ color }', 32 | attributes: ['color'], 33 | className: expect.any(String), 34 | cssRule: expect.objectContaining({ 35 | selectorText: expect.stringContaining('.A .B'), 36 | }), 37 | }), 38 | ]); 39 | }); 40 | 41 | test('Replaces the CSSOM rules after DOM load', () => { 42 | onDOMLoad(() => { 43 | setTimeout(() => { 44 | expect(boundAttrs[0].cssRule).not.toBe(boundCSSRule); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /react-dom/dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | var omitBy = exports.omitBy = function omitBy(object, filter) { 7 | var newObj = {}; 8 | for (var key in object) { 9 | var value = object[key]; 10 | if (!filter(value, key)) { 11 | newObj[key] = value; 12 | } 13 | } 14 | return newObj; 15 | }; 16 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy91dGlscy5qcyJdLCJuYW1lcyI6WyJvbWl0QnkiLCJvYmplY3QiLCJmaWx0ZXIiLCJuZXdPYmoiLCJrZXkiLCJ2YWx1ZSJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBTyxJQUFNQSwwQkFBUyxTQUFUQSxNQUFTLENBQUNDLE1BQUQsRUFBU0MsTUFBVCxFQUFvQjtBQUN4QyxNQUFNQyxTQUFTLEVBQWY7QUFDQSxPQUFLLElBQU1DLEdBQVgsSUFBa0JILE1BQWxCLEVBQTBCO0FBQ3hCLFFBQU1JLFFBQVFKLE9BQU9HLEdBQVAsQ0FBZDtBQUNBLFFBQUksQ0FBQ0YsT0FBT0csS0FBUCxFQUFjRCxHQUFkLENBQUwsRUFBeUI7QUFDdkJELGFBQU9DLEdBQVAsSUFBY0MsS0FBZDtBQUNEO0FBQ0Y7QUFDRCxTQUFPRixNQUFQO0FBQ0QsQ0FUTSIsImZpbGUiOiJ1dGlscy5qcyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBjb25zdCBvbWl0QnkgPSAob2JqZWN0LCBmaWx0ZXIpID0+IHtcbiAgY29uc3QgbmV3T2JqID0ge307XG4gIGZvciAoY29uc3Qga2V5IGluIG9iamVjdCkge1xuICAgIGNvbnN0IHZhbHVlID0gb2JqZWN0W2tleV07XG4gICAgaWYgKCFmaWx0ZXIodmFsdWUsIGtleSkpIHtcbiAgICAgIG5ld09ialtrZXldID0gdmFsdWU7XG4gICAgfVxuICB9XG4gIHJldHVybiBuZXdPYmo7XG59O1xuIl19 -------------------------------------------------------------------------------- /postcss/append-attr.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | const attrToTemplate = require('./attr-to-template'); 3 | 4 | /** 5 | * @typedef {Object} Attr Components' CSS declarations which contain the attr() function representation 6 | * @property {string} prop The declaration's CSS property 7 | * @property {string} selector The CSS selector of the declaration's rule 8 | * @property {Template} template The declaration with attr() replaced to template placeholders 9 | * @property {string} attributes Attributes referenced in the declaration with the attr() function 10 | */ 11 | 12 | /** 13 | * @param {Object} components 14 | * @param {string} component 15 | * @param {string} rule 16 | * @param {Declaration} decl 17 | * @returns {Attr[]} 18 | */ 19 | module.exports = function appendAttr(components, component, rule, decl) { 20 | const { prop, value } = decl; 21 | decl.remove(); 22 | return _.update( 23 | [component, 'attrs'], 24 | (attrs = []) => { 25 | const { template, attributes } = attrToTemplate(value); 26 | return [ 27 | ...attrs, 28 | { 29 | prop: _.camelCase(prop), 30 | selector: rule.selector, 31 | template, 32 | attributes, 33 | }, 34 | ]; 35 | }, 36 | components 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /loader/readme.md: -------------------------------------------------------------------------------- 1 |
2 | Stylesheet Logo 3 | Stylesheet Logo 4 |

Stylesheet Loader

5 |

Webpack loader for Stylesheet

6 |
7 | 8 |

Usage

9 | 10 | ```bash 11 | npm install --save stylesheet 12 | ``` 13 | 14 | Stylesheet uses Webpack to go through your CSS modules and export the components. The loader extends Webpack's [CSS Loader] so you benefit the community standard for parsing CSS while getting extra functionality. 15 | 16 | ```JavaScript 17 | module.exports = { 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.css$/, 22 | exclude: /node_modules/, 23 | use: [ 24 | 'style-loader', 25 | { 26 | loader: 'stylesheet/loader' 27 | query: { 28 | bindings: BINDINGS 29 | } 30 | } 31 | ] 32 | } 33 | // the rest of your webpack config 34 | ``` 35 | [CSS Loader]: (https://github.com/webpack-contrib/css-loader) 36 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .info { 44 | margin: 65px auto 0; 45 | color: #bfbfbf; 46 | font-size: 10px; 47 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 48 | text-align: center; 49 | } 50 | 51 | .info p { 52 | line-height: 1; 53 | } 54 | 55 | .info a { 56 | color: inherit; 57 | text-decoration: none; 58 | font-weight: 400; 59 | } 60 | 61 | .info a:hover { 62 | text-decoration: underline; 63 | } 64 | -------------------------------------------------------------------------------- /core/match-attribute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Object} attribute A CSS attribute selector representation 3 | * @param {string} attribute.operator 4 | * @param {string} attribute.value 5 | * @param {boolean} attribute.insensitive 6 | * @param {string} value The element's attribute value 7 | */ 8 | export default function matchAttribute(attribute, value) { 9 | const { insensitive } = attribute; 10 | const attributeValue = insensitive ? attribute.value.toLowerCase() : attribute.value; 11 | const elementValue = insensitive ? value.toLowerCase() : value; 12 | switch (attribute.operator) { 13 | case '=': { 14 | return elementValue === attributeValue; 15 | } 16 | case '~=': { 17 | return whitespaceList(elementValue).includes(attributeValue); 18 | } 19 | case '|=': { 20 | return beforeDash(elementValue) === attributeValue; 21 | } 22 | case '^=': { 23 | return elementValue.startsWith(attributeValue); 24 | } 25 | case '$=': { 26 | return elementValue.endsWith(attributeValue); 27 | } 28 | case '*=': { 29 | return elementValue.includes(attributeValue); 30 | } 31 | default: { 32 | return Boolean(elementValue); 33 | } 34 | } 35 | } 36 | 37 | const beforeDash = string => string.split('-')[0]; 38 | 39 | const whitespaceList = string => string.split(/\s+/); 40 | -------------------------------------------------------------------------------- /react-dom/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash/fp'); 2 | const validAttributes = require('./validAttributes'); 3 | const mapToObject = require('./map-to-object'); 4 | 5 | exports.createComponentPath = require.resolve('./dist/create-css-component'); 6 | 7 | /** 8 | * @typedef {Object} PreprocessOptions 9 | * @property {Object} options 10 | * @property {string} options.selector 11 | * @property {string} options.className 12 | * @property {Attribute[]} options.attributes 13 | * @property {Attr[]} options.attrs 14 | * @property {string} base 15 | */ 16 | 17 | /** 18 | * @typedef {PreprocessOptions} CreateComponentOptions 19 | * @property {Object} invalidProps 20 | */ 21 | 22 | /** 23 | * @param {PreprocessOptions} options 24 | * @return {CreateComponentOptions} 25 | */ 26 | exports.preprocess = ({ selector, className, attributes = [], attrs = [], base }) => ({ 27 | selector, 28 | className, 29 | attributes, 30 | attrs, 31 | base, 32 | invalidProps: flagInvalidProps(attributes, attrs), 33 | }); 34 | 35 | /** 36 | * @param {Attributes[]} attributes 37 | * @param {Attr[]} attrs 38 | * @returns {Object} 39 | */ 40 | const flagInvalidProps = _.flow([ 41 | (attributes, attrs) => _.flattenDeep([_.map('name', attributes), _.map('attributes', attrs)]), 42 | mapToObject(prop => !validAttributes(prop)), 43 | ]); 44 | -------------------------------------------------------------------------------- /__tests__/core/template.js: -------------------------------------------------------------------------------- 1 | import * as template from '../../core/template'; 2 | 3 | test('Format inserts values', () => { 4 | expect( 5 | template.format('Hello world! My name is { name } and I like to { hobby }.', { 6 | name: 'Christoph', 7 | hobby: 'code', 8 | }) 9 | ).toEqual('Hello world! My name is Christoph and I like to code.'); 10 | }); 11 | 12 | test('Format inserts default values', () => { 13 | expect( 14 | template.format( 15 | 'Hello world! My name is { name = "Christoph" } and I like to { hobby = "code" }.', 16 | {} 17 | ) 18 | ).toEqual('Hello world! My name is Christoph and I like to code.'); 19 | }); 20 | 21 | test("Format doesn't inserts default values when values are defined", () => { 22 | expect( 23 | template.format( 24 | 'Hello world! My name is { name = "Christoph" } and I like to { hobby = "code" }.', 25 | { 26 | name: 'Steve', 27 | hobby: 'design', 28 | } 29 | ) 30 | ).toEqual('Hello world! My name is Steve and I like to design.'); 31 | }); 32 | 33 | test('Format works without spaces', () => { 34 | expect( 35 | template.format('Hello world! My name is {name} and I like to {hobby="code"}.', { 36 | name: 'Christoph', 37 | }), 38 | {} 39 | ).toEqual('Hello world! My name is Christoph and I like to code.'); 40 | }); 41 | -------------------------------------------------------------------------------- /loader/pitch-loader.js: -------------------------------------------------------------------------------- 1 | const { stringifyRequest, getOptions } = require('loader-utils'); 2 | const shortid = require('shortid'); 3 | const _ = require('lodash/fp'); 4 | 5 | const id = shortid.generate(); 6 | 7 | const NORMAL_LOADER_PATH = require.resolve('./normal-loader.js'); 8 | 9 | const isCSSLoader = ({ path }) => path.includes('node_modules/css-loader/index.js'); 10 | 11 | module.exports.pitch = function() { 12 | const { loaders } = this; 13 | const loadersAfterCSSLoader = loaders.slice(this.loaderIndex + 1, loaders.findIndex(isCSSLoader)); 14 | const loadersBeforeCSSLoader = loaders.slice(loaders.findIndex(isCSSLoader) + 1, loaders.length); 15 | 16 | const staticCSSPath = [ 17 | '!!', 18 | ..._.map('request', loadersAfterCSSLoader), 19 | loaders.find(isCSSLoader).request + '?' + JSON.stringify({ importLoaders: 1 }), 20 | require.resolve('postcss-loader') + 21 | '?' + 22 | JSON.stringify({ config: { path: require.resolve('./postcss.config'), ctx: { id }}}), 23 | this.resourcePath, 24 | ].join('!'); 25 | 26 | const componentsPath = [ 27 | '!' + NORMAL_LOADER_PATH + '?' + JSON.stringify(Object.assign({ id }, getOptions(this))), 28 | ..._.map('request', loadersBeforeCSSLoader), 29 | this.resourcePath, 30 | ].join('!'); 31 | 32 | return ` 33 | export * from ${ stringifyRequest(this, componentsPath) }; 34 | import ${ stringifyRequest(this, staticCSSPath) }; 35 | `; 36 | }; 37 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/Filters.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Filters as StyledFilters, Link } from './Filters.css'; 4 | 5 | const Filters = ({ onSelect, selected }) => ( 6 | 7 | 8 | 9 | 15 | 16 | ); 17 | 18 | Filters.propTypes = { 19 | onSelect: PropTypes.func.isRequired, 20 | selected: PropTypes.string.isRequired, 21 | }; 22 | 23 | class Filter extends PureComponent { 24 | static propTypes = { 25 | onSelect: PropTypes.func.isRequired, 26 | label: PropTypes.string.isRequired, 27 | value: PropTypes.string.isRequired, 28 | selected: PropTypes.bool.isRequired, 29 | }; 30 | 31 | handleSelect = () => { 32 | const { onSelect, value } = this.props; 33 | onSelect(value); 34 | }; 35 | 36 | render() { 37 | const { label, value, selected } = this.props; 38 | return ( 39 |
  • 40 | {label} 41 |
  • 42 | ); 43 | } 44 | } 45 | 46 | export default Filters; 47 | -------------------------------------------------------------------------------- /dom/dist/dom-load.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = onDOMLoad; 7 | function onDOMLoad(callback) { 8 | if (document.readyState === 'complete') { 9 | callback(); 10 | } else { 11 | var handleDOMLoad = function handleDOMLoad() { 12 | removeEventListener('load', handleDOMLoad); 13 | callback(); 14 | }; 15 | addEventListener('load', handleDOMLoad); 16 | } 17 | } 18 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9kb20tbG9hZC5qcyJdLCJuYW1lcyI6WyJvbkRPTUxvYWQiLCJjYWxsYmFjayIsImRvY3VtZW50IiwicmVhZHlTdGF0ZSIsImhhbmRsZURPTUxvYWQiLCJyZW1vdmVFdmVudExpc3RlbmVyIiwiYWRkRXZlbnRMaXN0ZW5lciJdLCJtYXBwaW5ncyI6Ijs7Ozs7a0JBQXdCQSxTO0FBQVQsU0FBU0EsU0FBVCxDQUFtQkMsUUFBbkIsRUFBNkI7QUFDMUMsTUFBSUMsU0FBU0MsVUFBVCxLQUF3QixVQUE1QixFQUF3QztBQUN0Q0Y7QUFDRCxHQUZELE1BRU87QUFDTCxRQUFNRyxnQkFBZ0IsU0FBaEJBLGFBQWdCLEdBQU07QUFDMUJDLDBCQUFvQixNQUFwQixFQUE0QkQsYUFBNUI7QUFDQUg7QUFDRCxLQUhEO0FBSUFLLHFCQUFpQixNQUFqQixFQUF5QkYsYUFBekI7QUFDRDtBQUNGIiwiZmlsZSI6ImRvbS1sb2FkLmpzIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gb25ET01Mb2FkKGNhbGxiYWNrKSB7XG4gIGlmIChkb2N1bWVudC5yZWFkeVN0YXRlID09PSAnY29tcGxldGUnKSB7XG4gICAgY2FsbGJhY2soKTtcbiAgfSBlbHNlIHtcbiAgICBjb25zdCBoYW5kbGVET01Mb2FkID0gKCkgPT4ge1xuICAgICAgcmVtb3ZlRXZlbnRMaXN0ZW5lcignbG9hZCcsIGhhbmRsZURPTUxvYWQpO1xuICAgICAgY2FsbGJhY2soKTtcbiAgICB9O1xuICAgIGFkZEV2ZW50TGlzdGVuZXIoJ2xvYWQnLCBoYW5kbGVET01Mb2FkKTtcbiAgfVxufVxuIl19 -------------------------------------------------------------------------------- /dom/dist/on-dom-load.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = onDOMLoad; 7 | function onDOMLoad(callback) { 8 | if (document.readyState === 'complete') { 9 | callback(); 10 | } else { 11 | var handleDOMLoad = function handleDOMLoad() { 12 | removeEventListener('load', handleDOMLoad); 13 | callback(); 14 | }; 15 | addEventListener('load', handleDOMLoad); 16 | } 17 | } 18 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9vbi1kb20tbG9hZC5qcyJdLCJuYW1lcyI6WyJvbkRPTUxvYWQiLCJjYWxsYmFjayIsImRvY3VtZW50IiwicmVhZHlTdGF0ZSIsImhhbmRsZURPTUxvYWQiLCJyZW1vdmVFdmVudExpc3RlbmVyIiwiYWRkRXZlbnRMaXN0ZW5lciJdLCJtYXBwaW5ncyI6Ijs7Ozs7a0JBQXdCQSxTO0FBQVQsU0FBU0EsU0FBVCxDQUFtQkMsUUFBbkIsRUFBNkI7QUFDMUMsTUFBSUMsU0FBU0MsVUFBVCxLQUF3QixVQUE1QixFQUF3QztBQUN0Q0Y7QUFDRCxHQUZELE1BRU87QUFDTCxRQUFNRyxnQkFBZ0IsU0FBaEJBLGFBQWdCLEdBQU07QUFDMUJDLDBCQUFvQixNQUFwQixFQUE0QkQsYUFBNUI7QUFDQUg7QUFDRCxLQUhEO0FBSUFLLHFCQUFpQixNQUFqQixFQUF5QkYsYUFBekI7QUFDRDtBQUNGIiwiZmlsZSI6Im9uLWRvbS1sb2FkLmpzIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gb25ET01Mb2FkKGNhbGxiYWNrKSB7XG4gIGlmIChkb2N1bWVudC5yZWFkeVN0YXRlID09PSAnY29tcGxldGUnKSB7XG4gICAgY2FsbGJhY2soKTtcbiAgfSBlbHNlIHtcbiAgICBjb25zdCBoYW5kbGVET01Mb2FkID0gKCkgPT4ge1xuICAgICAgcmVtb3ZlRXZlbnRMaXN0ZW5lcignbG9hZCcsIGhhbmRsZURPTUxvYWQpO1xuICAgICAgY2FsbGJhY2soKTtcbiAgICB9O1xuICAgIGFkZEV2ZW50TGlzdGVuZXIoJ2xvYWQnLCBoYW5kbGVET01Mb2FkKTtcbiAgfVxufVxuIl19 -------------------------------------------------------------------------------- /__tests__/core/match-attribute.js: -------------------------------------------------------------------------------- 1 | import matchAttribute from '../../core/match-attribute'; 2 | 3 | test('[attribute]', () => { 4 | expect(matchAttribute({ name: 'highlighted' }, true)).toBeTruthy(); 5 | }); 6 | 7 | test('[attribute=true]', () => { 8 | expect(matchAttribute({ name: 'highlighted', operator: '=', value: true }, true)).toBeTruthy(); 9 | }); 10 | 11 | test('[attribute=value]', () => { 12 | expect(matchAttribute({ name: 'name', operator: '=', value: 'Dan' }, 'Dan')).toBeTruthy(); 13 | }); 14 | 15 | test('[attribute~=value]', () => { 16 | expect(matchAttribute({ name: 'name', operator: '~=', value: 'Dan' }, 'Dan Ron')).toBeTruthy(); 17 | }); 18 | 19 | test('[attribute|=value]', () => { 20 | expect(matchAttribute({ name: 'name', operator: '|=', value: 'Dan' }, 'Dan')).toBeTruthy(); 21 | }); 22 | 23 | test('[attribute|=value-other]', () => { 24 | expect(matchAttribute({ name: 'name', operator: '|=', value: 'Dan' }, 'Dan-Smith')).toBeTruthy(); 25 | }); 26 | 27 | test('[attribute^=value]', () => { 28 | expect(matchAttribute({ name: 'name', operator: '^=', value: 'Dan' }, 'DanSmith')).toBeTruthy(); 29 | }); 30 | 31 | test('[attribute$=value]', () => { 32 | expect(matchAttribute({ name: 'name', operator: '$=', value: 'Smith' }, 'DanSmith')).toBeTruthy(); 33 | }); 34 | 35 | test('[attribute*=value]', () => { 36 | expect(matchAttribute({ name: 'name', operator: '*=', value: 'Dan' }, 'SrDanSmith')).toBeTruthy(); 37 | }); 38 | 39 | test('[attribute=value i]', () => { 40 | expect( 41 | matchAttribute({ name: 'name', operator: '=', value: 'dan', insensitive: true }, 'Dan') 42 | ).toBeTruthy(); 43 | }); 44 | -------------------------------------------------------------------------------- /vanilla-dom/readme.md: -------------------------------------------------------------------------------- 1 |
    2 | Stylesheet Logo 3 | Stylesheet Logo 4 |

    Stylesheet Vanilla DOM

    5 |

    Dynamic CSS for the Web

    6 |
    7 | 8 |

    Usage

    9 | 10 | ```bash 11 | npm install --save stylesheet 12 | ``` 13 | 14 | Stylesheet Vanilla DOM allows you to create dynamically styled DOM elements using pure standard CSS. Wrap your dynamic CSS properties with the experminatal attr() function and Stylesheet will automatically update and render them with your data. 15 | 16 | *stylesheet.css* 17 | ```CSS 18 | Title { 19 | font-size: 4em; 20 | color: attr(textColor color); 21 | } 22 | ``` 23 | 24 | #### DOM 25 | 26 | ```JSX 27 | import { Title } from './stylesheet.css'; 28 | 29 | const title = Title.create({ 30 | textColor: 'white' 31 | }); 32 | 33 | title.appendChild(document.createTextNode('My dynamically styled app')); 34 | 35 | documnet.body.appendChild(title); 36 | 37 | title.textColor = 'blue'; 38 | ``` 39 | 40 | #### Webpack 41 | 42 | ```JavaScript 43 | module.exports = { 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.css$/, 48 | exclude: /node_modules/, 49 | use: [ 50 | 'style-loader', 51 | { 52 | loader: 'stylesheet/loader' 53 | query: { 54 | bindings: 'vanilla-dom' 55 | } 56 | } 57 | ] 58 | } 59 | // the rest of your webpack config 60 | ``` 61 | -------------------------------------------------------------------------------- /react-dom/readme.md: -------------------------------------------------------------------------------- 1 |
    2 | Stylesheet Logo 3 | React Logo 4 |

    Stylesheet ReactDOM

    5 |

    Dynamic CSS for React on the Web

    6 |
    7 | 8 |

    Usage

    9 | 10 | ```bash 11 | npm install --save stylesheet 12 | ``` 13 | 14 | Stylesheet ReactDOM allows you to create dynamically styled React components using pure standard CSS. Wrap your dynamic CSS properties with the experminatal attr() function and Stylesheet will automatically update and render them with your data. 15 | 16 | *stylesheet.css* 17 | ```CSS 18 | Title { 19 | font-size: 4em; 20 | color: attr(textColor color); 21 | } 22 | ``` 23 | 24 | #### React (Web) 25 | 26 | ```JSX 27 | import React, { Component } from 'react'; 28 | import { Title } from './stylesheet.css'; 29 | 30 | class Header extends Component { 31 | render() { 32 | return My dynamically styled app; 33 | } 34 | } 35 | ``` 36 | 37 | #### Webpack 38 | 39 | ```JavaScript 40 | module.exports = { 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.css$/, 45 | exclude: /node_modules/, 46 | use: [ 47 | 'style-loader', 48 | { 49 | loader: 'stylesheet/loader' 50 | query: { 51 | bindings: 'react-dom' 52 | } 53 | } 54 | ] 55 | } 56 | // the rest of your webpack config 57 | ``` 58 | 59 |

    Prior Art and Comparison

    60 | 61 | #### Styled Components 62 | A library for composing components from tagged template literals of CSS code. 63 | 64 | - Does not use standard CSS for dynamic properties. 65 | - Does not use external CSS. 66 | - Compiles at runtime 67 | - Requires 70KB of *minified* code for full usage 68 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { TodoItem as StyledTodoItem, TodoView, TodoEdit, Toggle, Destroy } from './TodoItem.css'; 4 | 5 | class TodoItem extends PureComponent { 6 | static propTypes = { 7 | id: PropTypes.string.isRequired, 8 | title: PropTypes.string.isRequired, 9 | completed: PropTypes.bool.isRequired, 10 | onChange: PropTypes.func.isRequired, 11 | onDestroy: PropTypes.func.isRequired, 12 | }; 13 | 14 | state = { 15 | editing: false, 16 | }; 17 | 18 | setInput = input => { 19 | this.input = input; 20 | }; 21 | 22 | focusInput = () => { 23 | this.input.focus(); 24 | }; 25 | 26 | startEdit = () => { 27 | this.setState({ editing: true }, this.focusInput); 28 | }; 29 | 30 | endEdit = () => { 31 | this.setState({ editing: false }); 32 | }; 33 | 34 | changeTitle = e => { 35 | const { onChange, id } = this.props; 36 | onChange({ id, title: e.target.value }); 37 | }; 38 | 39 | changeComplete = e => { 40 | const { onChange, id } = this.props; 41 | onChange({ id, completed: e.target.checked }); 42 | }; 43 | 44 | destroy = () => { 45 | const { onDestroy, id } = this.props; 46 | onDestroy({ id }); 47 | }; 48 | 49 | render() { 50 | const { title, completed } = this.props; 51 | const { editing } = this.state; 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 65 | 66 | ); 67 | } 68 | } 69 | 70 | export default TodoItem; 71 | -------------------------------------------------------------------------------- /loader/normal-loader.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { stringifyRequest, getOptions } = require('loader-utils'); 3 | const postcss = require('postcss'); 4 | const atImport = require('postcss-import'); 5 | const shortid = require('shortid'); 6 | const _ = require('lodash/fp'); 7 | const stylesheetPostcssPlugin = require('../postcss'); 8 | const bindings = require('./bindings'); 9 | 10 | const id = shortid.generate(); 11 | 12 | const resolvePromise = (loader, request) => 13 | new Promise((resolve, reject) => { 14 | loader.resolve(loader.context, request, (err, result) => { 15 | if (err) { 16 | reject(err); 17 | } 18 | resolve(result); 19 | }); 20 | }); 21 | 22 | module.exports = function(content) { 23 | let components; 24 | const callback = this.async(); 25 | const options = getOptions(this); 26 | 27 | assert( 28 | bindings[options.bindings], 29 | `Bindings must be provided and be one of the following: ${ Object.keys(bindings).join() }`, 30 | ); 31 | const { preprocess, createComponentPath } = bindings[options.bindings]; 32 | 33 | postcss([ 34 | atImport({ 35 | resolve: request => resolvePromise(this, request), 36 | }), 37 | stylesheetPostcssPlugin({ 38 | id: options.id || id, 39 | onComponents(receivedComponents) { 40 | components = receivedComponents; 41 | }, 42 | }), 43 | ]) 44 | .process(content) 45 | .then(result => { 46 | console.log(result.css); 47 | callback( 48 | null, 49 | ` 50 | var data = ${ JSON.stringify(_.mapValues(preprocess, components)) }; 51 | var createComponent = require(${ stringifyRequest(this, createComponentPath) }); 52 | ${ Object.keys(components) 53 | .map( 54 | component => ` 55 | export var ${ component } = createComponent(Object.assign({}, data.${ component }, { 56 | displayName: ${ JSON.stringify(component) } 57 | }));`, 58 | ) 59 | .join('\n') } 60 | `, 61 | ); 62 | }) 63 | .catch(callback); 64 | }; 65 | -------------------------------------------------------------------------------- /__tests__/react-dom/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import { createComponentPath, preprocess } from '../../react-dom'; 3 | 4 | test('createComponentPath navigates to a component constructor', () => { 5 | const createComponent = require(createComponentPath); 6 | const component = createComponent( 7 | preprocess({ 8 | selector: '.A', 9 | className: '.A', 10 | base: 'li', 11 | }) 12 | ); 13 | expect(new component()).toBeInstanceOf(Component); 14 | }); 15 | 16 | test('Preprocess normalizes types', () => { 17 | expect( 18 | preprocess({ 19 | selector: '.A', 20 | className: '.A', 21 | base: 'li', 22 | }) 23 | ).toEqual({ 24 | selector: '.A', 25 | className: '.A', 26 | attributes: [], 27 | attrs: [], 28 | base: 'li', 29 | invalidProps: {}, 30 | }); 31 | }); 32 | 33 | test('Preprocess defines invalidProps', () => { 34 | expect( 35 | preprocess({ 36 | selector: '.A', 37 | className: '.A', 38 | base: 'li', 39 | attributes: [ 40 | { 41 | name: 'highlighted', 42 | }, 43 | ], 44 | attrs: [ 45 | { 46 | prop: 'color', 47 | selector: '.A .B', 48 | template: '{ color }', 49 | attributes: ['color'], 50 | }, 51 | { 52 | prop: 'background-color', 53 | selector: '.A .B', 54 | template: '{ bgColor }', 55 | attributes: ['bgColor'], 56 | }, 57 | ], 58 | }) 59 | ).toEqual({ 60 | selector: '.A', 61 | className: '.A', 62 | base: 'li', 63 | attributes: [ 64 | { 65 | name: 'highlighted', 66 | }, 67 | ], 68 | attrs: [ 69 | { 70 | prop: 'color', 71 | selector: '.A .B', 72 | template: '{ color }', 73 | attributes: ['color'], 74 | }, 75 | { 76 | prop: 'background-color', 77 | selector: '.A .B', 78 | template: '{ bgColor }', 79 | attributes: ['bgColor'], 80 | }, 81 | ], 82 | invalidProps: { 83 | bgColor: true, 84 | color: false, 85 | highlighted: true, 86 | }, 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /dom/dist/bind-attrs-to-cssom.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/bind-attrs-to-cssom.js"],"names":["getCSSRule","attr","className","document","styleSheets","cssStylesheet","i","cssRules","length","rule","selectorText","includes","selector","insertRule","Error","template","bindAttrsToCSSOM","then","attrs","map","Math","random","toString","slice","cssRule"],"mappings":";;;;;;;;AAAA;;;;;;AAEA,IAAMA,aAAa,SAAbA,UAAa,CAACC,IAAD,EAAOC,SAAP,EAAqB;AAAA;AAAA;AAAA;;AAAA;AACtC,yBAA4BC,SAASC,WAArC,8HAAkD;AAAA,UAAvCC,aAAuC;;AAChD,WAAK,IAAIC,IAAI,CAAb,EAAgBA,IAAID,cAAcE,QAAd,CAAuBC,MAA3C,EAAmDF,GAAnD,EAAwD;AACtD,YAAMG,OAAOJ,cAAcE,QAAd,CAAuBD,CAAvB,CAAb;AACA,YAAIG,KAAKC,YAAL,IAAqBD,KAAKC,YAAL,CAAkBC,QAAlB,CAA2BV,KAAKW,QAAhC,CAAzB,EAAoE;AAClEP,wBAAcQ,UAAd,OAA8BX,SAA9B,UAA+CI,IAAI,CAAnD;AACA,iBAAOD,cAAcE,QAAd,CAAuBD,IAAI,CAA3B,CAAP;AACD;AACF;AACF;AATqC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAUtC,QAAM,IAAIQ,KAAJ,sBACgBb,KAAKc,QADrB,qEAAN;AAGD,CAbD;;AAeA,IAAMC,mBAAmB,SAAnBA,gBAAmB;AAAA,SACvB,kBAAQC,IAAR,CAAa;AAAA,WACXC,MAAMC,GAAN,CAAU,gBAAQ;AAChB,UAAMjB,YAAY,MAAMkB,KAAKC,MAAL,GAAcC,QAAd,CAAuB,EAAvB,EAA2BC,KAA3B,CAAiC,CAAjC,CAAxB;AACA,UAAMC,UAAUxB,WAAWC,IAAX,EAAiBC,SAAjB,CAAhB;AACA,0BAAYD,IAAZ,IAAkBC,oBAAlB,EAA6BsB,gBAA7B;AACD,KAJD,CADW;AAAA,GAAb,CADuB;AAAA,CAAzB;;kBASeR,gB","file":"bind-attrs-to-cssom.js","sourcesContent":["import DOMLoad from './dom-load';\n\nconst getCSSRule = (attr, className) => {\n for (const cssStylesheet of document.styleSheets) {\n for (let i = 0; i < cssStylesheet.cssRules.length; i++) {\n const rule = cssStylesheet.cssRules[i];\n if (rule.selectorText && rule.selectorText.includes(attr.selector)) {\n cssStylesheet.insertRule(`.${ className } {}`, i + 1);\n return cssStylesheet.cssRules[i + 1];\n }\n }\n }\n throw new Error(\n `The CSS rule of ${ attr.template } was not found. Make sure you imported the source CSS correctly`\n );\n};\n\nconst bindAttrsToCSSOM = attrs =>\n DOMLoad.then(() =>\n attrs.map(attr => {\n const className = 'a' + Math.random().toString(32).slice(6);\n const cssRule = getCSSRule(attr, className);\n return { ...attr, className, cssRule };\n })\n );\n\nexport default bindAttrsToCSSOM;\n"]} -------------------------------------------------------------------------------- /dom/src/bind-attrs-to-cssom.js: -------------------------------------------------------------------------------- 1 | import onDOMLoad from './on-dom-load'; 2 | 3 | function getFirstStyleSheet() { 4 | const [firstStyleSheet] = document.styleSheets; 5 | if (firstStyleSheet) { 6 | return firstStyleSheet; 7 | } else { 8 | const styleElement = document.createElement('style'); 9 | styleElement.setAttribute('type', 'text/css'); 10 | document.head.appendChild(styleElement); 11 | return styleElement.sheet; 12 | } 13 | } 14 | 15 | function insertCSSRule(selector, rule) { 16 | for (const cssStylesheet of document.styleSheets) { 17 | for (let i = 0; i < cssStylesheet.cssRules.length; i++) { 18 | const cssRule = cssStylesheet.cssRules[i]; 19 | if (cssRule.selectorText && cssRule.selectorText.includes(selector)) { 20 | cssStylesheet.insertRule(rule, i + 1); 21 | return cssStylesheet.cssRules[i + 1]; 22 | } 23 | } 24 | } 25 | throw new Error( 26 | 'A CSS rule used by a Stylesheet component was not found. Make sure you imported the source CSS correctly' 27 | ); 28 | } 29 | 30 | /** 31 | * @typedef {Attr} BoundAttr Attr bound to a CSSOM rule 32 | * @property {CSSRule} cssRule will be used to apply the result of the attr declaration 33 | * @property {string} className cssRule's class name 34 | */ 35 | 36 | /** 37 | * @param {Attr[]} attrs 38 | * @returns {BoundAttr[]} 39 | */ 40 | const bindAttrsToCSSOM = attrs => { 41 | const firstStyleSheet = getFirstStyleSheet(); 42 | const boundAttrs = attrs.map(attr => { 43 | const className = 'a' + Math.random().toString(32).slice(6); 44 | const cssRuleIndex = firstStyleSheet.cssRules.length; 45 | firstStyleSheet.insertRule(`${ attr.selector }.${ className } {}`, cssRuleIndex); 46 | const cssRule = firstStyleSheet.cssRules[cssRuleIndex]; 47 | return { ...attr, className, cssRule }; 48 | }); 49 | onDOMLoad(() => { 50 | for (const attr of boundAttrs) { 51 | const cssRuleIndex = [...firstStyleSheet.cssRules].indexOf(attr.cssRule); 52 | firstStyleSheet.deleteRule(cssRuleIndex); 53 | attr.cssRule = insertCSSRule(attr.selector, attr.cssRule.cssText); 54 | } 55 | }); 56 | return boundAttrs; 57 | }; 58 | 59 | export default bindAttrsToCSSOM; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylesheet", 3 | "version": "0.9.1", 4 | "description": "Dynamic CSS for user interfaces", 5 | "scripts": { 6 | "build": "yarn run build:internal dom; yarn run build:internal vanilla-dom; yarn run build:internal react-dom;", 7 | "build:internal": "build(){ babel $1/src --out-dir $1/dist --source-maps inline; }; build", 8 | "lint": "eslint core dom loader postcss react-dom vanilla-dom", 9 | "test": "jest --coverage && cat ./coverage/lcov.info | coveralls" 10 | }, 11 | "keywords": [ 12 | "css-modules", 13 | "webpack", 14 | "react", 15 | "css", 16 | "dynamic-style-sheets" 17 | ], 18 | "bugs": { 19 | "url": "https://github.com/iddan/stylesheet/issues", 20 | "email": "mail@aniddan.com" 21 | }, 22 | "license": "MIT", 23 | "main": " ", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/iddan/stylesheet.git" 27 | }, 28 | "directories": {}, 29 | "author": { 30 | "name": "Iddan Aharonson", 31 | "email": "mail@aniddan.com", 32 | "url": "aniddan.com" 33 | }, 34 | "dependencies": { 35 | "babel-core": "^6.24.1", 36 | "loader-utils": "^1.0.4", 37 | "lodash": "^4.17.4", 38 | "postcss": "^6.0.1", 39 | "postcss-import": "^10.0.0", 40 | "postcss-loader": "^2.0.6", 41 | "postcss-selector-parser": "^2.2.3", 42 | "postcss-value-parser": "^3.3.0", 43 | "react": "^15.4.2", 44 | "shorthash": "^0.0.2", 45 | "shortid": "^2.2.8", 46 | "webpack": "^4.0.0" 47 | }, 48 | "devDependencies": { 49 | "babel-cli": "^6.24.1", 50 | "babel-preset-env": "^1.2.2", 51 | "babel-preset-stage-2": "^6.24.1", 52 | "coveralls": "^2.13.1", 53 | "eslint": "^3.19.0", 54 | "eslint-config-fbjs-opensource": "^1.0.0", 55 | "eslint-plugin-lodash-fp": "^2.1.3", 56 | "eslint-plugin-promise": "^3.5.0", 57 | "jest": "^20.0.4", 58 | "raf": "^3.3.2" 59 | }, 60 | "engines": { 61 | "node": ">=6.0.0" 62 | }, 63 | "babel": { 64 | "presets": [ 65 | "env", 66 | "stage-2" 67 | ] 68 | }, 69 | "jest": { 70 | "setupFiles": [ 71 | "raf/polyfill" 72 | ], 73 | "coverageDirectory": "./coverage/", 74 | "collectCoverage": true 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/App.css: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none; 3 | } 4 | 5 | Container { 6 | background: #fff; 7 | margin: 130px 0 40px 0; 8 | position: relative; 9 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 10 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 11 | } 12 | 13 | Container input::-webkit-input-placeholder { 14 | font-style: italic; 15 | font-weight: 300; 16 | color: #e6e6e6; 17 | } 18 | 19 | Container input::-moz-placeholder { 20 | font-style: italic; 21 | font-weight: 300; 22 | color: #e6e6e6; 23 | } 24 | 25 | Container input::input-placeholder { 26 | font-style: italic; 27 | font-weight: 300; 28 | color: #e6e6e6; 29 | } 30 | 31 | Container h1 { 32 | position: absolute; 33 | top: -155px; 34 | width: 100%; 35 | font-size: 100px; 36 | font-weight: 100; 37 | text-align: center; 38 | color: rgba(175, 47, 47, 0.15); 39 | -webkit-text-rendering: optimizeLegibility; 40 | -moz-text-rendering: optimizeLegibility; 41 | text-rendering: optimizeLegibility; 42 | } 43 | 44 | Main { 45 | @apply section; 46 | position: relative; 47 | z-index: 2; 48 | border-top: 1px solid #e6e6e6; 49 | } 50 | 51 | label[for='toggle-all'] { 52 | display: none; 53 | } 54 | 55 | ToggleAll { 56 | @apply input; 57 | position: absolute; 58 | top: -55px; 59 | left: -12px; 60 | width: 60px; 61 | height: 34px; 62 | text-align: center; 63 | border: none; /* Mobile Safari */ 64 | } 65 | 66 | ToggleAll::before { 67 | content: "❯"; 68 | font-size: 22px; 69 | color: #e6e6e6; 70 | padding: 10px 27px 10px 27px; 71 | } 72 | 73 | ToggleAll:checked::before { 74 | color: #737373; 75 | } 76 | 77 | /* 78 | Hack to remove background from Mobile Safari. 79 | Can't use it globally since it destroys checkboxes in Firefox 80 | */ 81 | @media screen and (-webkit-min-device-pixel-ratio:0) { 82 | ToggleAll, 83 | TodoList li Toggle { 84 | background: none; 85 | } 86 | 87 | TodoList li Toggle { 88 | height: 40px; 89 | } 90 | 91 | ToggleAll { 92 | -webkit-transform: rotate(90deg); 93 | transform: rotate(90deg); 94 | -webkit-appearance: none; 95 | appearance: none; 96 | } 97 | } 98 | 99 | @media (max-width: 430px) { 100 | Footer { 101 | height: 50px; 102 | } 103 | 104 | Filters { 105 | bottom: 10px; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /loader/index.old.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable promise/no-callback-in-promise promise/always-return */ 2 | const postcss = require('postcss'); 3 | const cssLoader = require('css-loader'); 4 | const assert = require('assert'); 5 | const { stringifyRequest, getOptions } = require('loader-utils'); 6 | const _ = require('lodash/fp'); 7 | const stylesheetPostcssPlugin = require('../postcss'); 8 | const shortid = require('shortid'); 9 | const bindings = require('./bindings'); 10 | 11 | const id = shortid.generate(); 12 | 13 | module.exports = function(content) { 14 | const callback = this.async(); 15 | const options = getOptions(this); 16 | assert( 17 | bindings[options.bindings], 18 | `Bindings must be provided and be one of the following: ${ Object.keys(bindings).join() }` 19 | ); 20 | const { preprocess, createComponentPath } = bindings[options.bindings]; 21 | this.async = () => this.callback; 22 | let components; 23 | postcss([ 24 | stylesheetPostcssPlugin({ 25 | id: options.id || id, 26 | onComponents(receivedComponents) { 27 | components = receivedComponents; 28 | }, 29 | }), 30 | ]) 31 | .process(content) 32 | .then(result => { 33 | this.callback = (err, parsedContent, sourceMap, abstractSyntaxTree) => { 34 | callback( 35 | err, 36 | ` 37 | ${ parsedContent } 38 | var deepMerge = require(${ stringifyRequest(this, require.resolve('./deep-merge')) }); 39 | var importedComponentsData = exports 40 | .slice(0, exports.length - 1) 41 | .map(([id]) => __webpack_require__(id).components); 42 | var createComponent = require(${ stringifyRequest(this, createComponentPath) }); 43 | var moduleData = ${ JSON.stringify(_.mapValues(preprocess, components)) }; 44 | var data = deepMerge.apply(null, importedComponentsData.concat(moduleData)); 45 | exports.components = data; 46 | exports.locals = { 47 | ${ Object.keys(components) 48 | .map(component => `${ component }: createComponent(Object.assign({}, data.${ component }, { 49 | displayName: ${ JSON.stringify(component) } 50 | }))`) 51 | .join(',\n') } 52 | }; 53 | if (module.hot) { 54 | for (var key in module.hot.data) { 55 | Object.assign(module.hot.data[key], exports.locals[key]); 56 | }; 57 | module.hot.dispose(data => Object.assign(data, module.hot.data || exports.locals)); 58 | } 59 | `, 60 | sourceMap, 61 | abstractSyntaxTree 62 | ); 63 | }; 64 | cssLoader.call(this, result); 65 | }) 66 | .catch(callback); 67 | }; 68 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/TodoItem.css: -------------------------------------------------------------------------------- 1 | @import './TodoEdit.css'; 2 | 3 | TodoList { 4 | @apply ul; 5 | margin: 0; 6 | padding: 0; 7 | list-style: none; 8 | } 9 | 10 | TodoList TodoItem { 11 | @apply li; 12 | position: relative; 13 | font-size: 24px; 14 | border-bottom: 1px solid #ededed; 15 | } 16 | 17 | TodoList TodoItem:last-child { 18 | border-bottom: none; 19 | } 20 | 21 | TodoList TodoItem[editing] { 22 | border-bottom: none; 23 | padding: 0; 24 | } 25 | 26 | TodoList TodoItem[editing] TodoEdit { 27 | display: block; 28 | width: 506px; 29 | padding: 13px 17px 12px 17px; 30 | margin: 0 0 0 43px; 31 | } 32 | 33 | TodoList TodoItem[editing] TodoView { 34 | display: none; 35 | } 36 | 37 | TodoList TodoItem Toggle { 38 | @apply input; 39 | text-align: center; 40 | width: 40px; 41 | /* auto, since non-WebKit browsers doesn't support input styling */ 42 | height: auto; 43 | position: absolute; 44 | top: 0; 45 | bottom: 0; 46 | margin: auto 0; 47 | border: none; /* Mobile Safari */ 48 | -webkit-appearance: none; 49 | appearance: none; 50 | } 51 | 52 | TodoList TodoItem Toggle::after { 53 | content: url('data:image/svg+xml;utf8,'); 54 | } 55 | 56 | TodoList TodoItem Toggle:checked::after { 57 | content: url('data:image/svg+xml;utf8,'); 58 | } 59 | 60 | TodoList TodoItem label { 61 | white-space: pre-line; 62 | word-break: break-all; 63 | padding: 15px 60px 15px 15px; 64 | margin-left: 45px; 65 | display: block; 66 | line-height: 1.2; 67 | transition: color 0.4s; 68 | } 69 | 70 | TodoList TodoItem[completed] label { 71 | color: #d9d9d9; 72 | text-decoration: line-through; 73 | } 74 | 75 | TodoList TodoItem Destroy { 76 | @apply button; 77 | display: none; 78 | position: absolute; 79 | top: 0; 80 | right: 10px; 81 | bottom: 0; 82 | width: 40px; 83 | height: 40px; 84 | margin: auto 0; 85 | font-size: 30px; 86 | color: #cc9a9a; 87 | margin-bottom: 11px; 88 | transition: color 0.2s ease-out; 89 | } 90 | 91 | TodoList TodoItem Destroy:hover { 92 | color: #af5b5e; 93 | } 94 | 95 | TodoList TodoItem Destroy::after { 96 | content: '×'; 97 | } 98 | 99 | TodoList TodoItem:hover Destroy { 100 | display: block; 101 | } 102 | 103 | TodoList TodoItem TodoEdit { 104 | display: none; 105 | } 106 | 107 | TodoList TodoItem[editing]:last-child { 108 | margin-bottom: -1px; 109 | } 110 | -------------------------------------------------------------------------------- /dom/dist/generate-class-name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _matchAttribute = require('../../core/match-attribute'); 8 | 9 | var _matchAttribute2 = _interopRequireDefault(_matchAttribute); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 14 | 15 | var getClassName = function getClassName(_ref) { 16 | var className = _ref.className; 17 | return className; 18 | }; 19 | 20 | var generateClassName = function generateClassName(_ref2) { 21 | var className = _ref2.className, 22 | _ref2$attributes = _ref2.attributes, 23 | attributes = _ref2$attributes === undefined ? [] : _ref2$attributes, 24 | _ref2$attrs = _ref2.attrs, 25 | attrs = _ref2$attrs === undefined ? [] : _ref2$attrs; 26 | return function (props) { 27 | return [className].concat(_toConsumableArray(attrs.map(getClassName)), _toConsumableArray(attributes.filter(function (attribute) { 28 | return (0, _matchAttribute2.default)(attribute, props[attribute.name]); 29 | }).map(getClassName))).join(' '); 30 | }; 31 | }; 32 | 33 | exports.default = generateClassName; 34 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9nZW5lcmF0ZS1jbGFzcy1uYW1lLmpzIl0sIm5hbWVzIjpbImdldENsYXNzTmFtZSIsImNsYXNzTmFtZSIsImdlbmVyYXRlQ2xhc3NOYW1lIiwiYXR0cmlidXRlcyIsImF0dHJzIiwibWFwIiwiZmlsdGVyIiwiYXR0cmlidXRlIiwicHJvcHMiLCJuYW1lIiwiam9pbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUE7Ozs7Ozs7O0FBRUEsSUFBTUEsZUFBZSxTQUFmQSxZQUFlO0FBQUEsTUFBR0MsU0FBSCxRQUFHQSxTQUFIO0FBQUEsU0FBbUJBLFNBQW5CO0FBQUEsQ0FBckI7O0FBRUEsSUFBTUMsb0JBQW9CLFNBQXBCQSxpQkFBb0I7QUFBQSxNQUFHRCxTQUFILFNBQUdBLFNBQUg7QUFBQSwrQkFBY0UsVUFBZDtBQUFBLE1BQWNBLFVBQWQsb0NBQTJCLEVBQTNCO0FBQUEsMEJBQStCQyxLQUEvQjtBQUFBLE1BQStCQSxLQUEvQiwrQkFBdUMsRUFBdkM7QUFBQSxTQUFnRDtBQUFBLFdBQ3hFLENBQ0VILFNBREYsNEJBRUtHLE1BQU1DLEdBQU4sQ0FBVUwsWUFBVixDQUZMLHNCQUdLRyxXQUNBRyxNQURBLENBQ087QUFBQSxhQUFhLDhCQUFlQyxTQUFmLEVBQTBCQyxNQUFNRCxVQUFVRSxJQUFoQixDQUExQixDQUFiO0FBQUEsS0FEUCxFQUVBSixHQUZBLENBRUlMLFlBRkosQ0FITCxHQU1FVSxJQU5GLENBTU8sR0FOUCxDQUR3RTtBQUFBLEdBQWhEO0FBQUEsQ0FBMUI7O2tCQVNlUixpQiIsImZpbGUiOiJnZW5lcmF0ZS1jbGFzcy1uYW1lLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IG1hdGNoQXR0cmlidXRlIGZyb20gJy4uLy4uL2NvcmUvbWF0Y2gtYXR0cmlidXRlJztcblxuY29uc3QgZ2V0Q2xhc3NOYW1lID0gKHsgY2xhc3NOYW1lIH0pID0+IGNsYXNzTmFtZTtcblxuY29uc3QgZ2VuZXJhdGVDbGFzc05hbWUgPSAoeyBjbGFzc05hbWUsIGF0dHJpYnV0ZXMgPSBbXSwgYXR0cnMgPSBbXSB9KSA9PiBwcm9wcyA9PlxuICBbXG4gICAgY2xhc3NOYW1lLFxuICAgIC4uLmF0dHJzLm1hcChnZXRDbGFzc05hbWUpLFxuICAgIC4uLmF0dHJpYnV0ZXNcbiAgICAgIC5maWx0ZXIoYXR0cmlidXRlID0+IG1hdGNoQXR0cmlidXRlKGF0dHJpYnV0ZSwgcHJvcHNbYXR0cmlidXRlLm5hbWVdKSlcbiAgICAgIC5tYXAoZ2V0Q2xhc3NOYW1lKSxcbiAgXS5qb2luKCcgJyk7XG5cbmV4cG9ydCBkZWZhdWx0IGdlbmVyYXRlQ2xhc3NOYW1lO1xuIl19 -------------------------------------------------------------------------------- /react-dom/src/create-css-component.js: -------------------------------------------------------------------------------- 1 | import { createElement, Component } from 'react'; 2 | import { format } from '../../core/template'; 3 | import bindAttrsToCSSOM from '../../dom/dist/bind-attrs-to-cssom'; 4 | import generateClassName from '../../dom/dist/generate-class-name'; 5 | import Stylesheet from '../hot'; 6 | import { omitBy } from './utils.js'; 7 | 8 | /** 9 | * @param {string} displayName to be displayed in the React Dev Tools wrapped with Styled() 10 | * @param {string} className to be used for basic styles bound to the component's tag name 11 | * @param {Array} attributes selectors the component can be bound to 12 | * @param {Array} attrs in properties that the component can be bound to 13 | * @param {string} [base] tag the component uses by default 14 | * @param {Object} invalidProps to avoid passing to the component's DOM element 15 | * @returns {Class} 16 | */ 17 | module.exports = function createCSSComponent({ 18 | displayName, 19 | className, 20 | attributes, 21 | attrs, 22 | base = 'div', 23 | invalidProps, 24 | }) { 25 | return class CSSComponent extends Component { 26 | /** Defined on the component so they can be redefined and be updated in instances later */ 27 | static displayName = `Styled(${ displayName })`; 28 | static className = className; 29 | static attributes = attributes; 30 | static attrs = attrs; 31 | static base = base; 32 | static invalidProps = invalidProps; 33 | 34 | constructor(props) { 35 | super(props); 36 | this.init(); 37 | /** Register the instance for hot updates */ 38 | Stylesheet.register(this); 39 | } 40 | 41 | /** 42 | * Defined outside the constructor as it can be reused when the component's static properties change. 43 | * Currently used for HMR 44 | */ 45 | init() { 46 | this.attrs = bindAttrsToCSSOM(this.constructor.attrs); 47 | this.generateClassName = generateClassName({ 48 | className: this.constructor.className, 49 | attributes: this.constructor.attributes, 50 | attrs: this.attrs, 51 | }); 52 | this.applyAttrs(this.props); 53 | } 54 | 55 | componentWillUpdate(nextProps) { 56 | this.applyAttrs(nextProps); 57 | } 58 | 59 | componentWillUnmount() { 60 | Stylesheet.unregister(this); 61 | } 62 | 63 | /** Updates the element's CSS properties using attr() */ 64 | applyAttrs = props => { 65 | for (const attr of this.attrs) { 66 | attr.cssRule.style[attr.prop] = format(attr.template, props); 67 | } 68 | }; 69 | 70 | /** 71 | * @param {*} value of prop 72 | * @param {*} key of prop 73 | * @return {boolean} 74 | */ 75 | shouldOmitProp = (value, key) => { 76 | return this.constructor.invalidProps[key] || key === 'innerRef'; 77 | }; 78 | 79 | render() { 80 | const { props } = this; 81 | return createElement(this.constructor.base, { 82 | ref: props.innerRef, 83 | ...omitBy(props, this.shouldOmitProp), 84 | className: this.generateClassName(this.props), 85 | }); 86 | } 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /vanilla-dom/src/create-component.js: -------------------------------------------------------------------------------- 1 | import { format } from '../../core/template'; 2 | import matchAttribute from '../../core/match-attribute'; 3 | import bindAttrsToCSSOM from '../../dom/dist/bind-attrs-to-cssom'; 4 | import generateClassName from '../../dom/dist/generate-class-name'; 5 | 6 | const getAttributeClassNames = attributes => props => 7 | attributes 8 | .filter(attribute => matchAttribute(attribute, props[attribute.name])) 9 | .map(attribute => attribute.className); 10 | 11 | class CSSComponent { 12 | willUpdate = false; 13 | 14 | constructor(element, attrs, props = {}) { 15 | this.element = element; 16 | this.attrs = attrs; 17 | this.props = props; 18 | } 19 | 20 | observe(properties) { 21 | Object.defineProperties( 22 | this.element, 23 | properties.reduce( 24 | (acc, property) => ({ 25 | ...acc, 26 | [property]: { 27 | get: () => { 28 | return this.props[property]; 29 | }, 30 | set: value => { 31 | this.update({ ...this.props, [property]: value }); 32 | return value; 33 | }, 34 | }, 35 | }), 36 | {} 37 | ) 38 | ); 39 | } 40 | 41 | update(nextProps) { 42 | const prevProps = this.props; 43 | this.props = nextProps; 44 | if (!this.willUpdate) { 45 | this.willUpdate = true; 46 | setTimeout(() => { 47 | this.element.dispatchEvent( 48 | Object.assign(new Event('willUpdateStyle'), { 49 | props: prevProps, 50 | nextProps: this.props, 51 | }) 52 | ); 53 | this.willUpdate = false; 54 | requestAnimationFrame(() => { 55 | this.render(); 56 | this.element.dispatchEvent( 57 | Object.assign(new Event('didUpdateStyle'), { prevProps, props: this.props }) 58 | ); 59 | }); 60 | }); 61 | } 62 | } 63 | 64 | render() { 65 | const { props } = this; 66 | this.element.className = this.className; 67 | for (const attr of this.attrs) { 68 | if (attr.cssRule) { 69 | attr.cssRule.style[attr.prop] = format(attr.template, props); 70 | } 71 | } 72 | } 73 | } 74 | 75 | const createComponent = ({ className, attributes, attrs, base = 'div' }) => class 76 | extends CSSComponent { 77 | static getAttributeClassNames = getAttributeClassNames(attributes); 78 | 79 | static create(initialAttributes) { 80 | const instance = new this(initialAttributes); 81 | return instance.element; 82 | } 83 | 84 | static propKeys = [ 85 | ...attributes.map(attribute => attribute.name), 86 | ...attrs.reduce((acc, attr) => acc.concat(attr.attributes), []), 87 | ]; 88 | 89 | static className = className; 90 | static attributes = attributes; 91 | static attrs = attrs; 92 | static base = base; 93 | 94 | constructor(props) { 95 | super(document.createElement(base), bindAttrsToCSSOM(attrs), props); 96 | this.observe(this.constructor.propKeys); 97 | this.render(); 98 | } 99 | 100 | generateClassName = generateClassName({ className, attributes, attrs: this.attrs }); 101 | 102 | get className() { 103 | return this.generateClassName(this.props); 104 | } 105 | }; 106 | 107 | module.exports = createComponent; 108 | -------------------------------------------------------------------------------- /assets/react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /react-dom/dist/create-css-component.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/create-css-component.js"],"names":["module","exports","createCSSComponent","displayName","selector","className","attributes","attrs","base","invalidProps","props","state","then","setState","boundAttrs","nextProps","nextState","attr","cssRule","style","prop","template","value","key","map","filter","attribute","name","join"],"mappings":";;;;;;AAAA;;AACA;;AACA;;;;AACA;;;;AACA;;;;;;;;;;;;AAEA;;;;;;;;AAQAA,OAAOC,OAAP,GAAiB,SAASC,kBAAT,OAQd;AAAA;;AAAA,MAPDC,WAOC,QAPDA,WAOC;AAAA,MANDC,QAMC,QANDA,QAMC;AAAA,MALDC,SAKC,QALDA,SAKC;AAAA,MAJDC,UAIC,QAJDA,UAIC;AAAA,MAHDC,KAGC,QAHDA,KAGC;AAAA,uBAFDC,IAEC;AAAA,MAFDA,IAEC,6BAFM,KAEN;AAAA,MADDC,YACC,QADDA,YACC;;AACD;AAAA;;AAOE,0BAAYC,KAAZ,EAAmB;AAAA;;AAAA,8HACXA,KADW;;AAAA,YAJnBC,KAImB,GAJX;AACNJ,eAAO;AADD,OAIW;;AAEjB,sCAAiBA,KAAjB,EAAwBK,IAAxB,CAA6B;AAAA,eAAc,MAAKC,QAAL,CAAc,EAAEN,OAAOO,UAAT,EAAd,CAAd;AAAA,OAA7B;AAFiB;AAGlB;;AAVH;AAAA;AAAA,0CAYsBC,SAZtB,EAYiCC,SAZjC,EAY4C;AAAA;AAAA;AAAA;;AAAA;AACxC,+BAAmBA,UAAUT,KAA7B,8HAAoC;AAAA,gBAAzBU,IAAyB;;AAClC,gBAAIA,KAAKC,OAAT,EAAkB;AAChBD,mBAAKC,OAAL,CAAaC,KAAb,CAAmBF,KAAKG,IAAxB,IAAgC,sBAAOH,KAAKI,QAAZ,EAAsBN,SAAtB,CAAhC;AACD;AACF;AALuC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMzC;AAlBH;AAAA;AAAA,+BAoBW;AAAA,YACCL,KADD,GACkB,IADlB,CACCA,KADD;AAAA,YACQC,KADR,GACkB,IADlB,CACQA,KADR;;AAEP,eAAO,0BAAcH,IAAd,eACF,mBAAOE,KAAP,EAAc,UAACY,KAAD,EAAQC,GAAR;AAAA,iBAAgBd,aAAac,GAAb,CAAhB;AAAA,SAAd,CADE;AAELlB,qBAAW,CACTA,SADS,4BAENM,MAAMJ,KAAN,CAAYiB,GAAZ,CAAgB;AAAA,mBAAQP,KAAKZ,SAAb;AAAA,WAAhB,CAFM,sBAGNC,WACAmB,MADA,CAEC;AAAA,mBAAaf,MAAMgB,UAAUC,IAAhB,KAAyB,8BAAeD,SAAf,EAA0BhB,MAAMgB,UAAUC,IAAhB,CAA1B,CAAtC;AAAA,WAFD,EAIAH,GAJA,CAII;AAAA,mBAAaE,UAAUrB,SAAvB;AAAA,WAJJ,CAHM,GAQTuB,IARS,CAQJ,GARI;AAFN,WAAP;AAYD;AAlCH;;AAAA;AAAA,8BACSzB,WADT,GACuBA,WADvB;AAoCD,CA7CD","file":"create-css-component.js","sourcesContent":["import { createElement, Component } from 'react';\nimport { format } from '../../core/template';\nimport matchAttribute from '../../core/match-attribute';\nimport bindAttrsToCSSOM from '../../dom/dist/bind-attrs-to-cssom';\nimport { omitBy } from './utils.js';\n\n/**\n * @param {string} displayName\n * @param {string} selector\n * @param {string} className\n * @param {Object} props\n * @param {Object} attrs\n * @param {Object} invalidProps\n */\nmodule.exports = function createCSSComponent({\n displayName,\n selector,\n className,\n attributes,\n attrs,\n base = 'div',\n invalidProps,\n}) {\n return class CSSComponent extends Component {\n static displayName = displayName;\n\n state = {\n attrs: [],\n };\n\n constructor(props) {\n super(props);\n bindAttrsToCSSOM(attrs).then(boundAttrs => this.setState({ attrs: boundAttrs }));\n }\n\n componentWillUpdate(nextProps, nextState) {\n for (const attr of nextState.attrs) {\n if (attr.cssRule) {\n attr.cssRule.style[attr.prop] = format(attr.template, nextProps);\n }\n }\n }\n\n render() {\n const { props, state } = this;\n return createElement(base, {\n ...omitBy(props, (value, key) => invalidProps[key]),\n className: [\n className,\n ...state.attrs.map(attr => attr.className),\n ...attributes\n .filter(\n attribute => props[attribute.name] && matchAttribute(attribute, props[attribute.name])\n )\n .map(attribute => attribute.className),\n ].join(' '),\n });\n }\n };\n};\n"]} -------------------------------------------------------------------------------- /__tests__/vanilla-dom/create-component.js: -------------------------------------------------------------------------------- 1 | import { preprocess } from '../../vanilla-dom'; 2 | import createComponent from '../../vanilla-dom/dist/create-component'; 3 | 4 | test('component shape', () => { 5 | const Component = createComponent( 6 | preprocess({ 7 | selector: '.A', 8 | className: 'A', 9 | base: 'li', 10 | }) 11 | ); 12 | const instance = new Component(); 13 | expect(instance).toEqual( 14 | expect.objectContaining({ 15 | element: expect.any(HTMLElement), 16 | }) 17 | ); 18 | expect(Component.create()).toBeInstanceOf(HTMLElement); 19 | }); 20 | 21 | test('element listens to changes and dispatches render events', done => { 22 | const Component = createComponent( 23 | preprocess({ 24 | selector: '.A', 25 | className: 'A', 26 | base: 'li', 27 | attributes: [ 28 | { 29 | name: 'highlighted', 30 | className: '.A-highlighted', 31 | }, 32 | ], 33 | attrs: [ 34 | { 35 | prop: 'color', 36 | selector: '.A', 37 | template: '{ color }', 38 | attributes: ['color'], 39 | }, 40 | { 41 | prop: 'background-color', 42 | selector: '.A', 43 | template: '{ bgColor }', 44 | attributes: ['bgColor'], 45 | }, 46 | ], 47 | }) 48 | ); 49 | const element = Component.create(); 50 | element.addEventListener('willUpdateStyle', e => { 51 | try { 52 | expect(e).toEqual( 53 | expect.objectContaining({ 54 | props: {}, 55 | nextProps: { highlighted: true, color: 'white', bgColor: 'blue' }, 56 | }) 57 | ); 58 | } catch (err) { 59 | done.fail(err); 60 | } 61 | }); 62 | element.addEventListener('didUpdateStyle', e => { 63 | try { 64 | expect(e).toEqual( 65 | expect.objectContaining({ 66 | prevProps: {}, 67 | props: { highlighted: true, color: 'white', bgColor: 'blue' }, 68 | }) 69 | ); 70 | } catch (err) { 71 | done.fail(err); 72 | } 73 | done(); 74 | }); 75 | element.highlighted = true; 76 | element.color = 'white'; 77 | element.bgColor = 'blue'; 78 | }); 79 | 80 | test("element's class is updated according to props", done => { 81 | const Component = createComponent( 82 | preprocess({ 83 | selector: '.A', 84 | className: 'A', 85 | base: 'li', 86 | attributes: [ 87 | { 88 | name: 'highlighted', 89 | className: '.A-highlighted', 90 | }, 91 | ], 92 | }) 93 | ); 94 | const element = Component.create(); 95 | expect(element.className).not.toEqual(expect.stringContaining('.A-highlighted')); 96 | element.highlighted = true; 97 | element.addEventListener('didUpdateStyle', () => { 98 | try { 99 | expect(element.className).toEqual(expect.stringContaining('.A-highlighted')); 100 | } catch (err) { 101 | done.fail(err); 102 | } 103 | done(); 104 | }); 105 | }); 106 | 107 | test("element's style is updating according to props", done => { 108 | const Component = createComponent( 109 | preprocess({ 110 | selector: '.A', 111 | className: 'A', 112 | base: 'li', 113 | attrs: [ 114 | { 115 | prop: 'color', 116 | selector: '.A', 117 | template: '{ color = "yellow" }', 118 | attributes: ['color'], 119 | }, 120 | ], 121 | }) 122 | ); 123 | const instance = new Component(); 124 | const { element } = instance; 125 | for (const attr of instance.attrs) { 126 | expect(element.matches(attr.cssRule.selectorText)).toBeTruthy(); 127 | } 128 | element.addEventListener('didUpdateStyle', () => { 129 | try { 130 | expect(instance.attrs[0].cssRule.style.color).toBe('blue'); 131 | } catch (err) { 132 | done.fail(err); 133 | } 134 | done(); 135 | }); 136 | element.color = 'blue'; 137 | }); 138 | -------------------------------------------------------------------------------- /__tests__/postcss/index.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import stylesheetPostcssPlugin from '../../postcss'; 3 | 4 | const process = async cssString => { 5 | let components; 6 | return { 7 | ...(await postcss([ 8 | stylesheetPostcssPlugin({ 9 | id: 'id', 10 | onComponents: receivedComponents => { 11 | components = receivedComponents; 12 | }, 13 | }), 14 | ]).process(cssString)), 15 | components, 16 | }; 17 | }; 18 | 19 | test('Extracts component', async () => { 20 | const { components } = await process( 21 | ` 22 | Label { 23 | background: grey; 24 | } 25 | ` 26 | ); 27 | return expect(components).toEqual( 28 | expect.objectContaining({ 29 | Label: { 30 | attributes: [], 31 | attrs: [], 32 | base: undefined, 33 | className: 'Label_id', 34 | }, 35 | }) 36 | ); 37 | }); 38 | 39 | test('Extracts no operator attribute', async () => { 40 | const { components } = await process( 41 | ` 42 | Label[highlighted] { 43 | background: grey; 44 | } 45 | ` 46 | ); 47 | return expect(components).toEqual( 48 | expect.objectContaining({ 49 | Label: { 50 | attributes: [ 51 | { 52 | className: expect.stringContaining('Label-highlighted'), 53 | name: 'highlighted', 54 | insensitive: undefined, 55 | operator: undefined, 56 | value: undefined, 57 | }, 58 | ], 59 | attrs: [], 60 | base: undefined, 61 | className: 'Label_id', 62 | }, 63 | }) 64 | ); 65 | }); 66 | 67 | test('Extracts = operator attribute ', async () => { 68 | const { components } = await process( 69 | ` 70 | Person[name="Dan"] { 71 | background: grey; 72 | } 73 | ` 74 | ); 75 | return expect(components).toEqual( 76 | expect.objectContaining({ 77 | Person: { 78 | attributes: [ 79 | { 80 | className: expect.stringContaining('Person-name'), 81 | name: 'name', 82 | insensitive: undefined, 83 | operator: '=', 84 | value: 'Dan', 85 | }, 86 | ], 87 | attrs: [], 88 | base: undefined, 89 | className: 'Person_id', 90 | }, 91 | }) 92 | ); 93 | }); 94 | 95 | test('Extracts attr() declaration', async () => { 96 | const { components } = await process( 97 | ` 98 | Person { 99 | background: attr(bgColor color); 100 | } 101 | ` 102 | ); 103 | return expect(components).toEqual( 104 | expect.objectContaining({ 105 | Person: { 106 | attributes: [], 107 | attrs: [ 108 | { 109 | attributes: ['bgColor'], 110 | prop: 'background', 111 | selector: '.Person_id', 112 | template: '{ bgColor }', 113 | }, 114 | ], 115 | base: undefined, 116 | className: 'Person_id', 117 | }, 118 | }) 119 | ); 120 | }); 121 | 122 | test('Postfixes attr() declaration', async () => { 123 | const { components } = await process( 124 | ` 125 | Person { 126 | height: attr(height %); 127 | } 128 | ` 129 | ); 130 | return expect(components).toEqual( 131 | expect.objectContaining({ 132 | Person: { 133 | attributes: [], 134 | attrs: [ 135 | { 136 | attributes: ['height'], 137 | prop: 'height', 138 | selector: '.Person_id', 139 | template: '{ height }%', 140 | }, 141 | ], 142 | base: undefined, 143 | className: 'Person_id', 144 | }, 145 | }) 146 | ); 147 | }); 148 | 149 | test('Extracts @apply base rules', async () => { 150 | const { components } = await process( 151 | ` 152 | Person { 153 | @apply figure; 154 | } 155 | ` 156 | ); 157 | return expect(components).toEqual( 158 | expect.objectContaining({ 159 | Person: { 160 | attributes: [], 161 | attrs: [], 162 | base: 'figure', 163 | className: 'Person_id', 164 | }, 165 | }) 166 | ); 167 | }); 168 | -------------------------------------------------------------------------------- /assets/stylesheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 23 | 30 | 31 | 32 | 33 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/react-todo-mvc/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Container, Main, ToggleAll, TodoList } from './App.css'; 4 | import NewTodo from './NewTodo'; 5 | import TodoItem from './TodoItem'; 6 | import Footer from './Footer'; 7 | 8 | const filterTodos = (filter, todos) => { 9 | switch (filter) { 10 | case 'active': { 11 | return todos.filter(todo => !todo.completed); 12 | } 13 | case 'completed': { 14 | return todos.filter(todo => todo.completed); 15 | } 16 | default: { 17 | return todos; 18 | } 19 | } 20 | }; 21 | 22 | class TodoApp extends PureComponent { 23 | static propTypes = { 24 | todos: PropTypes.arrayOf( 25 | PropTypes.shape({ 26 | id: PropTypes.string.isRequired, 27 | title: PropTypes.string.isRequired, 28 | completed: PropTypes.bool.isRequired, 29 | }) 30 | ), 31 | onTodosChange: PropTypes.func.isRequired, 32 | }; 33 | 34 | state = { 35 | filter: location.hash.replace('#/', '') || 'all', 36 | todos: this.props.todos || [], 37 | }; 38 | 39 | componentDidUpdate(prevProps, prevState) { 40 | if (prevState.todos !== this.state.todos) { 41 | this.props.onTodosChange(this.state.todos); 42 | } 43 | } 44 | 45 | addNewTodo = ({ title }) => { 46 | this.setState(state => ({ 47 | ...state, 48 | todos: [ 49 | ...state.todos, 50 | { 51 | id: Math.random().toString(32), 52 | completed: false, 53 | title, 54 | }, 55 | ], 56 | })); 57 | }; 58 | 59 | setFilter = filter => { 60 | this.setState({ filter }); 61 | }; 62 | 63 | handleToggleAllChange = e => { 64 | const { checked } = e.target; 65 | this.setState(state => ({ 66 | ...state, 67 | todos: state.todos.map(todo => ({ 68 | ...todo, 69 | completed: checked, 70 | })), 71 | })); 72 | }; 73 | 74 | updateTodo = todo => { 75 | this.setState(state => { 76 | const { todos } = state; 77 | const todoIndex = todos.findIndex(({ id }) => todo.id === id); 78 | const newTodos = [...todos]; 79 | newTodos[todoIndex] = { 80 | ...todos[todoIndex], 81 | ...todo, 82 | }; 83 | return { 84 | ...state, 85 | todos: newTodos, 86 | }; 87 | }); 88 | }; 89 | 90 | destroyTodo = todo => { 91 | this.setState(state => { 92 | const { todos } = this.state; 93 | const todoIndex = todos.findIndex(({ id }) => todo.id === id); 94 | return { 95 | ...state, 96 | todos: [...todos.slice(0, todoIndex), ...todos.slice(todoIndex + 1, todos.length)], 97 | }; 98 | }); 99 | }; 100 | 101 | handleClearCompleted = () => { 102 | this.setState(state => ({ 103 | todos: state.todos.filter(todo => !todo.completed), 104 | })); 105 | }; 106 | 107 | render() { 108 | const { todos, filter } = this.state; 109 | const complete = todos.reduce((sum, todo) => sum + Number(todo.completed), 0); 110 | const shownTodos = filterTodos(filter, todos); 111 | return ( 112 | 113 |
    114 |

    todos

    115 | 116 |
    117 |
    118 | todo.completed)} 121 | onChange={this.handleToggleAllChange} 122 | /> 123 | 124 | {shownTodos.map(todo => ( 125 | 133 | ))} 134 | 135 |
    136 | {Boolean(todos.length) && 137 |