diff --git a/.gitignore b/.gitignore index 515cf0f..ebdf1ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ node_modules public/*.js +public/*.css +public/stats.html coverage .nyc_output diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2f41c29 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +sudo: false +language: node_js +node_js: +- '6' +script: npm test && npm run build +after_success: npm run coverage:ci +deploy: + provider: script + skip_cleanup: true + script: npm run deploy + on: + repo: tracespace/viewer + tags: true + all_branches: true +env: + global: + secure: M/MgaJEOmT7Vg8p2MAiNKzGpAPgNZJtUKHEGcv6vm55TfXQ+7BwZYjr2xU+A+LvqMSU1aQEZWnX7TeD94YZExmEIptrgz9Q1D3E4UXPEMY3ojJp9jRnuJQghe6TzBV62ERrAK+aYpb2+ONHfIfmV54+gSj3aT/kTzQeR5Uv83yItZJ1WhaDQgvUTbMiORt+6yQl8GE0uG6VAam0uPQ4e6xdFVQLA2uVXKwsDDkBuMydhDFCT8CUgPE2dvTu+WU5d00mmP56AmUbncVvw3E3GfhYMW+CaqsWiNb3cpNwcb/afiUTf48fal8oAUYvPJtecydQ9b4fCtqTHpUCYoch/dcaeiM/cLKC4CSYvgxAyRybDxsCQJlixgT7YLf9fETnKvTCDz9sz5MzZmBGtVgGe4CjnGH3aeiE7lwlEaHui/dnHlz6XLB+FucUbClNxCcnGvA3uB50lrAzkj9jxBnvAnS+sljTK/AAz1nu/8GHZK8xOlOEmEWdVGNgPPmOzCF1t0EsB5gRURLBWGGAQXferB/dqnTRyjKI/H/0cUsYNHdMv1G4sJB1I3weuYtZMbkVqqycVP0TLKG0/5a/TDhZgOpd05Q10fjdRrC/wU7guNINHV8Qes08Bx2kcQk55GC/R42edasWZ89JrR3NvaUesNsR33CZGMoAzpcQ3uOwXTUA= diff --git a/README.md b/README.md index 0130491..c0f1825 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,51 @@ -# tracespace pcb viewer +# tracespace viewer (beta) -[![Travis](https://img.shields.io/travis/tracespace/viewer.svg?style=flat-square)](https://travis-ci.org/tracespace/viewer) -[![Coveralls](https://img.shields.io/coveralls/tracespace/viewer.svg?style=flat-square)](https://coveralls.io/github/tracespace/viewer) +[![GitHub stars](https://img.shields.io/github/stars/tracespace/viewer.svg?style=flat-square&label=%E2%AD%90&maxAge=2592000)](https://github.com/tracespace/viewer) +[![GitHub issues](https://img.shields.io/github/issues/tracespace/viewer.svg?style=flat-square&maxAge=2592000)](https://github.com/tracespace/viewer/issues) +[![Travis](https://img.shields.io/travis/tracespace/viewer/master.svg?style=flat-square)](https://travis-ci.org/tracespace/viewer) +[![Coveralls](https://img.shields.io/coveralls/tracespace/viewer/master.svg?style=flat-square)](https://coveralls.io/github/tracespace/viewer) [![David](https://img.shields.io/david/tracespace/viewer.svg?style=flat-square)](https://david-dm.org/tracespace/viewer) [![David devDependences](https://img.shields.io/david/dev/tracespace/viewer.svg?style=flat-square)](https://david-dm.org/tracespace/viewer#info=devDependencies) -[![GitHub stars](https://img.shields.io/github/stars/tracespace/viewer.svg?style=social&label=Star&maxAge=2592000?style=flat-square)](https://github.com/tracespace/viewer) +Probably the best printed circuit board viewer on the internet -[http://viewer.tracespace.io](viewer.tracespace.io) - -Probably the best printed circuit board viewer on the internet. + ## about -This particular PCB viewer takes your gerber and drill files and gives you individual layer renders as well as very fancy renders of what your completed boards are going to look like. It does this all locally using the [gerber-to-svg](https://github.com/mcous/gerber-to-svg) module, so nothing gets sent to any server. Also, because the renders are SVGs, they're super easy to save and show off. +This particular PCB viewer takes your Gerber and drill files and gives you individual layer renders as well as very fancy renders of what your completed boards are going to look like. -Using awesome new web technologies, the tracespace viewer also works offline and saves your renders locally. Pretty cool. +The tracespace viewer accomplishes all this locally (nothing gets sent to any server!) by converting your files to SVGs. Thanks to the "Scalable" and "Vector" in "SVG", the renders are easy to examine and quite accurate. ## motivation -Because you should always check your design files for problems! Existing Gerber viewers are good, but tend not to give you a full picture of what your board will look like, meaning you can miss important details (shout out to [OSH Park](https://oshpark.com) for super cool renders by default). Gerber files (i.e. your PCB manufacturing files) are vector image files, so it makes sense to convert them into a web-friendly vector format. And an offline-enabled web-app means you don't have to worry about downloading or updating any software, and you'll still always have your the viewer available. +Because you should always check your design files for problems! Existing Gerber viewers are good, but tend not to give you a full picture of what your board will look like, meaning you can miss important details (shout out to [OSH Park](https://oshpark.com) for super cool renders by default). Gerber files (i.e. your PCB manufacturing files) are vector image files, so it makes sense to convert them into a scalable, web-friendly vector format. ## issues -See something that doesn't look right? [Open an issue!](https://github.com/tracespace/viewer/issues) Screenshots and Gerber files help if you're able to. If you're not comfortable publicly posting your designs, you can also email viewer@tracespace.io with your issue. +See something that doesn't look right? [Open an issue!](https://github.com/tracespace/viewer/issues) Screenshots and Gerber files help if you're able to. If you're not comfortable publicly posting your designs, you can also email with your issue. If you're technically inclined, PR's are always welcome! ## contributing -This site is written in ES2015 and uses [babel](https://babeljs.io/) and [webpack](http://webpack.github.io/) to run in your browser. The state layer is build using [redux](http://rackt.github.io/redux/) and the view layer is built with [deku](http://dekujs.github.io/deku/index.html). Rendering is accomplished with [web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API), [gerber-to-svg](https://github.com/mcous/gerber-to-svg), and the [tracespace pcb stackup builder](https://github.com/tracespace/pcb-stackup). Local data is stored in [indexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). +This site is written in [ES2040](https://github.com/ahdinosaur/es2040) and uses [babel](https://babeljs.io/) and [webpack](http://webpack.github.io/) to build for the browser. The state layer is build using [redux](http://rackt.github.io/redux/) and the view layer is built with [deku](https://github.com/anthonyshort/deku). Rendering is accomplished with [web workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API), [gerber-to-svg](https://github.com/mcous/gerber-to-svg), and the [tracespace pcb stackup builder](https://github.com/tracespace/pcb-stackup-core). + +Tests are run with [ava](https://github.com/avajs/ava) and code is linted with [eslint](http://eslint.org/). Please make sure any PRs are accompanied by unit tests. We use [travis](https://travis-ci.org/) as our CI server. -Tests are run with [mocha](http://mochajs.org/) and code is linted with [eslint](http://eslint.org/). Please make sure any PRs are accompanied by unit tests. We use [travis](https://travis-ci.org/) as our CI server and run browser tests with [sauce labs](https://saucelabs.com/opensauce/) and [zuul](https://github.com/defunctzombie/zuul). +This app is hosted on [GitHub Pages](https://pages.github.com/). ### build scripts -Nothing fancy here, just npm scripts. See [package.json](package.json) for the full list. These are the important ones: +Nothing fancy here, just npm scripts. See [package.json](https://github.com/tracespace/viewer/blob/master/package.json) for the full list. These are the important ones: * `$ npm start` - starts an HMR dev server at [localhost:8080](http://localhost:8080) * `$ npm test` - run tests * `$ npm run test:watch` - run tests on code changes -* `$ npm run test:browser` - run tests locally in a browser of your choosing -* `$ npm run test:sauce` - run the tests on sauce (sauce credentials in [.zuulrc](https://github.com/defunctzombie/zuul/wiki/Zuulrc) required) -* `$ npm run deploy` - builds and deploys the site (credentials required) + +### deploying + +The site is deployed automatically by Travis if the commit is tagged (and tests pass). To deploy: + +1. `$ npm version ...` - Use the npm version command to bump the version and tag the commit +2. `$ git push --tags` - Push the tag to trigger a build diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..e304721 --- /dev/null +++ b/config/index.js @@ -0,0 +1,92 @@ +// build configs +'use strict' + +const path = require('path') +const webpack = require('webpack') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const Visualizer = require('webpack-visualizer-plugin') + +// postcss plugins +const postcssImport = require('postcss-import') +const cssnext = require('postcss-cssnext') + +const resolve = (file) => path.resolve(__dirname, file) + +const ENTRY = resolve('../src/index.js') +const OUT = resolve('../public') +const JS_OUT = 'bundle.js' +const CSS_OUT = 'bundle.css' + +const PROD_PLUGIN_OPTS = { + 'process.env': {NODE_ENV: JSON.stringify('production')} +} + +const BASE_CONFIG = { + devtool: '#source-map', + entry: [ENTRY], + output: { + path: OUT, + filename: JS_OUT, + library: 'app', + publicPath: '/' + }, + postcss: (wpContext) => [ + postcssImport({addDependencyTo: wpContext}), + cssnext + ], + module: {} +} + +module.exports = { + create(plugins = [], loaders = []) { + const config = Object.assign({}, BASE_CONFIG) + + config.plugins = plugins.map((factory) => factory()) + config.module.loaders = loaders + + return config + }, + + plugin: { + prodEnv: () => new webpack.DefinePlugin(PROD_PLUGIN_OPTS), + occurrence: () => new webpack.optimize.OccurrenceOrderPlugin(), + dedupe: () => new webpack.optimize.DedupePlugin(), + uglifyJs: () => new webpack.optimize.UglifyJsPlugin(), + extractCss: () => new ExtractTextPlugin(CSS_OUT), + visualizeBundle: () => new Visualizer(), + hmr: () => new webpack.HotModuleReplacementPlugin(), + noErrors: () => new webpack.NoErrorsPlugin() + }, + + loader: { + worker: { + test: /\worker\.js$/, + exclude: /node_modules/, + loader: 'worker' + }, + + babel: { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel', + query: { + presets: ['es2040'] + } + }, + + css: { + test: /\.css$/, + loader: 'style-loader!css-loader!postcss-loader' + }, + + cssExtracted: { + test: /\.css$/, + loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader') + }, + + markdown: { + test: /\.md$/, + loader: 'html!markdown' + } + } +} diff --git a/package.json b/package.json index a6eea5e..07102d6 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,20 @@ "main": "src/index.js", "scripts": { "start": "node server.js", - "lint": "eslint 'src/**/*.js' 'test/**/*.js'", + "prebuild": "rm -f public/*.js public/*.map public/*.css public/stats.html", + "build": "webpack", + "lint": "eslint '*.js' 'config/*.js' 'src/**/*.js' 'test/**/*.js'", "test": "nyc --all --include='src/**/*.js' ava --verbose", "test:watch": "ava --watch", - "test:browser": "echo 'script not implemented' && exit 1", - "test:sauce": "echo 'script not implemented' && exit 1", "posttest": "npm run lint", - "deploy": "echo 'script not implemented' && exit 1", "coverage": "nyc report", - "coverage:html": "nyc report --reporter=html" + "coverage:html": "nyc report --reporter=html", + "coverage:ci": "nyc report --reporter=text-lcov | coveralls", + "ci:gitconfig:name": "git config --global user.name 'tracespace bot'", + "ci:gitconfig:email": "git config --global user.email 'bot@tracespace.io'", + "ci:gitconfig": "npm run ci:gitconfig:name && npm run ci:gitconfig:email", + "predeploy": "if [ \"$CI\" = \"true\" ]; then npm run ci:gitconfig; fi", + "deploy": "gh-pages -x -r https://$GH_TOKEN@github.com/tracespace/viewer.git -d public -m \"Build $TRAVIS_BUILD_NUMBER\"" }, "repository": { "type": "git", @@ -28,20 +33,32 @@ "engines": { "node": ">=6.0.0" }, + "private": true, "devDependencies": { "amp-has-class": "^1.0.3", "ava": "^0.15.2", "babel-core": "^6.11.4", "babel-loader": "^6.2.4", "babel-preset-es2040": "^1.1.1", - "eslint": "^2.9.0", + "coveralls": "^2.11.11", + "css-loader": "^0.23.1", + "eslint": "^3.1.1", "express": "^4.13.4", + "extract-text-webpack-plugin": "^1.0.1", + "gh-pages": "^0.11.0", + "html-loader": "^0.4.3", "jsdom": "^9.4.1", + "markdown-loader": "^0.1.7", "nyc": "^7.0.0", + "postcss-cssnext": "^2.7.0", + "postcss-import": "^8.1.2", + "postcss-loader": "^0.9.1", "sinon": "^1.17.4", + "style-loader": "^0.13.1", "webpack": "^1.13.0", "webpack-dev-middleware": "^1.6.1", "webpack-hot-middleware": "^2.10.0", + "webpack-visualizer-plugin": "^0.1.5", "worker-loader": "^0.7.0" }, "dependencies": { @@ -50,11 +67,12 @@ "deku": "^2.0.0-rc16", "filereader-stream": "^1.0.0", "gerber-to-svg": "^2.0.1", + "is-svg-element": "github:tracespace/is-svg-element#v1.0.1", "lodash.omit": "^4.3.0", "lodash.set": "^4.2.0", "lodash.uniqueid": "^4.0.0", "lodash.without": "^4.2.0", - "pcb-stackup-core": "^1.0.0", + "pcb-stackup-core": "^2.0.0", "raf": "^3.2.0", "randomcolor": "^0.4.4", "redux": "^3.5.2", @@ -63,6 +81,8 @@ "redux-throttle": "^0.1.1", "reselect": "^2.5.1", "shortid": "^2.2.6", + "tachyons": "^4.0.4", + "tachyons-z-index": "^1.0.0", "viewbox": "^1.0.0", "whats-that-gerber": "^2.0.1" } diff --git a/public/CNAME b/public/CNAME new file mode 100644 index 0000000..ca3c08b --- /dev/null +++ b/public/CNAME @@ -0,0 +1 @@ +viewer.tracespace.io diff --git a/public/index.css b/public/index.css deleted file mode 100644 index b913eda..0000000 --- a/public/index.css +++ /dev/null @@ -1,250 +0,0 @@ -body { - font-family: 'Open Sans', Helvetica, sans-serif; -} - -.brand { - color: #3cc; -} - -.brand-2 { - color: #2D3142; -} - -.accent { - color: #BFC0C0; -} - -.accent:hover { - color: #BFC0C0; -} - -.bg-brand { - background-color: #3cc; -} - -.bg-brand-2 { - background-color: #2D3142; -} - -.bg-accent { - background-color: #BFC0C0; -} - -.bg-accent-hover:hover { - background-color: #BFC0C0; -} - -.bg-link { - transition: background-color .15s ease-in; -} - -.bg-app { - background-color: #C9DAEA; -} - -.striped--brand-light:nth-child(odd) { - background-color: rgba(51, 204, 204, 0.1); -} - -.inherit-color { - color: inherit; -} - -.z-1 { - z-index: 1; -} - -.z-back { - z-index: -1; -} - -.center-vertical { - top: 50%; - transform: translateY(-50%); -} - -.transform-center { - transform: translate(-50%, -50%); -} - -.smooth-transform { - transition: transform 100ms; -} - -.left-50 { - left: 50%; -} - -.top-50 { - top: 50%; -} - - -.btn[disabled], -.btn.disabled { - backgound-color: grey; - cursor: default; -} - -.layer-opacity { - opacity: 0.5; -} - -.fx { - display: flex; -} - -.fx-0-0 { - flex: 0 0 auto; -} - -.fx-1-1 { - flex: 1 1 auto; -} - -.fx-b-3 { - flex-basis: calc(100% / 3); -} - -.fx-jc-sa { - justify-content: space-around; -} - -.fx-jc-sb { - justify-content: space-between; -} - -.fx-ai-c { - align-items: center; -} - -.fx-d-c { - flex-direction: column; -} - -.fx-wrap { - flex-wrap: wrap; -} - -.mr-auto { - margin-right: auto; -} - -.mt3-past-h3 { - margin-top: 5rem; -} - -.app-ht { - height: calc(100% - 6rem); -} - -.max-app-ht { - max-height: calc(100% - 6rem); -} - -.max-h5 { - max-height: 16rem; -} - -.collapsible { - transition: max-height 0.3s linear; - overflow: hidden; -} - -.is-collapsed { - max-height: 0; -} - -.h0 { - height: 0 -} - -.h-15 { - height: 15%; -} - -.h-1-3 { - height: calc(100% / 3); -} - -.w0 { - width: 0; -} - -.w-5 { - width: 5%; -} - -.w-47-5 { - width: 47.5%; -} - -.w-1-3 { - width: calc(100% / 3); -} - -.grab { - cursor: move; - cursor: grab; - cursor: -moz-grab; - cursor: -webkit-grab; -} - -.grab:active { - cursor: grabbing; - cursor: -moz-grabbing; - cursor: -webkit-grabbing; -} - -.m-auto { - margin: auto; -} - -.mh-auto { - margin-left: auto; - margin-right: auto; -} - -.mb-1-3 { - margin-bottom: 0.5rem; -} - -.triangle-up { - border-top: 0; - border-left: 0.5rem solid transparent; - border-right: 0.5rem solid transparent; - border-bottom: 1rem solid; -} - -.triangle-right { - border-top: 0.5rem solid transparent; - border-left: 1rem solid; - border-right: 0; - border-bottom: 0.5rem solid transparent; -} - -.triangle-down { - border-top: 1rem solid; - border-left: 0.5rem solid transparent; - border-right: 0.5rem solid transparent; - border-bottom: 0; -} - -.triangle-left { - border-top: 0.5rem solid transparent; - border-left: 0; - border-right: 1rem solid; - border-bottom: 0.5rem solid transparent; -} - -.click-thru { - pointer-events: none; -} - -.clickable { - pointer-events: auto; -} - -.aspect-ratio--1x1 { - padding-bottom: 100%; -} diff --git a/public/index.html b/public/index.html index c4a0e3a..6f5abd7 100644 --- a/public/index.html +++ b/public/index.html @@ -2,11 +2,12 @@ tracespace | viewer + + + - - - - + + diff --git a/public/test.gif b/public/test.gif deleted file mode 100644 index 18909ed..0000000 Binary files a/public/test.gif and /dev/null differ diff --git a/server.js b/server.js index 0428f99..17caa1e 100644 --- a/server.js +++ b/server.js @@ -1,18 +1,38 @@ +// dev server with HMR +'use strict' + const webpack = require('webpack') const dev = require('webpack-dev-middleware') const hot = require('webpack-hot-middleware') const express = require('express') -const config = require('./webpack.config.js') - const HOST = process.env.DEV_HOST || 'localhost' const PORT = process.env.DEV_PORT || 8080 +const {create, plugin, loader} = require('./config') +const plugins = [plugin.hmr, plugin.noErrors, plugin.visualizeBundle] +const loaders = [loader.worker, loader.babel, loader.css, loader.markdown] +const config = create(plugins, loaders) + +// load hot middleware client +config.entry.unshift('webpack-hot-middleware/client') + const compiler = webpack(config) const app = express() -app.use(dev(compiler, {publicPath: config.output.publicPath})) -app.use(hot(compiler, {reload: true})) +const devOptions = { + publicPath: config.output.publicPath, + stats: { + colors: true + } +} + +const hotOptions = { + reload: true +} + +app.use(dev(compiler, devOptions)) +app.use(hot(compiler, hotOptions)) app.use(express.static(config.output.path)) app.listen(PORT, HOST, (error) => { diff --git a/src/app/action.js b/src/app/action.js index 3cf1b36..cb6926c 100644 --- a/src/app/action.js +++ b/src/app/action.js @@ -11,6 +11,7 @@ const action = module.exports = { DISCRETE_PAN: 'app:DISCRETE_PAN', ZOOM_TO: 'app:ZOOM_TO', TOGGLE_LAYER_SETTINGS: 'app:TOGGLE_LAYER_SETTINGS', + OPEN_ABOUT: 'app:OPEN_ABOUT', switchView(view) { return {type: action.SWITCH_VIEW, view} @@ -46,5 +47,9 @@ const action = module.exports = { toggleLayerSettings(id) { return {type: action.TOGGLE_LAYER_SETTINGS, id} + }, + + openAbout(open) { + return {type: action.OPEN_ABOUT, open} } } diff --git a/src/app/component/about.js b/src/app/component/about.js new file mode 100644 index 0000000..c38ad09 --- /dev/null +++ b/src/app/component/about.js @@ -0,0 +1,49 @@ +// about sliding modal component +'use strict' + +const {h} = require('deku') +const classnames = require('classnames') + +// grab content from repoisitory readme +const content = require('../../../README.md') + +const CloseButton = ({props}) => { + const {isOpen, onClick} = props + const classNames = classnames( + 'fixed top-1 right-2', + 'pa2', + 'app-bg-brand white link dim', + {dn: !isOpen}) + + return h('a', {href: '#', class: classNames, onClick}, [ + h('span', {class: 'fa fa-times mr2'}), + 'close' + ]) +} + +module.exports = function renderAbout({props}) { + const {isOpen, open} = props + const classNames = classnames( + 'fixed right-0 z-2', + 'w-100 h-100 shrink', + 'pointer', + (!isOpen) ? 'mw0' : 'mw-100') + + const handleClose = (event) => { + if (event.target !== event.currentTarget) { + return + } + + event.stopPropagation() + event.preventDefault() + + open(false) + } + + return h('div', {class: classNames, onClick: handleClose}, [ + h('div', {class: 'w7 h-100 fr bg-white pl3 overflow-auto cursor-auto'}, [ + h('div', {innerHTML: content}), + h(CloseButton, {isOpen, onClick: handleClose}) + ]) + ]) +} diff --git a/src/app/component/board-settings.js b/src/app/component/board-settings.js index bd515b1..7e548df 100644 --- a/src/app/component/board-settings.js +++ b/src/app/component/board-settings.js @@ -4,8 +4,8 @@ const {h, vnode} = require('deku') const Color = require('color') -const Select = require('../../input/select') -const Checkbox = require('../../input/checkbox') +const Select = require('./input/select') +const Checkbox = require('./input/checkbox') const COLOR_OPTIONS = [ { @@ -74,12 +74,12 @@ module.exports = function renderBoardSettings({props}) { return h(ColorSelect, {name, value, options, onChange}) }) - return h('div', {class: 'fx-0-0 w-100 clickable f5 pb2 bg-white-90'}, [ + return h('div', {class: 'flex-none w-100 click f5 lh-copy pb2 bg-white-90'}, [ + h('div', {}, colorSelects), h(Checkbox, { name: 'mask with outline', checked: board.maskWithOutline, onChange: handleSetMaskWithOutline() - }), - h('div', {}, colorSelects) + }) ]) } diff --git a/src/app/component/gerber-input.js b/src/app/component/gerber-input.js index 9bdb437..5c87a89 100644 --- a/src/app/component/gerber-input.js +++ b/src/app/component/gerber-input.js @@ -4,7 +4,7 @@ const {h} = require('deku') module.exports = function renderGerberInput({props, path}) { - return h('div', {class: 'bg-white-90 pt1 clickable fx-0-0'}, [ + return h('div', {class: 'bg-white-90 pt1 click flex-none'}, [ h('input', { id: path, type: 'file', @@ -15,7 +15,7 @@ module.exports = function renderGerberInput({props, path}) { h('label', { for: path, - class: 'pointer btn db w-auto tc border-box f3 bg-link dim bg-brand-2 near-white' + class: 'pointer db w-auto tc border-box f3 dim app-bg-brand near-white' }, ['+']) ]) } diff --git a/src/app/component/gerber-output.js b/src/app/component/gerber-output.js index 3fd7f06..13571ba 100644 --- a/src/app/component/gerber-output.js +++ b/src/app/component/gerber-output.js @@ -32,11 +32,11 @@ module.exports = function renderGerberOutput({props}) { }) }) - return h('output', {class: 'fx fx-d-c bg-white-90 clickable max-app-ht'}, [ + return h('output', {class: 'flex flex-column bg-white-90 click app-max-ht'}, [ h('ol', { - class: 'list ma0 ph0 pv2 fx-1-1 overflow-y-auto' + class: 'list ma0 ph0 pv2 flex-auto overflow-y-auto' }, children), - h('p', {class: 'ma0 pa1 tc fx-0-0'}, [`${children.length} files`]), + h('p', {class: 'ma0 pa1 tc flex-none'}, [`${children.length} files`]), h('svg', {class: 'clip'}, [ h(LayerDefs, {renders, units}) ]) diff --git a/src/app/component/gerber-settings.js b/src/app/component/gerber-settings.js index c16c608..ef83ce7 100644 --- a/src/app/component/gerber-settings.js +++ b/src/app/component/gerber-settings.js @@ -5,8 +5,8 @@ const {h} = require('deku') const {getAllTypes, getFullName} = require('whats-that-gerber') const classnames = require('classnames') -const Checkbox = require('../../input/checkbox') -const Select = require('../../input/select') +const Checkbox = require('./input/checkbox') +const Select = require('./input/select') const ALL_LAYER_OPTIONS = getAllTypes().map((type) => { return {value: type, title: getFullName(type)} @@ -29,10 +29,10 @@ const PlacesSelect = { } }) - return h('div', {class: 'ph2 striped--brand-light'}, [ - h('span', {class: 'fw9'}, [ - 'coordinate places', - h('span', {class: 'fa fa-angle-right mh1'}) + return h('div', {class: 'flex items-center ph2 striped--brand-light'}, [ + h('span', {class: 'flex items-center w-50 cf fw6'}, [ + h('span', {class: 'mr-auto'}, ['coordinate places']), + h('span', {class: 'fa fa-angle-right mh2'}) ]), numberInput('places[0]', places[0]), h('span', {class: 'fw9 mh2'}, ['.']), @@ -123,7 +123,7 @@ const ColorPicker = { const {color, onChange} = props return h('label', { - class: 'h2 w2 fx-0-0 btn pointer', + class: 'h2 w2 flex-none pointer', style: `background-color: ${color}` }, [ h('input', {onChange, id: path, type: 'color', value: color, class: 'clip'}) @@ -146,7 +146,7 @@ module.exports = function renderLayerDetailsItem({props}) { const {filename, color, layerType, isVisible, isRendering} = layer const name = getFullName(layerType) - const btnClass = 'pointer btn bn bg-transparent dim' + const btnClass = 'pointer bn bg-transparent dim' const btnDisabled = isRendering const visibilityIcon = classnames('fa', { @@ -154,16 +154,16 @@ module.exports = function renderLayerDetailsItem({props}) { 'fa-eye-slash': !isVisible }) - const settingsClass = classnames('collapsible', { - 'is-collapsed': !showSettings, - 'max-h5': showSettings + const settingsClass = classnames('shrink', { + 'mxht0': !showSettings, + 'mxht5': showSettings }) return h('li', {class: 'pv1'}, [ - h('div', {class: 'ph2 fx fx-ai-c ma0'}, [ + h('div', {class: 'ph2 flex items-center ma0'}, [ h(ColorPicker, {color, onChange: setColor}), h('span', {class: 'ml2 mr-auto'}, [ - h('p', {class: 'lh-title ma0 f6 b'}, [name]), + h('p', {class: 'lh-title ma0 f6 fw6'}, [name]), h('p', {class: 'lh-title ma0 f6'}, [filename]) ]), h('button', {class: btnClass, disabled: btnDisabled, onClick: toggleSettings}, [ diff --git a/src/input/checkbox.js b/src/app/component/input/checkbox.js similarity index 76% rename from src/input/checkbox.js rename to src/app/component/input/checkbox.js index 374879d..d8cd4db 100644 --- a/src/input/checkbox.js +++ b/src/app/component/input/checkbox.js @@ -7,8 +7,8 @@ module.exports = function renderCheckbox({props, path}) { const {name, checked, onChange} = props const handleChange = (event) => onChange(event.target.checked) - return h('label', {class: 'ph2 pointer db'}, [ + return h('label', {class: 'db ph2 lh-title pointer'}, [ h('input', {id: path, type: 'checkbox', checked, onChange: handleChange}), - h('span', {class: 'ml2 b'}, [name]) + h('span', {class: 'ml2 fw6'}, [name]) ]) } diff --git a/src/input/select.js b/src/app/component/input/select.js similarity index 61% rename from src/input/select.js rename to src/app/component/input/select.js index 4b68853..68f7528 100644 --- a/src/input/select.js +++ b/src/app/component/input/select.js @@ -15,18 +15,21 @@ module.exports = function renderSelect({props, path}) { const children = options.map((option) => { const optionProps = Object.assign({}, option, { - selected: option.value === value + selected: option.value === value, + class: 'normal' }) return h(Option, optionProps) }) - return h('label', {style, class: 'db ph2 pointer'}, [ - h('span', {class: 'b'}, [name]), - h('span', {class: 'fa fa-angle-right mh1'}), + return h('label', {style, class: 'flex items-center ph2 lh-title pointer'}, [ + h('span', {class: 'flex items-center w-50 fw6'}, [ + h('span', {class: 'mr-auto'}, [name]), + h('span', {class: 'fa fa-angle-right mh2'}) + ]), h('select', { id: path, - class: 'input-reset inherit-color bn bg-transparent pointe', + class: 'flex-none input-reset inherit-color bn bg-transparent pointer', onChange: handleChange }, children) ]) diff --git a/src/app/component/main.js b/src/app/component/main.js index 1adac3f..9694bef 100644 --- a/src/app/component/main.js +++ b/src/app/component/main.js @@ -5,6 +5,7 @@ const {h} = require('deku') const set = require('lodash.set') const Nav = require('./nav') +const About = require('./about') const GerberInput = require('./gerber-input') const GerberOutput = require('./gerber-output') const ViewSelect = require('./view-select') @@ -15,7 +16,8 @@ const appAction = require('../action') const { getSelectedView, getSelectedPanZoom, - getLayerDisplayStates + getLayerDisplayStates, + getAboutIsOpen } = require('../selector') const layerAction = require('../../layer/action') @@ -122,6 +124,9 @@ const handleSetMaskWithOutline = (dispatch) => () => (mask) => { dispatch(boardAction.maskWithOutline(mask)) } +const openAbout = (dispatch) => (open) => { + dispatch(appAction.openAbout(open)) +} module.exports = function renderMain({dispatch, context}) { const layers = getLayers(context) @@ -133,12 +138,19 @@ module.exports = function renderMain({dispatch, context}) { const selectedView = getSelectedView(context) const selectedPanZoom = getSelectedPanZoom(context) const totalViewbox = getTotalViewbox(context) + const aboutIsOpen = getAboutIsOpen(context) + const dipatchOpenAbout = openAbout(dispatch) + const windowAspect = context.browser.width / context.browser.height return h('div', {class: 'h-100 '}, [ - h(Nav, {}), + h(Nav, {openAbout: dipatchOpenAbout}), + + h(About, {isOpen: aboutIsOpen, open: dipatchOpenAbout}), - h('div', {class: 'w-25 mh3 mt3-past-h3 fixed right-0 max-app-ht z-1 fx fx-d-c'}, [ + h('div', { + class: 'fixed right-0 w-25 app-max-ht mh3 app-mt3-past-nav z-1 flex flex-column' + }, [ h(ViewSelect, {view: selectedView, switchView: switchView(dispatch)}), h(GerberOutput, { layers, @@ -161,7 +173,7 @@ module.exports = function renderMain({dispatch, context}) { h(GerberInput, {addGerber: addGerber(dispatch)}) ]), - h('div', {class: 'relative w-100 h-100 overflow-hidden bg-black-10'}, [ + h('div', {class: 'relative w-100 h-100 overflow-hidden app-bg'}, [ h(View, { view: selectedView, panZoom: selectedPanZoom, diff --git a/src/app/component/nav.js b/src/app/component/nav.js index 399fd13..0932ff5 100644 --- a/src/app/component/nav.js +++ b/src/app/component/nav.js @@ -3,17 +3,30 @@ const {h} = require('deku') -module.exports = function renderTopNav() { +module.exports = function renderTopNav({props}) { + const {openAbout} = props + + const handleAboutClick = (event) => { + event.preventDefault() + event.stopPropagation() + + openAbout(true) + } + return h('header', { - class: 'mb3 w-100 bg-white-90 brand-2 fx fx-ai-c fixed z-1' + class: 'mb3 w-100 bg-white-90 app-dark flex items-center fixed z-1' }, [ h('img', {src: '/logo.svg', class: 'h3 w3 pa2 border-box'}), h('span', {class: 'mr-auto'}, [ h('span', {class: 'f4 v-base'}, ['tracespace | ']), - h('span', {class: 'f4 fw2 v-base'}, ['viewer']) + h('span', {class: 'f4 fw3 v-base'}, ['viewer']) ]), h('nav', {class: 'fr lh-copy f4'}, [ - h('a', {href: '#', class: 'border-box pa3 no-underline link brand-2 dim'}, ['about']) + h('a', { + href: '#', + class: 'border-box pa3 no-underline link app-dark dim', + onClick: handleAboutClick + }, ['about']) ]) ]) } diff --git a/src/app/component/pan-zoom-controls.js b/src/app/component/pan-zoom-controls.js index c9b8b4c..e958511 100644 --- a/src/app/component/pan-zoom-controls.js +++ b/src/app/component/pan-zoom-controls.js @@ -37,7 +37,7 @@ const PanButton = { render({props}) { const {direction, onClick} = props const icon = PAN_BUTTON_ICONS[direction] - const className = 'dib bn btn pointer bg-transparent pa0 clickable w-1-3 h-100' + const className = 'dib bn pointer bg-transparent pa0 click w-1-3 h-100' return h('button', { class: className, @@ -49,7 +49,7 @@ const PanButton = { stroke: 'currentColor', 'stroke-linecap': 'square', 'stroke-width': 30, - class: 'w-100 h-100 brand-2 dim link' + class: 'w-100 h-100 app-dark dim link' }, icon) ]) } @@ -100,7 +100,7 @@ const ZoomBar = { return h('div', { onMouseDown: handleMouseEvent, onMouseMove: handleMouseEvent, - class: 'w-50 h-100 m-auto bg-white-90 clickable pointer grab' + class: 'w-50 h-100 m-auto bg-white-90 click pointer grab' }) } } @@ -121,10 +121,10 @@ const ZoomControl = { } } - return h('div', {class: 'w-100 h-100 relative'}, [ + return h('div', {class: 'w-100 h-100 relative dim-child'}, [ h(ZoomBar, {handleMouseEvent}), h('span', { - class: 'absolute w-100 border-box h1 left-0 bg-brand-2 dim ba b--white-80 grab', + class: 'absolute w-100 border-box h1 left-0 app-bg-dark dim ba b--white-80 child', style: `top: calc(${top}% - 0.5rem);`, role: 'slider', tabindex: 0, @@ -142,7 +142,7 @@ module.exports = function renderPanZoomControls({props}) { const {panZoom, handleFit, handleDiscretePan, handleZoomTo} = props return h('div', { - class: 'w-5 mh3 mt3-past-h3 fixed left-0 z-1 click-thru' + class: 'w-5 mh3 app-mt3-past-nav fixed left-0 z-1 click-none' }, [ h('div', {class: 'mb3'}, [ h(PanControl, {handleFit, handleDiscretePan}) diff --git a/src/app/component/view-select.js b/src/app/component/view-select.js index 84829f7..fa48d68 100644 --- a/src/app/component/view-select.js +++ b/src/app/component/view-select.js @@ -7,7 +7,7 @@ const classnames = require('classnames') const renderViewSelectButton = function({props}) { const {name, view, switchView} = props const isSelected = name === view - const classNames = classnames('btn dib w-50 f5 link brand-2 tc pv1', { + const classNames = classnames('dib w-50 f5 link app-dark tc pv1', { 'bg-black-20': !isSelected, dim: !isSelected, disabled: isSelected @@ -23,7 +23,7 @@ const renderViewSelectButton = function({props}) { module.exports = function renderViewSelect({props}) { const {view, switchView} = props - return h('div', {class: 'bg-white-90 clickable'}, [ + return h('div', {class: 'bg-white-90 click'}, [ h(renderViewSelectButton, {name: 'layers', view, switchView}), h(renderViewSelectButton, {name: 'board', view, switchView}) ]) diff --git a/src/app/reducer.js b/src/app/reducer.js index a6486f3..4707200 100644 --- a/src/app/reducer.js +++ b/src/app/reducer.js @@ -18,6 +18,7 @@ const NAME = 'app' const INITIAL_STATE = { view: 'layers', + aboutIsOpen: false, panZoom: { layers: {panStart: null, scale: 1, x: 0, y: 0}, board: {panStart: null, scale: 1, x: 0, y: 0} @@ -116,10 +117,18 @@ const view = function(state = INITIAL_STATE.view, action) { switch (action.type) { case appActionType.SWITCH_VIEW: return action.view + } + + return state +} - default: - return state +const aboutIsOpen = function(state = INITIAL_STATE.aboutIsOpen, action) { + switch (action.type) { + case appActionType.OPEN_ABOUT: + return action.open } + + return state } const panZoom = function(state = INITIAL_STATE.panZoom, action) { @@ -142,10 +151,9 @@ const panZoom = function(state = INITIAL_STATE.panZoom, action) { case appActionType.ZOOM_TO: return handleZoomTo(state, action) - - default: - return state } + + return state } const layers = function(state = INITIAL_STATE.layers, action) { @@ -162,11 +170,10 @@ const layers = function(state = INITIAL_STATE.layers, action) { return Object.assign({}, state, { [action.id]: {showSettings: !state[action.id].showSettings} }) - - default: - return state } + + return state } -module.exports = combineReducers({view, panZoom, layers}) +module.exports = combineReducers({view, aboutIsOpen, panZoom, layers}) module.exports.NAME = NAME diff --git a/src/app/selector.js b/src/app/selector.js index 6612883..41d0e26 100644 --- a/src/app/selector.js +++ b/src/app/selector.js @@ -8,14 +8,7 @@ const {NAME} = require('./reducer') const getPanZoom = (state) => state[NAME].panZoom const getLayerDisplayStates = (state) => state[NAME].layers const getSelectedView = (state) => state[NAME].view - -const getWindowSize = createSelector( - getPanZoom, - (panZoom) => panZoom.windowSize) - -const getWindowAspect = createSelector( - getWindowSize, - (size) => size.width / size.height) +const getAboutIsOpen = (state) => state[NAME].aboutIsOpen const getSelectedPanZoom = createSelector( getSelectedView, @@ -26,5 +19,5 @@ module.exports = { getLayerDisplayStates, getSelectedView, getSelectedPanZoom, - getWindowAspect + getAboutIsOpen } diff --git a/src/app/style.css b/src/app/style.css new file mode 100644 index 0000000..bc6fbde --- /dev/null +++ b/src/app/style.css @@ -0,0 +1,45 @@ +/* app specific style */ + +:root { + --background-color: #ddd; + --brand-color: #3cc; + --dark-color: #333; + --accent-color: #F18F01; +} + +/* app window height minus nav-bar and margins */ +.app-ht { + height: calc(100% - 6rem); +} + +.app-max-ht { + max-height: calc(100% - 6rem); +} + +/* margin past the nav */ + +.app-mt3-past-nav { + margin-top: 5rem; +} + +/* app colors */ + +.app-bg { + background-color: var(--background-color); +} + +.app-brand { + color: var(--brand-color); +} + +.app-bg-brand { + background-color: var(--brand-color); +} + +.app-dark { + color: var(--dark-color); +} + +.app-bg-dark { + background-color: var(--dark-color); +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..7555535 --- /dev/null +++ b/src/index.css @@ -0,0 +1,152 @@ +/* css entry point */ + +/* google fonts */ + +@import 'https://fonts.googleapis.com/css?family=Open+Sans:300,400,600'; + +/* tachyons for base css */ + +@import 'tachyons'; +@import 'tachyons-z-index'; + +/* application modules */ + +@import './app/style'; +@import './layer/style'; + +/* normalizations */ + +body { + font-family: 'Open Sans', Helvetica, sans-serif; +} + +[disabled], +.disabled { + opacity: 0.75; + cursor: default; + pointer-events: none; +} + +/* cursors */ + +.cursor-auto { + cursor: auto; +} + +.grab { + cursor: move; + cursor: grab; + cursor: -moz-grab; + cursor: -webkit-grab; +} + +.grab:active { + cursor: grabbing; + cursor: -moz-grabbing; + cursor: -webkit-grabbing; +} + +/* pointer events */ + +.click { + pointer-events: auto; +} + +.click-none { + pointer-events: none; +} + +/* positioning */ + +.transform-center { + transform: translate(-50%, -50%); +} + +.left-50 { + left: 50%; +} + +.top-50 { + top: 50%; +} + +/* sizing */ + +.h-1-3 { + height: calc(100% / 3); +} + +.w-5 { + width: 5%; +} + +.w-47-5 { + width: 47.5%; +} + +.w-1-3 { + width: calc(100% / 3); +} + +.w7 { + width: 48rem; +} + +.mw0 { + max-width: 0; +} + +.mxht5 { + max-height: 16rem; +} + +.mxht0 { + max-height: 0; +} + +.shrink-width { + max-height: 0; +} + +.shrink { + transition: max-height 0.3s linear, max-width 0.3s linear; + overflow: hidden; +} + +.aspect-ratio--1x1 { + padding-bottom: 100%; +} + +/* spacing */ + +.m-auto { + margin: auto; +} + +.mh-auto { + margin-left: auto; + margin-right: auto; +} + +.mr-auto { + margin-right: auto; +} + +/* colors */ + +.inherit-color { + color: inherit; +} + +.dim-child .child { + opacity: 1; transition: opacity .15s ease-in; +} + +.dim-child:hover .child, +.dim-child:focus .child { + opacity: .5; transition: opacity .15s ease-in; +} + +.dim-child:active .child { + opacity: .8; transition: opacity .15s ease-out; +} diff --git a/src/index.js b/src/index.js index 8fa9321..b8c5a13 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -// tracespace viewer +// tracespace viewer entry point 'use strict' const {createStore, combineReducers, applyMiddleware, compose} = require('redux') @@ -8,12 +8,16 @@ const throttle = require('redux-throttle') const {responsiveStoreEnhancer, responsiveStateReducer} = require('redux-responsive') const raf = require('raf') +// load application const appReducer = require('./app/reducer') const boardReducer = require('./board/reducer') const layerReducer = require('./layer/reducer') const layerMiddleware = require('./layer/middleware') const converter = require('./converter') +// load css +require('./index.css') + const reducer = combineReducers({ browser: responsiveStateReducer, [appReducer.NAME]: appReducer, diff --git a/src/layer/component.js b/src/layer/component.js index 842f396..16a79ed 100644 --- a/src/layer/component.js +++ b/src/layer/component.js @@ -59,7 +59,7 @@ const component = module.exports = { : 0 return h('div', { - class: 'relative w-100 h0', + class: 'w-100 aspect-ratio', style: `padding-bottom: ${padding}` }, [ renderLayer(model, {id: path, class: 'absolute'}, h, false) diff --git a/src/layer/style.css b/src/layer/style.css new file mode 100644 index 0000000..c0870c3 --- /dev/null +++ b/src/layer/style.css @@ -0,0 +1,5 @@ +/* layer module specific styles */ + +.layer-opacity { + opacity: 0.5; +} diff --git a/test/app/app-action_test.js b/test/app/app-action_test.js index a4f1e50..f3e0e95 100644 --- a/test/app/app-action_test.js +++ b/test/app/app-action_test.js @@ -84,3 +84,11 @@ test('it should be able to toggle the settings drawer flag', (t) => { t.deepEqual(result, {type: action.TOGGLE_LAYER_SETTINGS, id: 'foo'}) }) + +test('it should be able to open the about drawer', (t) => { + const open = action.openAbout(true) + const close = action.openAbout(false) + + t.deepEqual(open, {type: action.OPEN_ABOUT, open: true}) + t.deepEqual(close, {type: action.OPEN_ABOUT, open: false}) +}) diff --git a/test/app/app-reducer_test.js b/test/app/app-reducer_test.js index 4329ab4..d7ccabe 100644 --- a/test/app/app-reducer_test.js +++ b/test/app/app-reducer_test.js @@ -14,6 +14,7 @@ const { const EXPECTED_INITIAL_STATE = { view: 'layers', + aboutIsOpen: false, panZoom: { layers: {panStart: null, scale: 1, x: 0, y: 0}, board: {panStart: null, scale: 1, x: 0, y: 0} @@ -23,6 +24,7 @@ const EXPECTED_INITIAL_STATE = { const PAN_ZOOM_TEST_STATE = { view: 'layers', + aboutIsOpen: false, panZoom: { layers: {panStart: null, scale: 0.5, x: 0.5, y: 0.5}, board: {panStart: null, scale: 0.5, x: 0.5, y: 0.5} @@ -252,3 +254,15 @@ test('it shoud be able to handle a TOGGLE_LAYER_SETTINGS action', (t) => { state = reducer(state, toggleAction) t.deepEqual(state.layers, {foo: {showSettings: false}}) }) + +test('should be able to handle a OPEN_ABOUT action', (t) => { + const openAbout = {type: action.OPEN_ABOUT, open: true} + const closeAbout = {type: action.OPEN_ABOUT, open: false} + let state = EXPECTED_INITIAL_STATE + + state = reducer(state, openAbout) + t.true(state.aboutIsOpen) + + state = reducer(state, closeAbout) + t.false(state.aboutIsOpen) +}) diff --git a/test/app/app-selector_test.js b/test/app/app-selector_test.js index ee59e20..662b270 100644 --- a/test/app/app-selector_test.js +++ b/test/app/app-selector_test.js @@ -9,7 +9,7 @@ const selector = require('../../src/app/selector') const SELECTOR_TEST_STATE_LAYERS = { [reducer.NAME]: { view: 'layers', - windowSize: {width: 1024, height: 768}, + aboutIsOpen: false, panZoom: { windowSize: {width: 1024, height: 768}, layers: {panStart: null, scale: 0.1, x: 0.2, y: 0.3}, @@ -25,6 +25,7 @@ const SELECTOR_TEST_STATE_LAYERS = { const SELECTOR_TEST_STATE_BOARD = { [reducer.NAME]: { view: 'board', + aboutIsOpen: false, panZoom: { windowSize: {width: 1024, height: 768}, layers: {panStart: null, scale: 0.1, x: 0.2, y: 0.3}, @@ -61,8 +62,8 @@ test('it should be able to get the selected view pan zoom', (t) => { t.deepEqual(panZoomBoard, {panStart: null, scale: 0.4, x: 0.6, y: 0.7}) }) -test('it should be able to get the window aspect ratio', (t) => { - const aspect = selector.getWindowAspect(SELECTOR_TEST_STATE_LAYERS) +test('it should be able to get the about open state', (t) => { + const aboutIsOpen = selector.getAboutIsOpen(SELECTOR_TEST_STATE_LAYERS) - t.is(aspect, 1024 / 768) + t.false(aboutIsOpen) }) diff --git a/test/layer/layer-component_test.js b/test/layer/layer-component_test.js index 0fab257..948ccf2 100644 --- a/test/layer/layer-component_test.js +++ b/test/layer/layer-component_test.js @@ -88,7 +88,7 @@ test('it should have a Layers component that wraps Layer elements', (t) => { // ensure the element has the proper aspect ratio t.is(element.getAttribute('style'), 'padding-bottom: 75%') - hasClass(t, element, 'relative', 'w-100', 'relative') + hasClass(t, element, 'aspect-ratio', 'w-100') }) test('it should handle an empty viewbox', (t) => { diff --git a/webpack.config.js b/webpack.config.js index cc231da..aff5908 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,47 +1,15 @@ 'use strict' -const webpack = require('webpack') -const path = require('path') +const {create, loader, plugin} = require('./config') -module.exports = { - devtool: '#source-map', - entry: [ - 'webpack-hot-middleware/client', - './src/index.js' - ], - output: { - path: path.join(__dirname, 'public'), - filename: 'bundle.js', - library: 'app', - publicPath: '/' - }, - plugins: [ - new webpack.optimize.OccurrenceOrderPlugin(), - new webpack.HotModuleReplacementPlugin(), - new webpack.NoErrorsPlugin(), - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': '"development"' - }) - ], - module: { - loaders: [ - { - test: /\worker\.js$/, - exclude: /node_modules/, - loader: 'worker' - }, - { - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel', - query: { - presets: ['es2040'] - } - }, - { - test: /\.css$/, - loader: 'style-loader!css-loader' - } - ] - } -} +const plugins = [ + plugin.prodEnv, + plugin.occurrence, + plugin.dedupe, + plugin.uglifyJs, + plugin.extractCss +] + +const loaders = [loader.worker, loader.babel, loader.cssExtracted, loader.markdown] + +module.exports = create(plugins, loaders)