diff --git a/packages/react-base16-styling/.babelrc b/packages/react-base16-styling/.babelrc
new file mode 100644
index 00000000..644dde97
--- /dev/null
+++ b/packages/react-base16-styling/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["@babel/preset-env"],
+ "plugins": ["@babel/plugin-proposal-class-properties"]
+}
diff --git a/packages/react-base16-styling/README.md b/packages/react-base16-styling/README.md
new file mode 100644
index 00000000..982a1398
--- /dev/null
+++ b/packages/react-base16-styling/README.md
@@ -0,0 +1,91 @@
+# react-base16-styling [](https://travis-ci.org/alexkuz/react-base16-styling) [](https://www.npmjs.com/package/react-base16-styling)
+
+React styling with base16 color scheme support
+
+Inspired by [react-themeable](https://github.com/markdalgleish/react-themeable), this utility provides flexible theming for your component with [base16](https://github.com/chriskempson/base16) theme support.
+
+## Install
+
+```
+yarn add react-base16-styling
+```
+
+## Usage
+
+```jsx
+import { createStyling } from 'react-base16-styling';
+import base16Themes from './base16Themes';
+
+function getStylingFromBase16(base16Theme) {
+ return {
+ myComponent: {
+ backgroundColor: base16Theme.base00
+ },
+
+ myComponentToggleColor({ style, className }, clickCount) {
+ return {
+ style: {
+ ...style,
+ backgroundColor: clickCount % 2 ? 'red' : 'blue'
+ }
+ };
+ }
+ };
+}
+
+const createStylingFromTheme = createStyling(getStylingFromBase16, {
+ defaultBase16: base16Themes.solarized,
+ base16Themes
+});
+
+class MyComponent extends Component {
+ state = { clickCount: 0 };
+ render() {
+ const { theme } = this.props;
+ const { clickCount } = this.state;
+
+ const styling = createStylingFromTheme(theme);
+
+ return (
+
+ );
+ }
+}
+```
+
+## `createStyling`
+
+```js
+function(getStylingFromBase16, defaultStylingOptions, themeOrStyling)
+```
+
+| Argument | Signature | Description |
+| ----------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `getStylingFromBase16` | `function(base16Theme)` | creates object with default stylings for your component, using provided base16 theme. |
+| `defaultStylingOptions` | `{ defaultBase16, base16Themes }` | optional parameters, allow to set default `base16` theme and additional `base16` themes for component. |
+| `themeOrStyling` | `string` or `object` | `base16` theme name, `base16` theme object or styling object. Theme name can have a modifier: `"themeName:inverted"` to invert theme colors (see [[#invertTheme]]) |
+
+Styling object values could be: - objects (treated as style definitions) - strings (class names) - functions (they may be provided with additional parameters and should return object { style, className })
+
+## `getBase16Theme`
+
+```js
+function(themeOrStyling, base16Themes)
+```
+
+Helper method that returns `base16` theme object if `themeOrStyling` is `base16` theme name or theme object.
+
+## `invertTheme`
+
+```js
+function(theme)
+```
+
+Helper method that inverts `base16` theme colors, creating light theme out of dark one or vice versa.
diff --git a/packages/react-base16-styling/package.json b/packages/react-base16-styling/package.json
new file mode 100644
index 00000000..fa1ee4c9
--- /dev/null
+++ b/packages/react-base16-styling/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "react-base16-styling",
+ "version": "0.6.0",
+ "description": "React styling with base16 color scheme support",
+ "main": "lib/index.js",
+ "scripts": {
+ "clean": "rimraf lib",
+ "build": "babel src --out-dir lib",
+ "test": "jest",
+ "prepare": "npm run build",
+ "prepublishOnly": "npm run test && npm run clean && npm run build"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/reduxjs/redux-devtools.git"
+ },
+ "keywords": [
+ "react",
+ "theme",
+ "base16",
+ "styling"
+ ],
+ "author": "Alexander (http://kuzya.org/)",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/reduxjs/redux-devtools/issues"
+ },
+ "homepage": "https://github.com/reduxjs/redux-devtools",
+ "devDependencies": {
+ "@babel/cli": "^7.10.5",
+ "@babel/core": "^7.11.0",
+ "@babel/plugin-proposal-class-properties": "^7.10.4",
+ "@babel/plugin-transform-runtime": "^7.11.0",
+ "@babel/preset-env": "^7.11.0",
+ "rimraf": "^2.7.1"
+ },
+ "dependencies": {
+ "base16": "^1.0.0",
+ "lodash.curry": "^4.1.1",
+ "lodash.flow": "^3.5.0",
+ "pure-color": "^1.3.0"
+ }
+}
diff --git a/packages/react-base16-styling/src/colorConverters.js b/packages/react-base16-styling/src/colorConverters.js
new file mode 100644
index 00000000..e2960018
--- /dev/null
+++ b/packages/react-base16-styling/src/colorConverters.js
@@ -0,0 +1,30 @@
+export function yuv2rgb(yuv) {
+ var y = yuv[0],
+ u = yuv[1],
+ v = yuv[2],
+ r,
+ g,
+ b;
+
+ r = y * 1 + u * 0 + v * 1.13983;
+ g = y * 1 + u * -0.39465 + v * -0.5806;
+ b = y * 1 + u * 2.02311 + v * 0;
+
+ r = Math.min(Math.max(0, r), 1);
+ g = Math.min(Math.max(0, g), 1);
+ b = Math.min(Math.max(0, b), 1);
+
+ return [r * 255, g * 255, b * 255];
+}
+
+export function rgb2yuv(rgb) {
+ var r = rgb[0] / 255,
+ g = rgb[1] / 255,
+ b = rgb[2] / 255;
+
+ var y = r * 0.299 + g * 0.587 + b * 0.114;
+ var u = r * -0.14713 + g * -0.28886 + b * 0.436;
+ var v = r * 0.615 + g * -0.51499 + b * -0.10001;
+
+ return [y, u, v];
+}
diff --git a/packages/react-base16-styling/src/index.js b/packages/react-base16-styling/src/index.js
new file mode 100644
index 00000000..621ccf39
--- /dev/null
+++ b/packages/react-base16-styling/src/index.js
@@ -0,0 +1,212 @@
+import curry from 'lodash.curry';
+import * as base16 from 'base16';
+import rgb2hex from 'pure-color/convert/rgb2hex';
+import parse from 'pure-color/parse';
+import flow from 'lodash.flow';
+import { yuv2rgb, rgb2yuv } from './colorConverters';
+
+const DEFAULT_BASE16 = base16.default;
+
+const BASE16_KEYS = Object.keys(DEFAULT_BASE16);
+
+// we need a correcting factor, so that a dark, but not black background color
+// converts to bright enough inversed color
+const flip = x => (x < 0.25 ? 1 : x < 0.5 ? 0.9 - x : 1.1 - x);
+
+const invertColor = flow(
+ parse,
+ rgb2yuv,
+ ([y, u, v]) => [flip(y), u, v],
+ yuv2rgb,
+ rgb2hex
+);
+
+const merger = function merger(styling) {
+ return prevStyling => ({
+ className: [prevStyling.className, styling.className]
+ .filter(Boolean)
+ .join(' '),
+ style: { ...(prevStyling.style || {}), ...(styling.style || {}) }
+ });
+};
+
+const mergeStyling = function mergeStyling(customStyling, defaultStyling) {
+ if (customStyling === undefined) {
+ return defaultStyling;
+ }
+ if (defaultStyling === undefined) {
+ return customStyling;
+ }
+
+ const customType = typeof customStyling;
+ const defaultType = typeof defaultStyling;
+
+ switch (customType) {
+ case 'string':
+ switch (defaultType) {
+ case 'string':
+ return [defaultStyling, customStyling].filter(Boolean).join(' ');
+ case 'object':
+ return merger({ className: customStyling, style: defaultStyling });
+ case 'function':
+ return (styling, ...args) =>
+ merger({
+ className: customStyling
+ })(defaultStyling(styling, ...args));
+ }
+ break;
+ case 'object':
+ switch (defaultType) {
+ case 'string':
+ return merger({ className: defaultStyling, style: customStyling });
+ case 'object':
+ return { ...defaultStyling, ...customStyling };
+ case 'function':
+ return (styling, ...args) =>
+ merger({
+ style: customStyling
+ })(defaultStyling(styling, ...args));
+ }
+ break;
+ case 'function':
+ switch (defaultType) {
+ case 'string':
+ return (styling, ...args) =>
+ customStyling(
+ merger(styling)({
+ className: defaultStyling
+ }),
+ ...args
+ );
+ case 'object':
+ return (styling, ...args) =>
+ customStyling(
+ merger(styling)({
+ style: defaultStyling
+ }),
+ ...args
+ );
+ case 'function':
+ return (styling, ...args) =>
+ customStyling(defaultStyling(styling, ...args), ...args);
+ }
+ }
+};
+
+const mergeStylings = function mergeStylings(customStylings, defaultStylings) {
+ const keys = Object.keys(defaultStylings);
+ for (const key in customStylings) {
+ if (keys.indexOf(key) === -1) keys.push(key);
+ }
+
+ return keys.reduce(
+ (mergedStyling, key) => (
+ (mergedStyling[key] = mergeStyling(
+ customStylings[key],
+ defaultStylings[key]
+ )),
+ mergedStyling
+ ),
+ {}
+ );
+};
+
+const getStylingByKeys = (mergedStyling, keys, ...args) => {
+ if (keys === null) {
+ return mergedStyling;
+ }
+
+ if (!Array.isArray(keys)) {
+ keys = [keys];
+ }
+
+ const styles = keys.map(key => mergedStyling[key]).filter(Boolean);
+
+ const props = styles.reduce(
+ (obj, s) => {
+ if (typeof s === 'string') {
+ obj.className = [obj.className, s].filter(Boolean).join(' ');
+ } else if (typeof s === 'object') {
+ obj.style = { ...obj.style, ...s };
+ } else if (typeof s === 'function') {
+ obj = { ...obj, ...s(obj, ...args) };
+ }
+
+ return obj;
+ },
+ { className: '', style: {} }
+ );
+
+ if (!props.className) {
+ delete props.className;
+ }
+
+ if (Object.keys(props.style).length === 0) {
+ delete props.style;
+ }
+
+ return props;
+};
+
+export const invertTheme = theme =>
+ Object.keys(theme).reduce(
+ (t, key) => (
+ (t[key] = /^base/.test(key)
+ ? invertColor(theme[key])
+ : key === 'scheme'
+ ? theme[key] + ':inverted'
+ : theme[key]),
+ t
+ ),
+ {}
+ );
+
+export const createStyling = curry(
+ (getStylingFromBase16, options = {}, themeOrStyling = {}, ...args) => {
+ const { defaultBase16 = DEFAULT_BASE16, base16Themes = null } = options;
+
+ const base16Theme = getBase16Theme(themeOrStyling, base16Themes);
+ if (base16Theme) {
+ themeOrStyling = {
+ ...base16Theme,
+ ...themeOrStyling
+ };
+ }
+
+ const theme = BASE16_KEYS.reduce(
+ (t, key) => ((t[key] = themeOrStyling[key] || defaultBase16[key]), t),
+ {}
+ );
+
+ const customStyling = Object.keys(themeOrStyling).reduce(
+ (s, key) =>
+ BASE16_KEYS.indexOf(key) === -1
+ ? ((s[key] = themeOrStyling[key]), s)
+ : s,
+ {}
+ );
+
+ const defaultStyling = getStylingFromBase16(theme);
+
+ const mergedStyling = mergeStylings(customStyling, defaultStyling);
+
+ return curry(getStylingByKeys, 2)(mergedStyling, ...args);
+ },
+ 3
+);
+
+export const getBase16Theme = (theme, base16Themes) => {
+ if (theme && theme.extend) {
+ theme = theme.extend;
+ }
+
+ if (typeof theme === 'string') {
+ const [themeName, modifier] = theme.split(':');
+ theme = (base16Themes || {})[themeName] || base16[themeName];
+ if (modifier === 'inverted') {
+ theme = invertTheme(theme);
+ }
+ }
+
+ return theme && theme.hasOwnProperty('base00') ? theme : undefined;
+};
diff --git a/packages/react-base16-styling/test/index.test.js b/packages/react-base16-styling/test/index.test.js
new file mode 100644
index 00000000..82bc4469
--- /dev/null
+++ b/packages/react-base16-styling/test/index.test.js
@@ -0,0 +1,189 @@
+import { createStyling, invertTheme, getBase16Theme } from '../src';
+import apathy from 'base16/lib/apathy';
+
+const base16Theme = {
+ scheme: 'myscheme',
+ author: 'me',
+ base00: '#000000',
+ base01: '#222222',
+ base02: '#444444',
+ base03: '#666666',
+ base04: '#999999',
+ base05: '#bbbbbb',
+ base06: '#dddddd',
+ base07: '#ffffff',
+ base08: '#ff0000',
+ base09: '#ff9900',
+ base0A: '#ffff00',
+ base0B: '#999900',
+ base0C: '#009999',
+ base0D: '#009900',
+ base0E: '#9999ff',
+ base0F: '#ff0099'
+};
+
+const invertedBase16Theme = {
+ scheme: 'myscheme:inverted',
+ author: 'me',
+ base00: '#ffffff',
+ base01: '#ffffff',
+ base02: '#a2a1a2',
+ base03: '#807f80',
+ base04: '#807f80',
+ base05: '#5e5d5e',
+ base06: '#3c3b3c',
+ base07: '#1a191a',
+ base08: '#ff4d4d',
+ base09: '#cb6500',
+ base0A: '#545400',
+ base0B: '#a2a20a',
+ base0C: '#0fa8a8',
+ base0D: '#32cb32',
+ base0E: '#6868ce',
+ base0F: '#ff2ac3'
+};
+
+const apathyInverted = {
+ author: 'jannik siebert (https://github.com/janniks)',
+ base00: '#efffff',
+ base01: '#e3ffff',
+ base02: '#daffff',
+ base03: '#67a49a',
+ base04: '#66a399',
+ base05: '#51857c',
+ base06: '#3c635d',
+ base07: '#2a3f3c',
+ base08: '#2f8779',
+ base09: '#4e89a6',
+ base0A: '#8391db',
+ base0B: '#b167bf',
+ base0C: '#c8707e',
+ base0D: '#a7994f',
+ base0E: '#469038',
+ base0F: '#3a9257',
+ scheme: 'apathy:inverted'
+};
+
+const getStylingFromBase16 = base16 => ({
+ testClass: 'testClass',
+ testStyle: {
+ color: base16.base00
+ },
+ testFunc: ({ style }, arg) => ({
+ className: 'testClass--' + arg,
+ style: {
+ ...style,
+ width: 0,
+ color: base16.base00
+ }
+ }),
+ baseStyle: {
+ color: 'red'
+ },
+ additionalStyle: {
+ border: 0
+ }
+});
+
+test('invertTheme', () => {
+ expect(invertTheme(base16Theme)).toEqual(invertedBase16Theme);
+});
+
+test('getBase16Theme', () => {
+ expect(getBase16Theme('apathy')).toEqual(apathy);
+ expect(getBase16Theme({ extend: 'apathy' })).toEqual(apathy);
+ expect(getBase16Theme('apathy:inverted')).toEqual(apathyInverted);
+ expect(getBase16Theme({})).toBe(undefined);
+});
+
+test('createStyling (default)', () => {
+ const styling = createStyling(getStylingFromBase16, {
+ defaultBase16: apathy
+ });
+ const defaultStyling = styling(undefined);
+
+ expect(defaultStyling('testClass')).toEqual({ className: 'testClass' });
+ expect(defaultStyling('testStyle')).toEqual({
+ style: { color: apathy.base00 }
+ });
+ expect(defaultStyling('testFunc', 'mod')).toEqual({
+ className: 'testClass--mod',
+ style: {
+ width: 0,
+ color: apathy.base00
+ }
+ });
+});
+
+test('createStyling (custom)', () => {
+ const styling = createStyling(getStylingFromBase16, {
+ defaultBase16: apathy
+ });
+ let customStyling = styling({
+ testClass: 'customClass',
+ testStyle: { height: 0 },
+ testFunc: (styling, arg) => ({
+ className: styling.className + ' customClass--' + arg,
+ style: {
+ ...styling.style,
+ border: 0
+ }
+ })
+ });
+
+ expect(customStyling('testClass')).toEqual({
+ className: 'testClass customClass'
+ });
+ expect(customStyling('testStyle')).toEqual({
+ style: { color: apathy.base00, height: 0 }
+ });
+ expect(customStyling('testFunc', 'mod')).toEqual({
+ className: 'testClass--mod customClass--mod',
+ style: {
+ width: 0,
+ color: apathy.base00,
+ border: 0
+ }
+ });
+
+ customStyling = styling({
+ testClass: () => ({
+ className: 'customClass'
+ }),
+ testStyle: () => ({
+ style: {
+ border: 0
+ }
+ })
+ });
+
+ expect(customStyling('testClass')).toEqual({ className: 'customClass' });
+ expect(customStyling('testStyle')).toEqual({ style: { border: 0 } });
+});
+
+test('createStyling (multiple)', () => {
+ const styling = createStyling(getStylingFromBase16, {
+ defaultBase16: apathy
+ });
+ let customStyling = styling({
+ baseStyle: ({ style }) => ({ style: { ...style, color: 'blue' } })
+ });
+
+ expect(customStyling(['baseStyle', 'additionalStyle'])).toEqual({
+ style: {
+ color: 'blue',
+ border: 0
+ }
+ });
+
+ customStyling = styling({
+ additionalStyle: ({ style }) => ({ style: { ...style, border: 1 } })
+ });
+
+ expect(customStyling(['baseStyle', 'additionalStyle'])).toEqual({
+ style: {
+ color: 'red',
+ border: 1
+ }
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index 352c788b..47c30c22 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10539,7 +10539,7 @@ lodash.clonedeep@4.5.0, lodash.clonedeep@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
-lodash.curry@^4.0.1:
+lodash.curry@^4.0.1, lodash.curry@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170"
integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA=
@@ -10566,7 +10566,7 @@ lodash.flattendeep@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
-lodash.flow@^3.3.0:
+lodash.flow@^3.3.0, lodash.flow@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a"
integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=
@@ -12956,7 +12956,7 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-pure-color@^1.2.0:
+pure-color@^1.2.0, pure-color@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e"
integrity sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4=
@@ -13136,16 +13136,6 @@ react-base16-styling@^0.5.1, react-base16-styling@^0.5.3:
lodash.flow "^3.3.0"
pure-color "^1.2.0"
-react-base16-styling@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c"
- integrity sha1-7yFW1mz0E5aVyKFniGy2nqZgeSw=
- dependencies:
- base16 "^1.0.0"
- lodash.curry "^4.0.1"
- lodash.flow "^3.3.0"
- pure-color "^1.2.0"
-
react-bootstrap@^0.30.6:
version "0.30.10"
resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.30.10.tgz#dbba6909595f2af4d91937db0f96ec8c2df2d1a8"