├── .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 | 
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 (,