├── .npmignore ├── .gitignore ├── .babelrc ├── img └── ux.gif ├── test ├── styles_test │ └── Test.styles.js ├── themes_test │ └── test-theme.js ├── test_helper.js ├── components_test │ └── Test.jsx ├── theme_spec.js ├── store_spec.js ├── component_spec.js └── reducer_spec.js ├── src ├── utils │ ├── index.js │ ├── transitions.js │ ├── colorManipulator.js │ └── colors.js ├── index.js ├── themeDecorator.js ├── defaultTheme.js ├── themeReducer.js └── ReduxTheme.jsx ├── LICENSE ├── package.json ├── .jshintrc └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | img 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | lib 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "loose": "all" 4 | } 5 | -------------------------------------------------------------------------------- /img/ux.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamLebarbare/redux-theme/HEAD/img/ux.gif -------------------------------------------------------------------------------- /test/styles_test/Test.styles.js: -------------------------------------------------------------------------------- 1 | export default (theme) => { 2 | return { 3 | base: { 4 | fontFamily: theme.typo.font 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export Colors from './colors' 4 | export ColorManipulator from './colorManipulator' 5 | export Transitions from './transitions' 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export ReduxTheme from './ReduxTheme'; 2 | export themeReducer from './themeReducer'; 3 | export connectTheme from './themeDecorator'; 4 | export {applyTheme, registerTheme, registerStyle } from './themeReducer'; 5 | export { Colors, ColorManipulator, Transitions } from './utils'; 6 | export Theme from './defaultTheme'; 7 | -------------------------------------------------------------------------------- /test/themes_test/test-theme.js: -------------------------------------------------------------------------------- 1 | import { Theme, Colors, ColorManipulator } from '../../src/'; 2 | 3 | const customTheme = new Theme ('test'); 4 | // Change some default theme properties 5 | customTheme.typo.font = 'Luckiest Guy, sans-serif'; 6 | customTheme.palette.subTextColor = ColorManipulator.fade(Colors.white, 0.54); 7 | export default customTheme; 8 | -------------------------------------------------------------------------------- /test/test_helper.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | import chai from 'chai'; 3 | import chaiImmutable from 'chai-immutable'; 4 | 5 | const doc = jsdom.jsdom(''); 6 | const win = doc.defaultView; 7 | 8 | global.document = doc; 9 | global.window = win; 10 | 11 | Object.keys(window).forEach((key) => { 12 | if (!(key in global)) { 13 | global[key] = window[key]; 14 | } 15 | }); 16 | 17 | 18 | 19 | chai.use(chaiImmutable); 20 | -------------------------------------------------------------------------------- /test/components_test/Test.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {connectTheme} from '../../src/'; 3 | 4 | @connectTheme 5 | export default class Test extends Component { 6 | 7 | static propTypes = { 8 | styles: PropTypes.object.isRequired 9 | } 10 | 11 | render() { 12 | const {styles, kind, action, text} = this.props; 13 | return
20 | {text} 21 |
22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/themeDecorator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { connect } from 'react-redux'; 3 | import radium from 'radium'; 4 | 5 | export default component => { 6 | return connect (state => { 7 | let styles; 8 | 9 | // handle serialized or immutable data 10 | if (typeof state.theme.get === 'function') { 11 | styles = state.theme.get('styles')[component.name]; 12 | } else { 13 | styles = state.theme.styles[component.name]; 14 | } 15 | 16 | if (!styles) { 17 | styles = {base: {}}; 18 | } 19 | 20 | return {styles: styles}; 21 | })(radium(component)); 22 | }; 23 | -------------------------------------------------------------------------------- /test/theme_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {List, Map, fromJS} from 'immutable'; 3 | import {expect} from 'chai'; 4 | import Theme from '../src/defaultTheme'; 5 | import reducer from '../src/themeReducer'; 6 | 7 | describe('ThemeSpec -> class Theme', () => { 8 | 9 | it('has default name', () => { 10 | const testTheme = new Theme (); 11 | 12 | expect(testTheme.name).to.equal('default'); 13 | }); 14 | 15 | it('construct name', () => { 16 | const testTheme = new Theme ('test'); 17 | 18 | expect(testTheme.name).to.equal('test'); 19 | }); 20 | 21 | it('has default categories', () => { 22 | const testTheme = new Theme (); 23 | 24 | expect(testTheme).to.have.any.keys ( 25 | 'palette', 26 | 'typo', 27 | 'spacing', 28 | 'shapes', 29 | 'colors', 30 | 'transitions' 31 | ); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/store_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {List, Map, fromJS} from 'immutable'; 3 | import {expect} from 'chai'; 4 | import Theme from '../src/defaultTheme'; 5 | import reducer from '../src/themeReducer'; 6 | import {createStore} from 'redux'; 7 | describe('StoreSpec -> class Theme', () => { 8 | 9 | it('self register', () => { 10 | const defaultTheme = new Theme (); 11 | const store = createStore (reducer); 12 | 13 | defaultTheme.register (store.dispatch); 14 | const state = store.getState (); 15 | 16 | expect(state).to.equal (fromJS ({ 17 | currentTheme: 'not configured', 18 | themesRegistry: { 19 | 'default': Map (defaultTheme) 20 | } 21 | })); 22 | }); 23 | 24 | it('self apply', () => { 25 | const store = createStore (reducer); 26 | const defaultTheme = new Theme (); 27 | defaultTheme.register (store.dispatch); 28 | defaultTheme.apply (store.dispatch); 29 | const state = store.getState (); 30 | 31 | expect(state.get ('currentTheme')).to.equal ('default'); 32 | expect(state).to.include.keys ('styles'); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sam Le Barbare 4 | Copyright (c) 2014 Call-Em-All (Some utils and theming concepts under src/utils) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-theme", 3 | "version": "0.3.4", 4 | "description": "Theme and Styles provider for Redux", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "prepublish": "babel src --out-dir lib", 8 | "build": "babel src --out-dir lib", 9 | "clean": "rimraf lib", 10 | "test": "mocha --compilers js:babel/register --require ./test/test_helper.js --recursive", 11 | "test:watch": "npm run test -- --watch" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/SamLebarbare/redux-theme.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "redux", 20 | "theme", 21 | "styles" 22 | ], 23 | "author": "Sam Le Barbare ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/SamLebarbare/redux-theme/issues" 27 | }, 28 | "homepage": "https://github.com/SamLebarbare/redux-theme#readme", 29 | "devDependencies": { 30 | "babel": "^5.8.29", 31 | "chai": "^3.4.0", 32 | "chai-immutable": "^1.5.1", 33 | "jsdom": "^7.0.2", 34 | "mocha": "^2.3.3", 35 | "react": "^0.14.2", 36 | "rimraf": "^2.4.3" 37 | }, 38 | "dependencies": { 39 | "immutable": "^3.7.5", 40 | "radium": "^0.14.3", 41 | "react": "^0.14.1", 42 | "react-redux": "^4.0.0", 43 | "redux": "^3.0.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/defaultTheme.js: -------------------------------------------------------------------------------- 1 | import { Colors, ColorManipulator, Transitions } from './utils'; 2 | import {registerTheme, applyTheme} from './themeReducer'; 3 | 4 | export default class Theme { 5 | constructor (themeName) { 6 | this.name = themeName || 'default'; 7 | this.spacing = { 8 | iconSize: 24, 9 | desktopKeylineIncrement: 64 10 | }; 11 | 12 | this.typo = { 13 | font: 'Roboto, sans-serif', 14 | small: '12px', 15 | normal: '16px', 16 | big: '24px' 17 | }; 18 | 19 | this.spacing = { 20 | iconSize: 24, 21 | desktopKeylineIncrement: 64 22 | }; 23 | 24 | this.palette = { 25 | primary1Color: Colors.lightBlue500, 26 | primary2Color: Colors.lightBlue700, 27 | primary3Color: Colors.lightBlue100, 28 | accent1Color: Colors.pinkA200, 29 | accent2Color: Colors.pinkA400, 30 | accent3Color: Colors.pinkA100, 31 | textColor: Colors.darkBlack, 32 | subTextColor: ColorManipulator.fade(Colors.darkBlack, 0.54), 33 | canvasColor: Colors.white, 34 | paperColor: Colors.white, 35 | borderColor: Colors.grey300, 36 | disabledColor: ColorManipulator.fade(Colors.darkBlack, 0.3) 37 | }; 38 | 39 | this.shapes = { 40 | defaultBorderRadius: '2px' 41 | }; 42 | 43 | this.colors = Colors; 44 | 45 | this.transitions = Transitions; 46 | } 47 | 48 | register (dispatch) { 49 | dispatch (registerTheme (this)); 50 | } 51 | 52 | apply (dispatch) { 53 | dispatch (applyTheme (this.name)); 54 | } 55 | 56 | }; 57 | -------------------------------------------------------------------------------- /test/component_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {List, Map, fromJS} from 'immutable'; 3 | import {expect} from 'chai'; 4 | import Theme from '../src/defaultTheme'; 5 | import reducer from '../src/themeReducer'; 6 | import {createStore, combineReducers} from 'redux'; 7 | import React from 'react'; 8 | import {renderIntoDocument, findRenderedDOMComponentWithTag} from 'react-addons-test-utils'; 9 | import ReduxTheme from '../src/ReduxTheme'; 10 | import testTheme from './themes_test/test-theme'; 11 | import testStyle from './styles_test/Test.styles.js'; 12 | import TestComponent from './components_test/Test'; 13 | 14 | 15 | 16 | describe('component_spec -> ', () => { 17 | 18 | const reducers = combineReducers({ 19 | theme: reducer 20 | }); 21 | 22 | const testTheme = new Theme ('test'); 23 | testTheme.typo.font = 'Luckiest Guy, sans-serif'; 24 | 25 | 26 | const testStyle = (theme) => ({ 27 | base: { 28 | fontFamily: theme.typo.font 29 | } 30 | }); 31 | 32 | 33 | const styles = [{ 34 | componentName: 'Test', 35 | style: testStyle 36 | }]; 37 | const store = createStore (reducers); 38 | 39 | renderIntoDocument ( 40 | 45 | ); 46 | 47 | const component = renderIntoDocument ( 48 | 49 | ); 50 | 51 | const node = findRenderedDOMComponentWithTag (component, 'div'); 52 | 53 | it('set correct google font in header', () => { 54 | expect(document.getElementById ('reduxtheme').href).to 55 | .equal ('http://fonts.googleapis.com/css?family=Luckiest Guy'); 56 | }); 57 | 58 | it('Test component has correct style', () => { 59 | expect(node.style[0]).to 60 | .equal ('font-family'); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/reducer_spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {List, Map, fromJS} from 'immutable'; 3 | import {expect} from 'chai'; 4 | import reducer, 5 | {registerTheme, registerStyle, applyTheme} from '../src/themeReducer'; 6 | import Theme from '../src/defaultTheme'; 7 | import testTheme from './themes_test/test-theme'; 8 | import testStyle from './styles_test/Test.styles.js'; 9 | 10 | describe('ReducerSpec -> actions', () => { 11 | 12 | it('initialize', () => { 13 | const nextState = reducer(); 14 | 15 | expect(nextState).to.equal(fromJS ({ 16 | currentTheme: 'not configured' 17 | })); 18 | }); 19 | 20 | it('register default theme', () => { 21 | const initialState = Map(fromJS ({ 22 | currentTheme: 'not configured', 23 | }) 24 | ); 25 | const theme = new Theme (); 26 | const nextState = reducer(initialState, registerTheme (theme)); 27 | 28 | expect(nextState).to.equal(fromJS ({ 29 | currentTheme: 'not configured', 30 | themesRegistry: { 31 | default: Map (theme) 32 | } 33 | })); 34 | }); 35 | 36 | it('register style for Test component', () => { 37 | const initialState = Map(fromJS ({ 38 | currentTheme: 'not configured' 39 | }) 40 | ); 41 | 42 | const nextState = reducer(initialState, registerStyle ('Test', testStyle)); 43 | expect(nextState).to.equal(fromJS ({ 44 | currentTheme: 'not configured', 45 | stylesRegistry: { 46 | Test: testStyle 47 | } 48 | })); 49 | }); 50 | 51 | it('apply `test-theme`', () => { 52 | const actions = [ 53 | registerTheme (testTheme), 54 | registerStyle ('Test', testStyle), 55 | applyTheme ('test') 56 | ]; 57 | 58 | 59 | const finalState = actions.reduce(reducer, Map()) 60 | expect(finalState.get ('currentTheme')).to.equal ('test'); 61 | expect(finalState).to.include.keys ('styles'); 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /src/utils/transitions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | defaultTransition: { 5 | duration: 500, 6 | enter: { 7 | opacity: [ 1, 0 ] 8 | }, 9 | leave: { 10 | opacity: [ 0, 1 ] 11 | } 12 | }, 13 | rotate: { 14 | duration: 250, 15 | enter: { 16 | translateZ: 0, // Force HA by animating a 3D property 17 | rotateZ: '360deg' 18 | }, 19 | leave: { 20 | translateZ: 0, // Force HA by animating a 3D property 21 | rotateZ: '-360deg' 22 | } 23 | }, 24 | slide: { 25 | duration: 250, 26 | enter: { 27 | translateZ: 0, // Force HA by animating a 3D property 28 | translateX: [ '0%', '-100%' ] 29 | }, 30 | leave: { 31 | translateZ: 0, // Force HA by animating a 3D property 32 | translateX: [ '100%', '0%' ] 33 | } 34 | }, 35 | overlay: { 36 | duration: 500, 37 | enter: { 38 | opacity: [ 1, 0 ] 39 | }, 40 | leave: { 41 | opacity: [ 0, 1 ] 42 | } 43 | }, 44 | leftPanel: { 45 | duration: 250, 46 | enter: { 47 | translateZ: 0, // Force HA by animating a 3D property 48 | translateX: [ '0%', '-100%' ] 49 | }, 50 | leave: { 51 | translateZ: 0, // Force HA by animating a 3D property 52 | translateX: [ '100%', '0%' ] 53 | } 54 | }, 55 | easeOutFunction: 'cubic-bezier(0.23, 1, 0.32, 1)', 56 | easeInOutFunction: 'cubic-bezier(0.445, 0.05, 0.55, 0.95)', 57 | 58 | easeOut: function(duration, property, delay, easeFunction) { 59 | easeFunction = easeFunction || this.easeOutFunction; 60 | return this.create (duration, property, delay, easeFunction); 61 | }, 62 | 63 | create: function(duration, property, delay, easeFunction) { 64 | duration = duration || '450ms'; 65 | property = property || 'all'; 66 | delay = delay || '0ms'; 67 | easeFunction = easeFunction || 'linear'; 68 | 69 | return property + ' ' + 70 | duration + ' ' + 71 | easeFunction + ' ' + 72 | delay; 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/themeReducer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Map, fromJS } from 'immutable'; 4 | const APPLY_THEME = 'redux-theme/APPLY_THEME'; 5 | const REGISTER_THEME = 'redux-theme/REGISTER_THEME'; 6 | const REGISTER_STYLE = 'redux-theme/REGISTER_STYLE'; 7 | 8 | function reloadStyles (state, themeName) { 9 | let newStyles = {}; 10 | let styles = state.getIn (['stylesRegistry']); 11 | let theme = state.getIn (['themesRegistry', themeName]); 12 | if (!styles || !theme) { 13 | return {}; 14 | } 15 | for (let [componentName, style] of styles.entries ()) { 16 | newStyles[componentName] = style (theme.toJS ()); 17 | } 18 | return newStyles; 19 | } 20 | 21 | function reloadTheme (state, themeName) { 22 | return state.set ('currentTheme', themeName) 23 | .setIn (['styles'], reloadStyles (state, themeName)); 24 | } 25 | 26 | function addTheme (state, theme) { 27 | return state.setIn (['themesRegistry', theme.name], Map (fromJS (theme))); 28 | } 29 | 30 | function addStyle (state, componentName, style) { 31 | return state.setIn (['stylesRegistry', componentName], style); 32 | } 33 | 34 | const initialState = Map(fromJS ({ 35 | currentTheme: 'not configured' 36 | }) 37 | ); 38 | 39 | export default function themeReducer (state = initialState, action = {}) { 40 | switch (action.type) { 41 | case REGISTER_STYLE: 42 | return addStyle (state, action.componentName, action.style); 43 | case REGISTER_THEME: 44 | return addTheme (state, action.theme); 45 | case APPLY_THEME: 46 | return reloadTheme (state, action.theme); 47 | default: 48 | return state; 49 | } 50 | } 51 | 52 | export function applyTheme (name) { 53 | if (typeof name !== 'string') { 54 | throw new Error('applyTheme need a theme name as first argument'); 55 | } 56 | return { type: APPLY_THEME, theme: name}; 57 | } 58 | 59 | export function registerTheme (theme) { 60 | return { type: REGISTER_THEME, theme: theme}; 61 | } 62 | 63 | export function registerStyle (component, style) { 64 | return { type: REGISTER_STYLE, componentName: component, style: style}; 65 | } 66 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr" : 50, 3 | 4 | // Enforcing 5 | "bitwise" : true, 6 | "camelcase" : true, 7 | "curly" : true, 8 | "eqeqeq" : true, 9 | "es3" : false, 10 | "forin" : true, 11 | "freeze" : true, 12 | "immed" : true, 13 | "indent" : 2, 14 | "latedef" : true, 15 | "newcap" : false, 16 | "noarg" : true, 17 | "noempty" : true, 18 | "nonbsp" : true, 19 | "nonew" : true, 20 | "plusplus" : false, 21 | "quotmark" : "single", 22 | "undef" : true, 23 | "unused" : true, 24 | "strict" : true, 25 | "maxparams" : false, 26 | "maxdepth" : false, 27 | "maxstatements" : false, 28 | "maxcomplexity" : false, 29 | 30 | // Relaxing 31 | "asi" : false, 32 | "boss" : false, 33 | "debug" : false, 34 | "eqnull" : false, 35 | "esnext" : true, 36 | "evil" : false, 37 | "expr" : false, 38 | "funcscope" : false, 39 | "globalstrict" : true, 40 | "iterator" : false, 41 | "lastsemic" : false, 42 | "laxbreak" : false, 43 | "laxcomma" : false, 44 | "loopfunc" : false, 45 | "moz" : false, 46 | "multistr" : false, 47 | "noyield" : false, 48 | "notypeof" : false, 49 | "proto" : false, 50 | "scripturl" : false, 51 | "shadow" : false, 52 | "sub" : false, 53 | "super" : false, 54 | "validthis" : false, 55 | 56 | // Environments 57 | "browser" : false, 58 | "browserify" : false, 59 | "couch" : false, 60 | "devel" : false, 61 | "dojo" : false, 62 | "jasmine" : false, 63 | "jquery" : false, 64 | "mocha" : false, 65 | "mootools" : false, 66 | "node" : true, 67 | "nonstandard" : false, 68 | "phantom" : false, 69 | "prototypejs" : false, 70 | "rhino" : false, 71 | "worker" : false, 72 | "wsh" : false, 73 | "yui" : false, 74 | 75 | // Custom Globals 76 | "globals" : { 77 | "require" : true, 78 | "module" : true, 79 | "console" : true, 80 | "document" : true, 81 | "window" : true 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ReduxTheme.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import React, {Component, PropTypes} from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { Theme, registerTheme, registerStyle, applyTheme } from './themeReducer'; 5 | import { Colors } from './utils/' 6 | @connect ( 7 | state => { 8 | // handle serialized or immutable data 9 | if (typeof state.theme.get === 'function') { 10 | return { 11 | theme: state.theme.getIn (['themesRegistry', state.theme.get ('currentTheme')]) 12 | }; 13 | } else { 14 | return { 15 | theme: state.theme.themesRegistry[state.theme.currentTheme] 16 | }; 17 | } 18 | }, 19 | dispatch => ({applyTheme, registerTheme, registerStyle, dispatch}), 20 | null, 21 | {pure: true}) 22 | export default class ReduxTheme extends Component { 23 | 24 | static propTypes = { 25 | defaultThemeName: PropTypes.string, 26 | styles: PropTypes.arrayOf(React.PropTypes.shape ({ 27 | componentName: PropTypes.string, 28 | style: PropTypes.func 29 | })).isRequired, 30 | themes: PropTypes.arrayOf(PropTypes.object).isRequired 31 | } 32 | 33 | configure() { 34 | const { 35 | dispatch, 36 | styles, 37 | themes, 38 | defaultTheme, 39 | applyTheme, 40 | registerStyle, 41 | registerTheme} = this.props; 42 | 43 | themes.forEach ((theme) => { 44 | dispatch (registerTheme (theme)); 45 | }); 46 | styles.forEach ((style) => { 47 | dispatch (registerStyle (style.componentName, style.style)); 48 | }); 49 | dispatch (applyTheme (defaultTheme)); 50 | } 51 | 52 | componentWillMount() { 53 | this.configure (); 54 | } 55 | 56 | render() { 57 | const { theme } = this.props; 58 | if (theme) { 59 | const font = theme.getIn (['typo']).font 60 | const canvasColor = theme.getIn (['palette']).canvasColor; 61 | 62 | // set bgcolor from theme 63 | document.body.style.backgroundColor = canvasColor; 64 | 65 | // try to get reduxtheme element in DOM 66 | let ss = document.getElementById ('reduxtheme'); 67 | if (!ss) { 68 | // create google fonts css req. 69 | ss = document.createElement ('link'); 70 | ss.id = 'reduxtheme'; 71 | ss.type = 'text/css'; 72 | ss.rel = 'stylesheet'; 73 | ss.href = 'http://fonts.googleapis.com/css?family='; 74 | ss.href += font.split (',')[0]; 75 | document.getElementsByTagName ('head')[0].appendChild(ss); 76 | } else { 77 | // update google fonts css req. 78 | ss.href = 'http://fonts.googleapis.com/css?family='; 79 | ss.href += font.split (',')[0]; 80 | } 81 | } 82 | 83 | const style = { 84 | display: 'none' 85 | }; 86 | 87 | return ( 88 |
89 | {this.props.children} 90 |
91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/utils/colorManipulator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | /** 6 | * The relative brightness of any point in a colorspace, normalized to 0 for 7 | * darkest black and 1 for lightest white. RGB colors only. Does not take 8 | * into account alpha values. 9 | * 10 | * TODO: 11 | * - Take into account alpha values. 12 | * - Identify why there are minor discrepancies for some use cases 13 | * (i.e. #F0F & #FFF). Note that these cases rarely occur. 14 | * 15 | * Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef 16 | */ 17 | _luminance: function (color) { 18 | color = this._decomposeColor (color); 19 | 20 | if (color.type.indexOf('rgb') > -1) { 21 | var rgb = color.values.map(function(val) { 22 | val /= 255; // normalized 23 | return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4); 24 | }); 25 | 26 | return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; 27 | 28 | } else { 29 | var message = 'Calculating the relative luminance is not available for ' + 30 | 'HSL and HSLA.'; 31 | console.error (message); 32 | return -1; 33 | } 34 | }, 35 | 36 | /** 37 | * @params: 38 | * additionalValue = An extra value that has been calculated but not included 39 | * with the original color object, such as an alpha value. 40 | */ 41 | _convertColorToString: function (color, additonalValue) { 42 | var str = color.type + '(' + 43 | parseInt(color.values[0]) + ',' + 44 | parseInt(color.values[1]) + ',' + 45 | parseInt(color.values[2]); 46 | 47 | if (additonalValue !== undefined) { 48 | str += ',' + additonalValue + ')'; 49 | } else if (color.values.length === 4) { 50 | str += ',' + color.values[3] + ')'; 51 | } else { 52 | str += ')'; 53 | } 54 | 55 | return str; 56 | }, 57 | 58 | // Converts a color from hex format to rgb format. 59 | _convertHexToRGB: function(color) { 60 | if (color.length === 4) { 61 | var extendedColor = '#'; 62 | for (var i = 1; i < color.length; i++) { 63 | extendedColor += color.charAt(i) + color.charAt(i); 64 | } 65 | color = extendedColor; 66 | } 67 | 68 | var values = { 69 | r: parseInt(color.substr(1,2), 16), 70 | g: parseInt(color.substr(3,2), 16), 71 | b: parseInt(color.substr(5,2), 16), 72 | }; 73 | 74 | return 'rgb(' + values.r + ',' + 75 | values.g + ',' + 76 | values.b + ')'; 77 | }, 78 | 79 | // Returns the type and values of a color of any given type. 80 | _decomposeColor: function(color) { 81 | if (color.charAt(0) === '#') { 82 | return this._decomposeColor(this._convertHexToRGB(color)); 83 | } 84 | 85 | var marker = color.indexOf('('); 86 | var type = color.substring(0, marker); 87 | var values = color.substring(marker + 1, color.length - 1).split(','); 88 | 89 | return {type: type, values: values}; 90 | }, 91 | 92 | // Set the absolute transparency of a color. 93 | // Any existing alpha values are overwritten. 94 | fade: function(color, amount) { 95 | color = this._decomposeColor(color); 96 | if (color.type === 'rgb' || color.type === 'hsl') { 97 | color.type += 'a'; 98 | } 99 | 100 | return this._convertColorToString (color, amount); 101 | }, 102 | 103 | // Desaturates rgb and sets opacity to 0.15 104 | lighten: function(color, amount) { 105 | color = this._decomposeColor(color); 106 | 107 | if (color.type.indexOf('hsl') > -1) { 108 | color.values[2] += amount; 109 | return this._decomposeColor(this._convertColorToString(color)); 110 | } else if (color.type.indexOf('rgb') > -1) { 111 | for (var i = 0; i < 3; i++) { 112 | color.values[i] *= 1 + amount; 113 | if (color.values[i] > 255) { 114 | color.values[i] = 255; 115 | } 116 | } 117 | } 118 | 119 | if (color.type.indexOf('a') <= -1) { 120 | color.type += 'a'; 121 | } 122 | 123 | return this._convertColorToString(color, '0.15'); 124 | }, 125 | 126 | darken: function(color, amount) { 127 | color = this._decomposeColor(color); 128 | 129 | if (color.type.indexOf('hsl') > -1) { 130 | color.values[2] += amount; 131 | return this._decomposeColor(this._convertColorToString(color)); 132 | } else if (color.type.indexOf('rgb') > -1) { 133 | for (var i = 0; i < 3; i++) { 134 | color.values[i] *= 1 - amount; 135 | if (color.values[i] < 0) { 136 | color.values[i] = 0; 137 | } 138 | } 139 | } 140 | 141 | return this._convertColorToString(color); 142 | }, 143 | 144 | 145 | // Calculates the contrast ratio between two colors. 146 | // 147 | // Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef 148 | contrastRatio: function(background, foreground) { 149 | var lumA = this._luminance(background); 150 | var lumB = this._luminance(foreground); 151 | 152 | if (lumA >= lumB) { 153 | return ((lumA + 0.05) / (lumB + 0.05)).toFixed(2); 154 | } else { 155 | return ((lumB + 0.05) / (lumA + 0.05)).toFixed(2); 156 | } 157 | }, 158 | 159 | /** 160 | * Determines how readable a color combination is based on its level. 161 | * Levels are defined from @LeaVerou: 162 | * https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/contrast-ratio.js 163 | */ 164 | contrastRatioLevel: function(background, foreground) { 165 | var levels = { 166 | 'fail': { 167 | range: [0, 3], 168 | color: 'hsl(0, 100%, 40%)' 169 | }, 170 | 'aa-large': { 171 | range: [3, 4.5], 172 | color: 'hsl(40, 100%, 45%)' 173 | }, 174 | 'aa': { 175 | range: [4.5, 7], 176 | color: 'hsl(80, 60%, 45%)' 177 | }, 178 | 'aaa': { 179 | range: [7, 22], 180 | color: 'hsl(95, 60%, 41%)' 181 | } 182 | }; 183 | 184 | var ratio = this.contrastRatio(background, foreground); 185 | 186 | for (var level in levels) { 187 | if (levels.hasOwnProperty(level)) { 188 | var range = levels[level].range; 189 | if (ratio >= range[0] && ratio <= range[1]) { 190 | return level; 191 | } 192 | } 193 | } 194 | 195 | } 196 | 197 | }; 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-theme v0.3.4 2 | 3 | **note: early preview ! wait for 1.0.0 before using** 4 | 5 | _Decorate your components using themes_ 6 | 7 | This package try to provide best practices for managing inline-styles. 8 | 9 | - radium for inline-style 10 | - material-ui inspired theme template 11 | - react-redux connect decorator for injecting themed styles 12 | 13 | In term of UX: 14 | 15 | ![](https://raw.githubusercontent.com/SamLebarbare/redux-theme/master/img/ux.gif) 16 | 17 | ## Installation 18 | 19 | ``` 20 | npm install --save redux-theme 21 | ``` 22 | 23 | ## Usage 24 | 25 | ### Modify app structure 26 | 27 | Using `redux-theme` add generally two new folders to your app structure: 28 | 29 | ``` 30 | └── src # Application source code 31 | ├── components # Generic React Components (generally Dumb components) 32 | ├── styles # Redux-theme styles definitions for components 33 | ├── themes # Redux-theme definitions 34 | ├── reducers # Redux reducers 35 | etc... 36 | ``` 37 | 38 | ### Configure your store 39 | 40 | Like other reducers, `theme:` is required in app state shape. 41 | 42 | ```js 43 | import { combineReducers } from 'redux'; 44 | import { routerStateReducer } from 'redux-router'; 45 | import { themeReducer } from 'redux-theme'; 46 | 47 | export default combineReducers({ 48 | theme: themeReducer, 49 | router: routerStateReducer 50 | }); 51 | ``` 52 | ### Provide theme via ReduxTheme component 53 | 54 | The ReduxTheme component responsible of : 55 | 56 | On mount: 57 | 58 | - Registering your themes 59 | - Registering your styles 60 | - Applying the first theme 61 | 62 | On theme change: 63 | 64 | - Update googlefont from theme 65 | - Update body.backgroundColor from theme 66 | 67 | Exemple: 68 | 69 | ```js 70 | import { ReduxTheme } from 'redux-theme'; 71 | 72 | /// Some themes... 73 | const baseTheme = new Theme ('base'); 74 | const myTheme = new Theme ('mytheme'); 75 | myTheme.typo.font = 'Luckiest Guy, sans-serif'; 76 | 77 | const textStyle = (theme) => ({ 78 | base: { 79 | fontFamily: theme.typo.font 80 | } 81 | }); 82 | 83 | 84 | // Build array of themes and styles 85 | const themes = [defaultTheme, myTheme]; 86 | const styles = [{ 87 | componentName: 'Text', // Will apply on decorated component with this name 88 | style: textStyle 89 | }]; 90 | 91 | export default class Root extends Component { 92 | render() { 93 | const {store} = this.props; 94 | return ( 95 |
96 | 97 | 102 | 103 | 104 | 105 | {routes} 106 | 107 | 108 |
109 | ); 110 | } 111 | ``` 112 | ### Decorate your components 113 | 114 | Connect you components using `@connectTheme` decorator. 115 | Your component will receive a `styles` props. with `radium` styles. 116 | 117 | **note:** The component class name is used for resolution of styles, 118 | in this case, it will look for a Button in theme.styles of your state. 119 | 120 | ```js 121 | import React, {Component, PropTypes} from 'react'; 122 | import {connectTheme} from 'redux-theme'; 123 | 124 | @connectTheme 125 | export default class Button extends Component { 126 | 127 | static propTypes = { 128 | styles: PropTypes.object.isRequired 129 | } 130 | 131 | render() { 132 | const {styles, kind, action, text} = this.props; 133 | return 142 | } 143 | } 144 | ``` 145 | 146 | ### Theme class 147 | 148 | You can use, override and export the default `redux-theme`: 149 | Colors and utilities is also provided. 150 | 151 | #### new Theme () 152 | 153 | ```js 154 | // /themes/custom-theme.js 155 | import { Theme, Colors, ColorManipulator } from 'redux-theme'; 156 | 157 | const customTheme = new Theme ('custom'); 158 | // Change some default theme properties 159 | customTheme.typo.font = 'Luckiest Guy, sans-serif'; 160 | customTheme.palette.subTextColor = ColorManipulator.fade(Colors.white, 0.54); 161 | export default customTheme; 162 | ``` 163 | 164 | #### Available methods 165 | 166 | A theme can register and apply if you provide the dispatch func. 167 | 168 | ```js 169 | const customTheme = new Theme ('custom'); 170 | // Change some default theme properties 171 | // ... 172 | const {dispatch} = this.props; 173 | customTheme.register (dispatch); 174 | customTheme.apply (dispatch); 175 | ``` 176 | 177 | ### Styles 178 | 179 | A style file is a function receiving the current theme as argument. 180 | Style file is using `radium` convention for applying a `kind`. 181 | 182 | ```js 183 | export default (theme) => { 184 | return { 185 | base: { 186 | display: 'inline-block', 187 | fontSize: '18px', 188 | fontFamily: theme.typo.font, 189 | fontWeight: 400, 190 | textTransform: 'uppercase', 191 | cursor: 'pointer', 192 | userSelect: 'none', 193 | outline: 'none', 194 | marginTop: '3px', 195 | minWidth: theme.spacing.desktopKeylineIncrement * 2, 196 | border: 'none', 197 | paddingLeft: '5px', 198 | paddingRight: '5px', 199 | paddingTop: '5px', 200 | paddingBottom: '5px', 201 | color: theme.palette.textColor, 202 | backgroundColor: theme.palette.primary1Color, 203 | borderRadius: theme.shapes.defaultBorderRadius, 204 | ':hover': { 205 | backgroundColor: theme.palette.primary3Color, 206 | }, 207 | ':focus': { 208 | backgroundColor: theme.palette.primary3Color, 209 | }, 210 | ':active': { 211 | backgroundColor: theme.palette.primary3Color, 212 | } 213 | }, 214 | small: { 215 | paddingLeft: '15px', 216 | paddingRight: '15px' 217 | }, 218 | accept: { 219 | fontWeight: 'bold', 220 | }, 221 | cancel: { 222 | fontStyle: 'italic', 223 | } 224 | } 225 | }; 226 | ``` 227 | ## Reducer actions 228 | 229 | You can bootstrap your theme by dipatching action yourself. 230 | Logic order is: 231 | 232 | - register your themes 233 | - register your styles 234 | - you can apply one of your registred themes 235 | 236 | `registerTheme ()` 237 | 238 | `registerStyle (,