├── src ├── styles │ ├── components │ │ ├── Header.styl │ │ ├── Nav.styl │ │ └── ComponentName.styl │ ├── base │ │ ├── reset.styl │ │ └── normalize.styl │ ├── theme │ │ ├── fonts.styl │ │ └── variables.styl │ └── app.styl └── app │ ├── config.js │ ├── containers │ ├── About.js │ ├── App.js │ └── Layout.js │ ├── reducers │ ├── index.js │ └── counter.js │ ├── routes.js │ ├── test │ ├── reducers │ │ └── counter-test.js │ ├── actions │ │ └── CounterActions.test.js │ └── components │ │ └── Counter-test.js │ ├── api │ └── Counter.js │ ├── client.js │ ├── store │ └── configureStore.js │ ├── actions │ └── Counter.js │ ├── components │ └── Counter.js │ ├── utils │ └── createDevToolsWindow.js │ └── server.js ├── docs └── header_universaljs.jpg ├── tasks ├── gulp-clean.js ├── gulp-lint-css.js ├── gulp-build-css.js ├── gulp-lint-js.js ├── cfg │ ├── gulp-config.js │ ├── build-webpack-config.js │ └── dev-webpack-config.js └── gulp-build-js.js ├── .jscsrc ├── webpack.config.js ├── .gitignore ├── .babelrc ├── .editorconfig ├── .stylintrc ├── gulpfile.babel.js ├── LICENSE ├── .eslintrc ├── README.md └── package.json /src/styles/components/Header.styl: -------------------------------------------------------------------------------- 1 | header 2 | padding 10 3 | text-align center 4 | -------------------------------------------------------------------------------- /src/styles/components/Nav.styl: -------------------------------------------------------------------------------- 1 | nav ul 2 | list-style none 3 | text-align center 4 | -------------------------------------------------------------------------------- /src/styles/base/reset.styl: -------------------------------------------------------------------------------- 1 | h1, 2 | h2, 3 | h3, 4 | h4, 5 | h5, 6 | h6 7 | margin 0 8 | padding 0 9 | -------------------------------------------------------------------------------- /docs/header_universaljs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosazaustre/universal-js-boilerplate/HEAD/docs/header_universaljs.jpg -------------------------------------------------------------------------------- /src/styles/components/ComponentName.styl: -------------------------------------------------------------------------------- 1 | .ComponentName 2 | background-color $white_base 3 | border 1px solid $gray_darker 4 | margin 2em 5 | padding 2em 6 | 7 | &-title 8 | color $gray_darker 9 | -------------------------------------------------------------------------------- /src/app/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | app: { 3 | name: 'Universal JS Boilerplate', 4 | port: process.env.PORT || 3000 5 | }, 6 | api: 'http://localhost:3001' 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /tasks/gulp-clean.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import del from 'del'; 3 | import config from './cfg/gulp-config'; 4 | 5 | export default () => { 6 | return del([ 7 | config.output + 'app.js', 8 | config.output + 'app.css' 9 | ]); 10 | }; 11 | -------------------------------------------------------------------------------- /src/app/containers/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class About extends React.Component { 4 | 5 | render() { 6 | return ( 7 |
8 |

About View

9 |
10 | ); 11 | } 12 | 13 | } 14 | 15 | export default About; 16 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "validateLineBreaks": "LF", 5 | "requireTrailingComma": false, 6 | "requireCapitalizedConstructors": false, 7 | "esnext": true, 8 | "excludeFiles": ["build/**", "node_modules/**"] 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/theme/fonts.styl: -------------------------------------------------------------------------------- 1 | @font-face 2 | font-family 'Lato' 3 | font-style normal 4 | font-weight 400 5 | src local('Lato Normal'), 6 | local('Lato-Normal'), 7 | url('https://themes.googleusercontent.com/static/fonts/lato/v6/9k-RPmcnxYEPm8CNFsH2gg.woff') 8 | format('woff') 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var devConfig = require('./tasks/cfg/dev-webpack-config'); 2 | var buildConfig = require('./tasks/cfg/build-webpack-config'); 3 | 4 | const TARGET = process.env.TARGET; 5 | 6 | if (TARGET === 'build') { 7 | module.exports = buildConfig; 8 | } else { 9 | module.exports = devConfig; 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: 3 | # https://help.github.com/articles/ignoring-files 4 | 5 | node_modules 6 | npm-debug.log 7 | 8 | # hide builded files on repo 9 | build/public/* 10 | !build/.gitkeep 11 | !build/public/favicon.ico 12 | 13 | src/app_/* 14 | App_ 15 | -------------------------------------------------------------------------------- /tasks/gulp-lint-css.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import stylint from 'gulp-stylint'; 3 | import config from './cfg/gulp-config'; 4 | 5 | const configStylint = { 6 | config: '.stylintrc' 7 | }; 8 | 9 | export default () => { 10 | return gulp 11 | .src(config.styles.input) 12 | .pipe(stylint(configStylint)); 13 | }; 14 | -------------------------------------------------------------------------------- /src/app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reduxReactRouter, routerStateReducer, ReduxRouter } from 'redux-router'; 3 | import counter from './counter'; 4 | import routes from '../routes'; 5 | 6 | const rootReducer = combineReducers({ 7 | counter, 8 | router: routerStateReducer 9 | }); 10 | 11 | export default rootReducer; 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | "react-transform" 7 | ], 8 | "extra": { 9 | "react-transform": { 10 | "transforms": [{ 11 | "transform": "react-transform-hmr", 12 | "imports": ["react"], 13 | "locals": ["module"] 14 | }] 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { SET_COUNTER, INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions/counter'; 2 | 3 | export default function counter(state = 0, action) { 4 | switch (action.type) { 5 | case SET_COUNTER: 6 | return action.payload; 7 | case INCREMENT_COUNTER: 8 | return state + 1; 9 | case DECREMENT_COUNTER: 10 | return state - 1; 11 | default: 12 | return state; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, DefaultRoute } from 'react-router'; 3 | import App from './containers/App'; 4 | import About from './containers/About'; 5 | 6 | const routes = ` 7 | 8 | 9 | 10 | 11 | 12 | `; 13 | 14 | export default routes; 15 | -------------------------------------------------------------------------------- /src/app/test/reducers/counter-test.js: -------------------------------------------------------------------------------- 1 | import counter from '../../reducers/counter'; 2 | import { increment, decrement } from '../../actions/CounterActions'; 3 | import { expect } from 'chai'; 4 | 5 | describe('counter-reducer', () => { 6 | 7 | it('increments', () => { 8 | expect(counter(10, increment())).to.equal(11); 9 | }); 10 | 11 | it('decrements', () => { 12 | expect(counter(10, decrement())).to.equal(9); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /src/app/api/Counter.js: -------------------------------------------------------------------------------- 1 | function getRandomInt(min, max) { 2 | return Math.floor(Math.random() * (max - min)) + min; 3 | } 4 | 5 | export function fetchCounter(callback) { 6 | // Rather than immediately returning, we delay our code with a timeout to simulate asynchronous behavior 7 | setTimeout(() => { 8 | callback(getRandomInt(1, 100)); 9 | }, 500); 10 | 11 | // In the case of a real world API call, you'll normally run into a Promise like this: 12 | // API.getUser().then(user => callback(user)); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/containers/App.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import Counter from '../components/Counter'; 4 | import * as CounterActions from '../actions/counter'; 5 | 6 | function mapStateToProps(state) { 7 | return { 8 | counter: state.counter 9 | }; 10 | } 11 | 12 | function mapDispatchToProps(dispatch) { 13 | return bindActionCreators(CounterActions, dispatch); 14 | } 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(Counter); 17 | -------------------------------------------------------------------------------- /tasks/gulp-build-css.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import stylus from 'gulp-stylus'; 3 | import nib from 'nib'; 4 | import minifyCSS from 'gulp-minify-css'; 5 | import config from './cfg/gulp-config'; 6 | 7 | const configStylus = { 8 | use: nib(), 9 | 'include css': true 10 | }; 11 | 12 | export default () => { 13 | return gulp 14 | .src(config.styles.main) 15 | .pipe(stylus(configStylus)) 16 | .pipe(minifyCSS()) 17 | .pipe(gulp.dest(config.output)); 18 | }; 19 | -------------------------------------------------------------------------------- /tasks/gulp-lint-js.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import eslint from 'gulp-eslint'; 3 | import jscs from 'gulp-jscs'; 4 | import config from './cfg/gulp-config'; 5 | 6 | const configEslint = { 7 | configFile: '.eslintrc' 8 | }; 9 | 10 | export default () => { 11 | return gulp 12 | .src(config.scripts.input) 13 | .pipe(jscs()) 14 | .pipe(jscs.reporter()) 15 | .pipe(jscs.reporter('console')) 16 | .pipe(eslint(configEslint)) 17 | .pipe(eslint.format()) 18 | .pipe(eslint.failOnError()); 19 | }; 20 | -------------------------------------------------------------------------------- /tasks/cfg/gulp-config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | scripts: { 3 | client: './src/app/client.js', 4 | input: [ 5 | './src/app/**/*.js', 6 | './src/app/**/*.jsx' 7 | ], 8 | watch: [ 9 | './src/app/server.js', 10 | './src/app/client.js', 11 | './src/app/**/*.jsx', 12 | './src/app/routes.js' 13 | ] 14 | }, 15 | styles: { 16 | main: './src/styles/app.styl', 17 | input: [ 18 | './src/styles/**/*.styl', 19 | '!./src/styles/base/normalize.styl' 20 | ], 21 | watch: [ 22 | './src/styles/**/*.styl' 23 | ] 24 | }, 25 | output: './build/public' 26 | }; 27 | -------------------------------------------------------------------------------- /tasks/gulp-build-js.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import babelify from 'babelify'; 3 | import browserify from 'browserify'; 4 | import source from 'vinyl-source-stream'; 5 | import buffer from 'vinyl-buffer'; 6 | import config from './cfg/gulp-config'; 7 | 8 | const configBrowserify = { 9 | entries: config.scripts.client, 10 | debug: true, 11 | extensions: ['.js', '.jsx'], 12 | transform: babelify 13 | }; 14 | 15 | export default () => { 16 | return browserify(configBrowserify) 17 | .bundle() 18 | .pipe(source('app.js')) 19 | .pipe(buffer()) 20 | .pipe(gulp.dest(config.output)); 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Link } from 'react-router'; 5 | import configureStore from './store/configureStore'; 6 | import App from './containers/App'; 7 | 8 | import routes from './routes'; 9 | 10 | const initialState = window.__INITIAL_STATE__; 11 | const store = configureStore(initialState); 12 | const rootElement = document.getElementById('app'); 13 | 14 | render( 15 | 16 | 17 | 23 | , 24 | rootElement 25 | ); 26 | -------------------------------------------------------------------------------- /src/styles/theme/variables.styl: -------------------------------------------------------------------------------- 1 | // -- Colors ------------------------------------------------------------------- 2 | 3 | $white_base = #fafafa 4 | 5 | $gray_base = #222 6 | $gray_darker = $gray_base + 15% 7 | $gray_dark = $gray_base + 25% 8 | $gray = $gray_base + 35% 9 | $gray_light = $gray_base + 50% 10 | $gray_lighter = $gray_base + 90% 11 | 12 | // -- Typography --------------------------------------------------------------- 13 | 14 | $font_family_base = 'Lato', 'Helvetica-Neue', sans-serif 15 | 16 | // -- Layout ------------------------------------------------------------------- 17 | 18 | $max_content-width = 1000px 19 | 20 | $screen_mobile = 480px 21 | $screen_tablet = 768px 22 | $screen_desktop = 992px 23 | $screen_wide = 1200px 24 | -------------------------------------------------------------------------------- /src/app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | import { reduxReactRouter, routerStateReducer, ReduxRouter } from 'redux-router'; 5 | import routes from './../routes'; 6 | 7 | const createStoreWithMiddleware = applyMiddleware( 8 | thunk 9 | )(createStore); 10 | 11 | export default function configureStore(initialState) { 12 | const store = createStoreWithMiddleware(rootReducer, initialState); 13 | 14 | if (module.hot) { 15 | // Enable Webpack hot module replacement for reducers 16 | module.hot.accept('../reducers', () => { 17 | const nextRootReducer = require('../reducers'); 18 | store.replaceReducer(nextRootReducer); 19 | }); 20 | } 21 | 22 | return store; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/containers/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Layout extends React.Component { 4 | 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | 11 | { this.props.title } 12 | 13 | 14 | 15 | 16 | 17 | 18 | { this.props.children } 19 | 20 | 21 | 22 | ); 23 | } 24 | } 25 | 26 | export default Layout; 27 | -------------------------------------------------------------------------------- /src/styles/app.styl: -------------------------------------------------------------------------------- 1 | /* 2 | * Universal JS Boilerplate - Styles 3 | * Copyright (c) Carlos Azaustre, carlosazaustre.es 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | // -- Base and Reset styles ---------------------------------------------------- 10 | 11 | @import './base/normalize.styl' 12 | @import './base/reset.styl' 13 | 14 | // -- Theme Styles ------------------------------------------------------------- 15 | 16 | @import './theme/fonts.styl' 17 | @import './theme/variables.styl' 18 | 19 | // -- Main styles -------------------------------------------------------------- 20 | 21 | body 22 | background-color $gray_light 23 | font-family $font_family_base 24 | font-size 16px 25 | 26 | // -- Component Styles --------------------------------------------------------- 27 | 28 | @import './components/*' 29 | -------------------------------------------------------------------------------- /src/app/actions/Counter.js: -------------------------------------------------------------------------------- 1 | export const SET_COUNTER = 'SET_COUNTER'; 2 | export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; 3 | export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; 4 | 5 | export function set(value) { 6 | return { 7 | type: SET_COUNTER, 8 | payload: value 9 | }; 10 | } 11 | 12 | export function increment() { 13 | return { 14 | type: INCREMENT_COUNTER 15 | }; 16 | } 17 | 18 | export function decrement() { 19 | return { 20 | type: DECREMENT_COUNTER 21 | }; 22 | } 23 | 24 | export function incrementIfOdd() { 25 | return (dispatch, getState) => { 26 | const { counter } = getState(); 27 | 28 | if (counter % 2 === 0) { 29 | return; 30 | } 31 | 32 | dispatch(increment()); 33 | }; 34 | } 35 | 36 | export function incrementAsync(delay = 1000) { 37 | return dispatch => { 38 | setTimeout(() => { 39 | dispatch(increment()); 40 | }, delay); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/app/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | export default class Counter extends Component { 4 | 5 | static propTypes = { 6 | increment: PropTypes.func.isRequired, 7 | incrementIfOdd: PropTypes.func.isRequired, 8 | incrementAsync: PropTypes.func.isRequired, 9 | decrement: PropTypes.func.isRequired, 10 | counter: PropTypes.number.isRequired 11 | } 12 | 13 | render() { 14 | const { increment, incrementIfOdd, incrementAsync, decrement, counter } = this.props; 15 | return ( 16 |
17 | Clicked: {counter} times 18 | {' '} 19 | 20 | {' '} 21 | 22 | {' '} 23 | 24 | {' '} 25 | 26 |
27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/utils/createDevToolsWindow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { DevTools, DebugPanel, LogMonitor } from 'redux-devtools/lib/react'; 4 | 5 | export default function createDevToolsWindow(store) { 6 | // Window name. 7 | const name = 'Redux DevTools'; 8 | 9 | // Give it a name so it reuses the same window. 10 | const win = window.open( 11 | null, 12 | name, 13 | 'menubar=no,location=no,resizable=yes,scrollbars=no,status=no' 14 | ); 15 | 16 | // Reload in case it's reusing the same window with the old content. 17 | win.location.reload(); 18 | 19 | // Set visible Window title. 20 | win.document.title = name; 21 | 22 | // Wait a little bit for it to reload, then render. 23 | setTimeout(() => render( 24 | 25 | 26 | , 27 | win.document.body.appendChild(document.createElement('div')) 28 | ), 10); 29 | } 30 | -------------------------------------------------------------------------------- /.stylintrc: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": false, 3 | "brackets": "never", 4 | "colons": "never", 5 | "colors": "always", 6 | "commaSpace": "always", 7 | "commentSpace": "always", 8 | "cssLiteral": "never", 9 | "depthLimit": false, 10 | "duplicates": true, 11 | "efficient": "always", 12 | "extendPref": false, 13 | "globalDupe": false, 14 | "indentPref": false, 15 | "leadingZero": "never", 16 | "maxErrors": false, 17 | "maxWarnings": false, 18 | "mixed": false, 19 | "namingConvention": false, 20 | "namingConventionStrict": false, 21 | "none": "never", 22 | "noImportant": true, 23 | "parenSpace": false, 24 | "placeholders": "always", 25 | "prefixVarsWithDollar": "always", 26 | "quotePref": false, 27 | "reporter": "../core/reporter", 28 | "semicolons": "never", 29 | "sortOrder": "alphabetical", 30 | "stackedProperties": "never", 31 | "trailingWhitespace": "never", 32 | "universal": false, 33 | "valid": true, 34 | "zeroUnits": "never", 35 | "zIndexNormalize": false 36 | } 37 | -------------------------------------------------------------------------------- /src/app/test/actions/CounterActions.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import { incrementIfOdd, increment, incrementAsync } from '../../actions/CounterActions'; 3 | import { spy, useFakeTimers } from 'sinon'; 4 | import chai, { expect } from 'chai'; 5 | import sinonChai from 'sinon-chai'; 6 | 7 | chai.use(sinonChai); 8 | 9 | describe('counter actions', () => { 10 | 11 | it('dispatch increment if odd', () => { 12 | let getState = () => { 13 | return { counter:3 }; 14 | }; 15 | 16 | let dispatch = spy(); 17 | incrementIfOdd()(dispatch, getState); 18 | expect(dispatch).to.have.been.calledWith(increment()); 19 | }); 20 | 21 | it('increment async calls increment after one sec', () => { 22 | let dispatch = spy(); 23 | let clock = useFakeTimers(); 24 | incrementAsync()(dispatch); 25 | clock.tick(999); 26 | expect(dispatch).not.to.have.been.called; 27 | clock.tick(1); 28 | expect(dispatch).to.have.been.calledWith(increment()); 29 | clock.restore(); 30 | 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tasks/cfg/build-webpack-config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | var ROOT_PATH = path.join(__dirname, '..', '..'); 6 | 7 | module.exports = { 8 | entry: [ 9 | path.resolve(ROOT_PATH, 'src/app/client.js'), 10 | path.resolve(ROOT_PATH, 'src/styles/app.styl') 11 | ], 12 | output: { 13 | path: path.resolve(ROOT_PATH, '../build/public'), 14 | filename: 'app.js', 15 | publicPath: '/build/public' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.HotModuleReplacementPlugin(), 20 | new webpack.NoErrorsPlugin() 21 | ], 22 | module: { 23 | loaders: [{ 24 | test: /\.js$|\.jsx$/, 25 | loader: 'babel' 26 | }, 27 | { 28 | test: /\.styl$/, 29 | exclude: /node_modules/, 30 | loader: 'css-loader!stylus-loader?paths=node_modules/bootstrap-stylus/stylus/' 31 | }, 32 | { 33 | test: /\.json$/, 34 | loader: 'json-loader' 35 | }] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /tasks/cfg/dev-webpack-config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | var ROOT_PATH = path.join(__dirname, '..', '..'); 5 | 6 | module.exports = { 7 | devtool: 'inline-source-map', 8 | entry: [ 9 | 'webpack-hot-middleware/client', 10 | path.resolve(ROOT_PATH, 'src/app/client.js'), 11 | path.resolve(ROOT_PATH, 'src/styles/app.styl') 12 | ], 13 | output: { 14 | path: path.resolve(ROOT_PATH, '../build/public'), 15 | filename: 'app.js', 16 | publicPath: '/build/public/' 17 | }, 18 | plugins: [ 19 | new webpack.optimize.OccurenceOrderPlugin(), 20 | new webpack.HotModuleReplacementPlugin(), 21 | new webpack.NoErrorsPlugin() 22 | ], 23 | module: { 24 | loaders: [{ 25 | test: /\.js$|\.jsx$/, 26 | loader: 'babel' 27 | }, 28 | { 29 | test: /\.styl$/, 30 | exclude: /node_modules/, 31 | loader: 'css-loader!stylus-loader?paths=node_modules/bootstrap-stylus/stylus/' 32 | }, 33 | { 34 | test: /\.json$/, 35 | loader: 'json-loader' 36 | }] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Universal JS Boilerplate 3 | * Copyright (c) Carlos Azaustre, carlosazaustre.es 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | import gulp from 'gulp'; 10 | import linterJS from './tasks/gulp-lint-js'; 11 | import linterCSS from './tasks/gulp-lint-css'; 12 | import buildJS from './tasks/gulp-build-js'; 13 | import buildCSS from './tasks/gulp-build-css'; 14 | 15 | import clean from './tasks/gulp-clean'; 16 | 17 | import config from './tasks/cfg/gulp-config'; 18 | 19 | gulp.task('eslint', linterJS); 20 | gulp.task('stylint', linterCSS); 21 | 22 | gulp.task('build:js', ['eslint'], buildJS); 23 | gulp.task('build:css', ['stylint'], buildCSS); 24 | 25 | gulp.task('clean', clean); 26 | 27 | gulp.task('default', ['clean', 'build', 'watch']); 28 | gulp.task('build', ['clean', 'build:js', 'build:css']); 29 | 30 | gulp.task('watch', () => { 31 | gulp.watch(config.scripts.watch, ['build:js']); 32 | gulp.watch(config.styles.watch, ['build:css']); 33 | }); 34 | -------------------------------------------------------------------------------- /src/app/test/components/Counter-test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import testdom from 'testdom'; 3 | testdom(''); 4 | 5 | import Counter from '../../components/Counter'; 6 | import React from 'react'; 7 | import ReactAddons from 'react/addons'; 8 | import sinon from 'sinon'; 9 | import chai, { expect } from 'chai'; 10 | import sinonChai from 'sinon-chai'; 11 | chai.use(sinonChai); 12 | 13 | const TestUtils = React.addons.TestUtils; 14 | 15 | describe('Counter component', () => { 16 | 17 | let increment = sinon.spy(); 18 | let incrementIfOdd = sinon.spy(); 19 | let decrement = sinon.spy(); 20 | let counterProps = { 21 | increment: increment, 22 | incrementIfOdd: incrementIfOdd, 23 | decrement: decrement, 24 | counter: 10 25 | }; 26 | let counter = TestUtils.renderIntoDocument(); 27 | 28 | it('increment click', () => { 29 | let buttons = TestUtils.scryRenderedDOMComponentsWithTag(counter, 'button'); 30 | TestUtils.Simulate.click(buttons[0]); 31 | expect(increment).to.have.been.called; 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Carlos Azaustre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "plugins": [ 5 | "react" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "es6": true, 11 | "mocha": true 12 | }, 13 | "globals": { 14 | "__DEV__": true, 15 | "__SERVER__": true, 16 | "describe": true, 17 | "it": true 18 | }, 19 | "ecmaFeatures": { 20 | "jsx": true, 21 | "arrowFunctions": true, 22 | "blockBindings": true, 23 | "modules": true, 24 | "classes": true 25 | }, 26 | "rules": { 27 | "valid-jsdoc": 2, 28 | "indent": [2, 2], 29 | "quotes": [2, "single", "avoid-escape"], 30 | "no-multi-spaces": 0, 31 | "key-spacing": 0, 32 | "strict": [2, "never"], 33 | "comma-dangle": [2, "never"], 34 | "no-shadow": [0], 35 | "no-use-before-define": [2, "nofunc"], 36 | "new-cap": [0], 37 | "object-curly-spacing": [2, "always"], 38 | "no-undef": 2, 39 | "prefer-const": 0, 40 | "no-underscore-dangle": 0, 41 | "func-names": 0, 42 | "no-else-return": 0, 43 | "no-console": 0, 44 | "no-throw-literal": 0, 45 | "id-length": 0, 46 | "jsx-quotes": [2, "prefer-single"], 47 | 48 | "no-unused-vars": 0, 49 | "padded-blocks": 0, 50 | "block-scoped-var": 0, 51 | 52 | "react/jsx-quotes": 0, 53 | "react/display-name": 0, 54 | "react/jsx-boolean-value": 1, 55 | "react/jsx-no-undef": 1, 56 | "react/jsx-sort-prop-types": 0, 57 | "react/jsx-sort-props": 0, 58 | "react/jsx-uses-react": 1, 59 | "react/jsx-uses-vars": 1, 60 | "react/no-did-mount-set-state": 1, 61 | "react/no-did-update-set-state": 1, 62 | "react/no-multi-comp": 1, 63 | "react/no-unknown-property": 1, 64 | "react/prop-types": 1, 65 | "react/react-in-jsx-scope": 1, 66 | "react/self-closing-comp": 1, 67 | "react/sort-comp": 1, 68 | "react/wrap-multilines": 1 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniversalJS Boilerplate 2 | 3 | ![Universal JS Boilerplate](docs/header_universaljs.jpg) 4 | 5 | A boilerplate to quick start JavaScript Isomorphic Web Applications 6 | 7 | ## Technology Stack 8 | 9 | * **[ES2015 / ECMAScript 6th Edition](https://babeljs.io/docs/learn-es2015/)** - the upcoming version of the ECMAScript standard. 10 | * **[Babel](https://babeljs.io/)** - Next generation JavaScript, today. 11 | * **[Browserify](http://browserify.org/)** - Browserify lets you require('modules') in the browser by bundling up all of your dependencies. 12 | * **[React](http://facebook.github.io/react/)** - A JavaScript library for building user interfaces. 13 | * **[React Router](http://rackt.github.io/react-router/)** - Complete routing solution designed specifically for React.js. 14 | * **[React Engine](https://github.com/paypal/react-engine)** - A composite render engine for isomorphic express apps to render both plain react views and react-router views. 15 | * **[Stylus](https://learnboost.github.io/stylus/)** - Expressive, dynamic, robust CSS 16 | * **[NodeJS](https://nodejs.org/)** - Platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications. 17 | * **[Express](http://expressjs.com/)** - Fast, unopinionated, minimalist web framework for Node.js. 18 | * **[Gulp](http://gulpjs.com/)** - Automate and enhance your workflow. 19 | 20 | ## Requirements and Install 21 | 22 | Node.js v0.10.33 installed at least. 23 | 24 | ``` 25 | $ npm install -g gulp@3.9.0 26 | $ npm install -g babel browserify nodemon 27 | $ npm install 28 | $ npm run build 29 | $ npm start 30 | ``` 31 | 32 | And open your browser on `http://localhost:3000` 33 | 34 | ## Folder structure 35 | 36 | ``` 37 | . 38 | ├── /build/ # Transpiled and minifiqued output files. 39 | ├── /docs/ # Documentation about your project. 40 | ├── /node_modules/ # Node Modules and 3rd-party libraries. 41 | ├── /src/ # Source code of the web application. 42 | │ ├── /app/ # Isomorphic App source code. 43 | │ │ ├── /components/ # React components. 44 | │ │ ├── /containers/ # Redux containers. 45 | │ │ └── routes.js # Shared routes between Client-Server. 46 | │ │ └── client.js # 47 | │ │ └── server.js # 48 | │ │ └── config.js # App config (URLs, names, etc...). 49 | │ └── /styles/ # Stylesheets coding with Stylus Preprocessor. 50 | ├── /tasks/ # Gulp tasks for build the client part 51 | ├── gulpfile.babel.js # Config file for automated Builders. 52 | ├── package.json # App manifest and list of libraries installed. 53 | ├── .editorconfig # define and maintain consistent coding styles. 54 | ├── .eslintrc # ECMAScript6 and React Code linter. 55 | ├── .babelrc # babel ES6 rules. 56 | ├── .jscsrc # JavaScript Code styles. 57 | └── .stylintc # Stylus CSS Code style. 58 | ``` 59 | 60 | ## License 61 | 62 | Open Source. [MIT](LICENSE) © Carlos Azaustre 63 | -------------------------------------------------------------------------------- /src/app/server.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import Express from 'express'; 3 | import qs from 'qs'; 4 | import engine from 'react-engine'; 5 | import favicon from 'serve-favicon'; 6 | import config from './config'; 7 | 8 | import webpack from 'webpack'; 9 | import webpackDevMiddleware from 'webpack-dev-middleware'; 10 | import webpackHotMiddleware from 'webpack-hot-middleware'; 11 | import webpackConfig from '../../webpack.config'; 12 | 13 | import React from 'react'; 14 | import { renderToString } from 'react-dom/server'; 15 | import { Provider } from 'react-redux'; 16 | 17 | import configureStore from './store/configureStore'; 18 | import App from './containers/App'; 19 | import { fetchCounter } from './api/counter'; 20 | 21 | const app = new Express(); 22 | const port = config.app.port; 23 | 24 | // -- Setup React Views engine ------------------------------------------------- 25 | 26 | app.engine('.jsx', engine.server.create({ 27 | reactRoutes: path.join(__dirname, '..', 'app', 'routes.js') 28 | })); 29 | app.set('views', path.join(__dirname, '..', 'app', 'components')); 30 | app.set('view engine', 'jsx'); 31 | app.set('view', engine.expressView); 32 | 33 | const publicPath = path.join(__dirname, '..', '..', 'build', 'public'); 34 | const faviconPath = path.join(__dirname, '..', '..', 'build', 'public', 'favicon.ico'); 35 | 36 | // Use this middleware to set up hot module reloading via webpack. 37 | const compiler = webpack(webpackConfig); 38 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: webpackConfig.output.publicPath })); 39 | app.use(webpackHotMiddleware(compiler)); 40 | 41 | app.use(Express.static(publicPath)); 42 | app.use(favicon(faviconPath)); 43 | 44 | app.get('*', (req, res) => { 45 | 46 | // Query our mock API asynchronously 47 | fetchCounter(apiResult => { 48 | // Read the counter from the request, if provided 49 | const params = qs.parse(req.query); 50 | const counter = parseInt(params.counter, 10) || apiResult || 0; 51 | 52 | // Compile an initial state 53 | const initialState = { counter }; 54 | 55 | // Create a new Redux store instance 56 | const store = configureStore(initialState); 57 | 58 | // Render the component to a string 59 | const html = renderToString( 60 | 61 | 62 | 63 | ); 64 | 65 | // Grab the initial state from our Redux store 66 | const firstState = store.getState(); 67 | 68 | // Send the rendered page back to the client 69 | res.send(renderFullPage(html, firstState)); 70 | }); 71 | }); 72 | 73 | function renderFullPage(html, initialState) { 74 | return ` 75 | 76 | 77 | 78 | Redux Universal Example 79 | 80 | 81 |
${html}
82 | 85 | 86 | 87 | 88 | `; 89 | } 90 | 91 | app.listen(port, () => { 92 | console.log(`WebServer listening on port ${port}`); 93 | }); 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "engines": { 3 | "node": ">=4.1 <5", 4 | "npm": ">=3.1 <4" 5 | }, 6 | "name": "universal-js-boilerplate", 7 | "version": "0.0.1", 8 | "description": "A boilerplate to start universal (isomorphic) web applications", 9 | "main": "./src/app/server.js", 10 | "scripts": { 11 | "start": "nodemon --exec babel-node -- ./src/app/server.js", 12 | "build": "TARGET=build webpack", 13 | "dev": "TARGET=dev webpack-dev-server --progress --colors --hot --inline --history-api-fallback --content-base www", 14 | "deploy": "TARGET=build webpack -p -d" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/carlosazaustre/universal-js-boilerplate" 19 | }, 20 | "keywords": [ 21 | "boilerplate", 22 | "web development", 23 | "frontend", 24 | "javascript", 25 | "es6", 26 | "ecmascript 6", 27 | "react", 28 | "isomorphic" 29 | ], 30 | "author": "Carlos Azaustre (https://carlosazaustre.es/blog)", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/carlosazaustre/universal-js-boilerplate/issues" 34 | }, 35 | "homepage": "https://github.com/carlosazaustre/universal-js-boilerplate", 36 | "dependencies": { 37 | "babel": "^5.8.21", 38 | "express": "^4.13.3", 39 | "qs": "^4.0.0", 40 | "react": "^0.14.0", 41 | "react-dom": "^0.14.0", 42 | "react-engine": "^1.7.0", 43 | "react-redux": "^4.0.0", 44 | "react-router": "^1.0.0-rc3", 45 | "redux": "^3.0.2", 46 | "redux-thunk": "^0.1.0", 47 | "serve-favicon": "^2.3.0", 48 | "serve-static": "^1.10.0" 49 | }, 50 | "devDependencies": { 51 | "babel": "^5.6.14", 52 | "babel-core": "^5.8.3", 53 | "babel-eslint": "^4.1.3", 54 | "babel-loader": "^5.3.2", 55 | "babel-plugin-react-transform": "^1.1.0", 56 | "babel-runtime": "^5.6.15", 57 | "babelify": "^6.1.2", 58 | "browserify": "^10.2.4", 59 | "chai": "^3.3.0", 60 | "css-loader": "^0.15.5", 61 | "del": "^2.0.2", 62 | "eslint": "^1.6.0", 63 | "eslint-config-airbnb": "^0.1.0", 64 | "eslint-loader": "^1.0.0", 65 | "eslint-plugin-react": "^2.7.1", 66 | "esprima-fb": "^15001.1.0-dev-harmony-fb", 67 | "expect": "^1.8.0", 68 | "extract-text-webpack-plugin": "^0.8.2", 69 | "gulp": "^3.9.0", 70 | "gulp-eslint": "^1.0.0", 71 | "gulp-jscs": "^3.0.1", 72 | "gulp-minify-css": "^1.2.0", 73 | "gulp-stylint": "^3.0.0", 74 | "gulp-stylus": "^2.1.0", 75 | "jsdom": "^5.6.1", 76 | "json-loader": "^0.5.2", 77 | "mocha": "^2.2.5", 78 | "mocha-jsdom": "^1.0.0", 79 | "nib": "^1.1.0", 80 | "node-libs-browser": "^0.5.2", 81 | "nodemon": "^1.3.7", 82 | "raw-loader": "^0.5.1", 83 | "react-hot-loader": "^1.2.8", 84 | "react-transform-hmr": "^1.0.0", 85 | "redux": "^3.0.2", 86 | "redux-devtools": "^2.1.5", 87 | "redux-logger": "^2.0.4", 88 | "redux-promise": "^0.5.0", 89 | "redux-router": "^1.0.0-beta3", 90 | "sinon": "^1.17.1", 91 | "sinon-chai": "^2.8.0", 92 | "style-loader": "^0.12.3", 93 | "stylus-loader": "^1.4.0", 94 | "todomvc-app-css": "^2.0.1", 95 | "vinyl-buffer": "^1.0.0", 96 | "vinyl-source-stream": "^1.1.0", 97 | "webpack": "^1.10.5", 98 | "webpack-dev-middleware": "^1.2.0", 99 | "webpack-dev-server": "^1.10.1", 100 | "webpack-hot-middleware": "^2.2.0", 101 | "webpack-merge": "^0.1.2", 102 | "webpack-module-hot-accept": "1.0.3" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/styles/base/normalize.styl: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS and IE text size adjust after device orientation change, 6 | * without disabling user zoom. 7 | */ 8 | 9 | html 10 | font-family: sans-serif // 1 11 | -ms-text-size-adjust: 100% // 2 12 | -webkit-text-size-adjust: 100% // 2 13 | 14 | /** 15 | * Remove default margin. 16 | */ 17 | 18 | body 19 | margin 0 20 | 21 | /* HTML5 display definitions 22 | ========================================================================== */ 23 | 24 | /** 25 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 26 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 27 | * and Firefox. 28 | * Correct `block` display not defined for `main` in IE 11. 29 | */ 30 | 31 | article, 32 | aside, 33 | details, 34 | figcaption, 35 | figure, 36 | footer, 37 | header, 38 | hgroup, 39 | main, 40 | menu, 41 | nav, 42 | section, 43 | summary 44 | display block 45 | 46 | /** 47 | * 1. Correct `inline-block` display not defined in IE 8/9. 48 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 49 | */ 50 | 51 | audio, 52 | canvas, 53 | progress, 54 | video 55 | display inline-block // 1 56 | vertical-align baseline // 2 57 | 58 | /** 59 | * Prevent modern browsers from displaying `audio` without controls. 60 | * Remove excess height in iOS 5 devices. 61 | */ 62 | 63 | audio:not([controls]) 64 | display none 65 | height 0 66 | 67 | /** 68 | * Address `[hidden]` styling not present in IE 8/9/10. 69 | * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. 70 | */ 71 | 72 | [hidden], 73 | template 74 | display none 75 | 76 | /* Links 77 | ========================================================================== */ 78 | 79 | /** 80 | * 1. Remove the gray background color from active links in IE 10. 81 | * 2. Improve readability of focused elements when they are also in 82 | * an active/hover state. 83 | */ 84 | 85 | a 86 | background-color transparent // 1 87 | &:active, 88 | &:hover 89 | outline 0 // 2 90 | 91 | /* Text-level semantics 92 | ========================================================================== */ 93 | 94 | /** 95 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 96 | */ 97 | 98 | abbr[title] 99 | border-bottom 1px dotted 100 | 101 | /** 102 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 103 | */ 104 | 105 | b, 106 | strong 107 | font-weight bold 108 | 109 | /** 110 | * Address styling not present in Safari and Chrome. 111 | */ 112 | 113 | dfn 114 | font-style italic 115 | 116 | /** 117 | * Address variable `h1` font-size and margin within `section` and `article` 118 | * contexts in Firefox 4+, Safari, and Chrome. 119 | */ 120 | 121 | h1 122 | font-size 2em 123 | margin 0.67em 0 124 | 125 | /** 126 | * Address styling not present in IE 8/9. 127 | */ 128 | 129 | mark 130 | background #ff0 131 | color #000 132 | 133 | /** 134 | * Address inconsistent and variable font size in all browsers. 135 | */ 136 | 137 | small 138 | font-size 80% 139 | 140 | /** 141 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 142 | */ 143 | 144 | sub, 145 | sup 146 | font-size 75% 147 | line-height 0 148 | position relative 149 | vertical-align baseline 150 | 151 | sup 152 | top -0.5em 153 | 154 | sub 155 | bottom -0.25em 156 | 157 | /* Embedded content 158 | ========================================================================== */ 159 | 160 | /** 161 | * Remove border when inside `a` element in IE 8/9/10. 162 | */ 163 | 164 | img 165 | border 0 166 | 167 | /** 168 | * Correct overflow not hidden in IE 9/10/11. 169 | */ 170 | 171 | svg:not(:root) 172 | overflow hidden 173 | 174 | /* Grouping content 175 | ========================================================================== */ 176 | 177 | /** 178 | * Address margin not present in IE 8/9 and Safari. 179 | */ 180 | 181 | figure 182 | margin 1em 40px 183 | 184 | /** 185 | * Address differences between Firefox and other browsers. 186 | */ 187 | 188 | hr 189 | box-sizing content-box 190 | height 0 191 | 192 | /** 193 | * Contain overflow in all browsers. 194 | */ 195 | 196 | pre 197 | overflow auto 198 | 199 | /** 200 | * Address odd `em`-unit font size rendering in all browsers. 201 | */ 202 | 203 | code, 204 | kbd, 205 | pre, 206 | samp 207 | font-family monospace, monospace 208 | font-size 1em 209 | 210 | /* Forms 211 | ========================================================================== */ 212 | 213 | /** 214 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 215 | * styling of `select`, unless a `border` property is set. 216 | */ 217 | 218 | /** 219 | * 1. Correct color not being inherited. 220 | * Known issue: affects color of disabled elements. 221 | * 2. Correct font properties not being inherited. 222 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 223 | */ 224 | 225 | button, 226 | input, 227 | optgroup, 228 | select, 229 | textarea 230 | color inherit // 1 231 | font inherit // 2 232 | margin 0 // 3 233 | 234 | /** 235 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 236 | */ 237 | 238 | button 239 | overflow visible 240 | 241 | /** 242 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 243 | * All other form control elements do not inherit `text-transform` values. 244 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 245 | * Correct `select` style inheritance in Firefox. 246 | */ 247 | 248 | button, 249 | select 250 | text-transform none 251 | 252 | /** 253 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 254 | * and `video` controls. 255 | * 2. Correct inability to style clickable `input` types in iOS. 256 | * 3. Improve usability and consistency of cursor style between image-type 257 | * `input` and others. 258 | */ 259 | 260 | button, 261 | html input[type="button"], // 1 262 | input[type="reset"], 263 | input[type="submit"] 264 | -webkit-appearance button // 2 265 | cursor pointer // 3 266 | 267 | /** 268 | * Re-set default cursor for disabled elements. 269 | */ 270 | 271 | button[disabled], 272 | html input[disabled] 273 | cursor default 274 | 275 | /** 276 | * Remove inner padding and border in Firefox 4+. 277 | */ 278 | 279 | button::-moz-focus-inner, 280 | input::-moz-focus-inner 281 | border 0 282 | padding 0 283 | 284 | /** 285 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 286 | * the UA stylesheet. 287 | */ 288 | 289 | input 290 | line-height normal 291 | 292 | /** 293 | * It's recommended that you don't attempt to style these elements. 294 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 295 | * 296 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 297 | * 2. Remove excess padding in IE 8/9/10. 298 | */ 299 | 300 | input[type="checkbox"], 301 | input[type="radio"] 302 | box-sizing border-box // 1 303 | padding 0 // 2 304 | 305 | /** 306 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 307 | * `font-size` values of the `input`, it causes the cursor style of the 308 | * decrement button to change from `default` to `text`. 309 | */ 310 | 311 | input[type="number"]::-webkit-inner-spin-button, 312 | input[type="number"]::-webkit-outer-spin-button 313 | height auto 314 | 315 | /** 316 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 317 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. 318 | */ 319 | 320 | input[type="search"] 321 | -webkit-appearance textfield // 1 322 | box-sizing content-box // 2 323 | 324 | /** 325 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 326 | * Safari (but not Chrome) clips the cancel button when the search input has 327 | * padding (and `textfield` appearance). 328 | */ 329 | 330 | input[type="search"]::-webkit-search-cancel-button, 331 | input[type="search"]::-webkit-search-decoration 332 | -webkit-appearance none 333 | 334 | /** 335 | * Define consistent border, margin, and padding. 336 | */ 337 | 338 | fieldset 339 | border 1px solid #c0c0c0 340 | margin 0 2px 341 | padding 0.35em 0.625em 0.75em 342 | 343 | /** 344 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 345 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 346 | */ 347 | 348 | legend 349 | border 0 // 1 350 | padding 0 // 2 351 | 352 | /** 353 | * Remove default vertical scrollbar in IE 8/9/10/11. 354 | */ 355 | 356 | textarea 357 | overflow auto 358 | 359 | /** 360 | * Don't inherit the `font-weight` (applied by a rule above). 361 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 362 | */ 363 | 364 | optgroup 365 | font-weight bold 366 | 367 | /* Tables 368 | ========================================================================== */ 369 | 370 | /** 371 | * Remove most spacing between table cells. 372 | */ 373 | 374 | table 375 | border-collapse collapse 376 | border-spacing 0 377 | 378 | td, 379 | th 380 | padding 0 381 | --------------------------------------------------------------------------------