that's cool

This commit is contained in:
Nathan Bierema 2020-08-16 01:07:13 -04:00
parent 23cb57a73f
commit 9301ea438d
7 changed files with 200 additions and 111 deletions

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
setupFiles: ['devui/tests/setup.js'], projects: ['<rootDir>/packages/*'],
}; };

View File

@ -1,6 +1,11 @@
{ {
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-typescript": "^7.10.4",
"@types/jest": "^26.0.9", "@types/jest": "^26.0.9",
"@typescript-eslint/eslint-plugin": "^3.9.0", "@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "^3.9.0", "@typescript-eslint/parser": "^3.9.0",
@ -16,6 +21,7 @@
"lerna": "^3.22.1", "lerna": "^3.22.1",
"lint-staged": "^10.2.11", "lint-staged": "^10.2.11",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"rimraf": "^3.0.2",
"ts-jest": "^26.2.0", "ts-jest": "^26.2.0",
"typescript": "^3.9.7" "typescript": "^3.9.7"
}, },

View File

@ -37,19 +37,13 @@
}, },
"dependencies": { "dependencies": {
"@types/base16": "^1.0.2", "@types/base16": "^1.0.2",
"@types/color-string": "^1.5.0",
"@types/lodash.curry": "^4.1.6", "@types/lodash.curry": "^4.1.6",
"base16": "^1.0.0", "base16": "^1.0.0",
"color-string": "^1.5.3", "color": "^3.1.2",
"csstype": "^3.0.2", "csstype": "^3.0.2",
"lodash.curry": "^4.1.1" "lodash.curry": "^4.1.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.10.5", "@types/color": "^3.0.1"
"@babel/core": "^7.11.1",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-typescript": "^7.10.4",
"rimraf": "^3.0.2"
} }
} }

View File

@ -1,8 +1,17 @@
import curry from 'lodash.curry';
import * as base16 from 'base16'; import * as base16 from 'base16';
import { Base16Theme } from 'base16'; import { Base16Theme } from 'base16';
import { get, to } from 'color-string'; import Color from 'color';
import { Color, yuv2rgb, rgb2yuv } from './colorConverters'; import * as CSS from 'csstype';
import curry from 'lodash.curry';
import { Color as ColorTuple, yuv2rgb, rgb2yuv } from './colorConverters';
import {
Styling,
StylingConfig,
StylingFunction,
StylingValue,
StylingValueFunction,
Theme,
} from './types';
const DEFAULT_BASE16 = base16.default; const DEFAULT_BASE16 = base16.default;
@ -12,17 +21,16 @@ const BASE16_KEYS = Object.keys(DEFAULT_BASE16);
// converts to bright enough inversed color // converts to bright enough inversed color
const flip = (x: number) => (x < 0.25 ? 1 : x < 0.5 ? 0.9 - x : 1.1 - x); const flip = (x: number) => (x < 0.25 ? 1 : x < 0.5 ? 0.9 - x : 1.1 - x);
const invertColor = (colorString: string) => { const invertColor = (hexString: string) => {
const color = get.rgb(colorString); const color = Color(hexString);
if (!color) throw new Error(`Unable to parse color: ${colorString}`); const [y, u, v] = rgb2yuv(color.array() as ColorTuple);
const [y, u, v] = rgb2yuv([color[0], color[1], color[2]]); const flippedYuv: ColorTuple = [flip(y), u, v];
const flippedYuv: Color = [flip(y), u, v];
const rgb = yuv2rgb(flippedYuv); const rgb = yuv2rgb(flippedYuv);
return to.hex(rgb); return Color.rgb(rgb).hex();
}; };
const merger = function merger(styling) { const merger = (styling: Styling) => {
return (prevStyling) => ({ return (prevStyling: Styling) => ({
className: [prevStyling.className, styling.className] className: [prevStyling.className, styling.className]
.filter(Boolean) .filter(Boolean)
.join(' '), .join(' '),
@ -30,7 +38,10 @@ const merger = function merger(styling) {
}); });
}; };
const mergeStyling = function mergeStyling(customStyling, defaultStyling) { const mergeStyling = function mergeStyling(
customStyling: StylingValue,
defaultStyling: StylingValue
): StylingValue | undefined {
if (customStyling === undefined) { if (customStyling === undefined) {
return defaultStyling; return defaultStyling;
} }
@ -47,53 +58,68 @@ const mergeStyling = function mergeStyling(customStyling, defaultStyling) {
case 'string': case 'string':
return [defaultStyling, customStyling].filter(Boolean).join(' '); return [defaultStyling, customStyling].filter(Boolean).join(' ');
case 'object': case 'object':
return merger({ className: customStyling, style: defaultStyling }); return merger({
className: customStyling as string,
style: defaultStyling as CSS.Properties<string | number>,
});
case 'function': case 'function':
return (styling, ...args) => return (styling: Styling, ...args: any[]) =>
merger({ merger({
className: customStyling, className: customStyling as string,
})(defaultStyling(styling, ...args)); })((defaultStyling as StylingValueFunction)(styling, ...args));
} }
break; break;
case 'object': case 'object':
switch (defaultType) { switch (defaultType) {
case 'string': case 'string':
return merger({ className: defaultStyling, style: customStyling }); return merger({
className: defaultStyling as string,
style: customStyling as CSS.Properties<string | number>,
});
case 'object': case 'object':
return { ...defaultStyling, ...customStyling }; return {
...(defaultStyling as CSS.Properties<string | number>),
...(customStyling as CSS.Properties<string | number>),
};
case 'function': case 'function':
return (styling, ...args) => return (styling: Styling, ...args: any[]) =>
merger({ merger({
style: customStyling, style: customStyling as CSS.Properties<string | number>,
})(defaultStyling(styling, ...args)); })((defaultStyling as StylingValueFunction)(styling, ...args));
} }
break; break;
case 'function': case 'function':
switch (defaultType) { switch (defaultType) {
case 'string': case 'string':
return (styling, ...args) => return (styling, ...args) =>
customStyling( (customStyling as StylingValueFunction)(
merger(styling)({ merger(styling)({
className: defaultStyling, className: defaultStyling as string,
}), }),
...args ...args
); );
case 'object': case 'object':
return (styling, ...args) => return (styling, ...args) =>
customStyling( (customStyling as StylingValueFunction)(
merger(styling)({ merger(styling)({
style: defaultStyling, style: defaultStyling as CSS.Properties<string | number>,
}), }),
...args ...args
); );
case 'function': case 'function':
return (styling, ...args) => return (styling, ...args) =>
customStyling(defaultStyling(styling, ...args), ...args); (customStyling as StylingValueFunction)(
(defaultStyling as StylingValueFunction)(styling, ...args),
...args
);
} }
} }
}; };
const mergeStylings = function mergeStylings(customStylings, defaultStylings) { const mergeStylings = (
customStylings: StylingConfig,
defaultStylings: StylingConfig
): StylingConfig => {
const keys = Object.keys(defaultStylings); const keys = Object.keys(defaultStylings);
for (const key in customStylings) { for (const key in customStylings) {
if (keys.indexOf(key) === -1) keys.push(key); if (keys.indexOf(key) === -1) keys.push(key);
@ -101,19 +127,23 @@ const mergeStylings = function mergeStylings(customStylings, defaultStylings) {
return keys.reduce( return keys.reduce(
(mergedStyling, key) => ( (mergedStyling, key) => (
(mergedStyling[key] = mergeStyling( (mergedStyling[key as keyof StylingConfig] = mergeStyling(
customStylings[key], customStylings[key] as StylingValue,
defaultStylings[key] defaultStylings[key] as StylingValue
)), ) as StylingValue),
mergedStyling mergedStyling
), ),
{} {} as StylingConfig
); );
}; };
const getStylingByKeys = (mergedStyling, keys, ...args) => { const getStylingByKeys = (
mergedStyling: StylingConfig,
keys: string | string[],
...args: any[]
): Styling => {
if (keys === null) { if (keys === null) {
return mergedStyling; return mergedStyling as Styling;
} }
if (!Array.isArray(keys)) { if (!Array.isArray(keys)) {
@ -148,42 +178,55 @@ const getStylingByKeys = (mergedStyling, keys, ...args) => {
return props; return props;
}; };
export const invertBase16Theme = (base16Theme: Base16Theme) => export const invertBase16Theme = (base16Theme: Base16Theme): Base16Theme =>
Object.keys(base16Theme).reduce( Object.keys(base16Theme).reduce(
(t, key) => ( (t, key) => (
(t[key] = /^base/.test(key) (t[key as keyof Base16Theme] = /^base/.test(key)
? invertColor(base16Theme[key]) ? invertColor(base16Theme[key as keyof Base16Theme])
: key === 'scheme' : key === 'scheme'
? base16Theme[key] + ':inverted' ? base16Theme[key] + ':inverted'
: base16Theme[key]), : base16Theme[key as keyof Base16Theme]),
t t
), ),
{} {} as Base16Theme
); );
export const createStyling = curry( export const createStyling = curry(
(getStylingFromBase16, options = {}, themeOrStyling = {}, ...args) => { (
getStylingFromBase16: (base16Theme: Base16Theme) => StylingConfig,
options: {
defaultBase16?: Base16Theme;
base16Themes?: { [themeName: string]: Base16Theme };
} = {},
themeOrStyling: Theme = {},
...args
): StylingFunction => {
const { defaultBase16 = DEFAULT_BASE16, base16Themes = null } = options; const { defaultBase16 = DEFAULT_BASE16, base16Themes = null } = options;
const base16Theme = getBase16Theme(themeOrStyling, base16Themes); const base16Theme = getBase16Theme(themeOrStyling, base16Themes);
if (base16Theme) { if (base16Theme) {
themeOrStyling = { themeOrStyling = {
...base16Theme, ...base16Theme,
...themeOrStyling, ...(themeOrStyling as Base16Theme | StylingConfig),
}; };
} }
const theme = BASE16_KEYS.reduce( const theme = BASE16_KEYS.reduce(
(t, key) => ((t[key] = themeOrStyling[key] || defaultBase16[key]), t), (t, key) => (
{} (t[key as keyof Base16Theme] =
(themeOrStyling as Base16Theme)[key as keyof Base16Theme] ||
defaultBase16[key as keyof Base16Theme]),
t
),
{} as Base16Theme
); );
const customStyling = Object.keys(themeOrStyling).reduce( const customStyling = Object.keys(themeOrStyling).reduce(
(s, key) => (s, key) =>
BASE16_KEYS.indexOf(key) === -1 BASE16_KEYS.indexOf(key) === -1
? ((s[key] = themeOrStyling[key]), s) ? ((s[key] = (themeOrStyling as StylingConfig)[key]), s)
: s, : s,
{} {} as StylingConfig
); );
const defaultStyling = getStylingFromBase16(theme); const defaultStyling = getStylingFromBase16(theme);
@ -195,38 +238,53 @@ export const createStyling = curry(
3 3
); );
export const getBase16Theme = (theme, base16Themes) => { const isStylingConfig = (theme: Theme): theme is StylingConfig =>
if (theme && theme.extend) { !!(theme as StylingConfig).extend;
theme = theme.extend;
export const getBase16Theme = (
theme: Theme,
base16Themes?: { [themeName: string]: Base16Theme } | null
): Base16Theme | undefined => {
if (theme && isStylingConfig(theme) && theme.extend) {
theme = theme.extend as string | Base16Theme;
} }
if (typeof theme === 'string') { if (typeof theme === 'string') {
const [themeName, modifier] = theme.split(':'); const [themeName, modifier] = theme.split(':');
theme = (base16Themes || {})[themeName] || base16[themeName]; if (base16Themes) {
theme = base16Themes[themeName];
} else {
theme = base16[themeName as keyof typeof base16];
}
if (modifier === 'inverted') { if (modifier === 'inverted') {
theme = invertBase16Theme(theme); theme = invertBase16Theme(theme);
} }
} }
return theme && Object.prototype.hasOwnProperty.call(theme, 'base00') return theme && Object.prototype.hasOwnProperty.call(theme, 'base00')
? theme ? (theme as Base16Theme)
: undefined; : undefined;
}; };
export const invertTheme = (theme) => { export const invertTheme = (theme: Theme): Theme => {
if (typeof theme === 'string') { if (typeof theme === 'string') {
return `${theme}:inverted`; return `${theme}:inverted`;
} }
if (theme && theme.extend) { if (theme && isStylingConfig(theme) && theme.extend) {
if (typeof theme.extend === 'string') { if (typeof theme.extend === 'string') {
return { ...theme, extend: `${theme.extend}:inverted` }; return { ...theme, extend: `${theme.extend}:inverted` };
} }
return { ...theme, extend: invertBase16Theme(theme.extend) }; return {
...theme,
extend: invertBase16Theme(theme.extend as Base16Theme),
};
} }
if (theme) { if (theme) {
return invertBase16Theme(theme); return invertBase16Theme(theme as Base16Theme);
} }
return theme;
}; };

View File

@ -1,11 +1,32 @@
import { Base16Theme } from 'base16'; import { Base16Theme } from 'base16';
import * as CSS from 'csstype'; import * as CSS from 'csstype';
export type StylingValue = string | CSS.Properties | export interface Styling {
className?: string;
export interface StylingConfig { style?: CSS.Properties<string | number>;
extend?: string | Base16Theme;
[name: string]: StylingValue;
} }
export type StylingValueFunction = (
styling: Styling,
...rest: any[]
) => Styling;
export type StylingValue =
| string
| CSS.Properties<string | number>
| StylingValueFunction;
export type StylingConfig = {
// Should actually only be string | Base16Theme
extend?: string | Base16Theme | StylingValue;
} & {
// Should actually only be StylingValue
[name: string]: StylingValue | string | Base16Theme;
};
export type Theme = string | Base16Theme | StylingConfig; export type Theme = string | Base16Theme | StylingConfig;
export type StylingFunction = (
keys: string | string[],
...rest: any[]
) => Styling;

View File

@ -1,5 +1,6 @@
import { createStyling, invertBase16Theme, getBase16Theme } from '../src'; import { createStyling, invertBase16Theme, getBase16Theme } from '../src';
import { apathy } from 'base16'; import { apathy, Base16Theme } from 'base16';
import { Styling, StylingConfig } from '../src/types';
const base16Theme = { const base16Theme = {
scheme: 'myscheme', scheme: 'myscheme',
@ -25,52 +26,52 @@ const base16Theme = {
const invertedBase16Theme = { const invertedBase16Theme = {
scheme: 'myscheme:inverted', scheme: 'myscheme:inverted',
author: 'me', author: 'me',
base00: '#ffffff', base00: '#FFFFFF',
base01: '#ffffff', base01: '#FFFFFF',
base02: '#a2a1a2', base02: '#A2A1A2',
base03: '#807f80', base03: '#807F80',
base04: '#807f80', base04: '#807F80',
base05: '#5e5d5e', base05: '#5E5D5E',
base06: '#3c3b3c', base06: '#3C3B3C',
base07: '#1a191a', base07: '#1A191A',
base08: '#ff4d4d', base08: '#FF4D4D',
base09: '#cb6500', base09: '#CB6500',
base0A: '#545400', base0A: '#545400',
base0B: '#a2a20a', base0B: '#A2A20A',
base0C: '#0fa8a8', base0C: '#0FA8A8',
base0D: '#32cb32', base0D: '#32CB32',
base0E: '#6868ce', base0E: '#6868CE',
base0F: '#ff2ac3', base0F: '#FF2AC3',
}; };
const apathyInverted = { const apathyInverted = {
author: 'jannik siebert (https://github.com/janniks)', author: 'jannik siebert (https://github.com/janniks)',
base00: '#efffff', base00: '#EFFFFF',
base01: '#e3ffff', base01: '#E3FFFF',
base02: '#daffff', base02: '#DAFFFF',
base03: '#67a49a', base03: '#67A49A',
base04: '#66a399', base04: '#66A399',
base05: '#51857c', base05: '#51857C',
base06: '#3c635d', base06: '#3C635D',
base07: '#2a3f3c', base07: '#2A3F3C',
base08: '#2f8779', base08: '#2F8779',
base09: '#4e89a6', base09: '#4E89A6',
base0A: '#8391db', base0A: '#8391DB',
base0B: '#b167bf', base0B: '#B167BF',
base0C: '#c8707e', base0C: '#C8707E',
base0D: '#a7994f', base0D: '#A7994F',
base0E: '#469038', base0E: '#469038',
base0F: '#3a9257', base0F: '#3A9257',
scheme: 'apathy:inverted', scheme: 'apathy:inverted',
}; };
const getStylingFromBase16 = (base16) => ({ const getStylingFromBase16 = (base16: Base16Theme): StylingConfig => ({
testClass: 'testClass', testClass: 'testClass',
testStyle: { testStyle: {
color: base16.base00, color: base16.base00,
}, },
testFunc: ({ style }, arg) => ({ testFunc: ({ style }, arg: string) => ({
className: 'testClass--' + arg, className: `testClass--${arg}`,
style: { style: {
...style, ...style,
width: 0, width: 0,
@ -93,7 +94,7 @@ test('getBase16Theme', () => {
expect(getBase16Theme('apathy')).toEqual(apathy); expect(getBase16Theme('apathy')).toEqual(apathy);
expect(getBase16Theme({ extend: 'apathy' })).toEqual(apathy); expect(getBase16Theme({ extend: 'apathy' })).toEqual(apathy);
expect(getBase16Theme('apathy:inverted')).toEqual(apathyInverted); expect(getBase16Theme('apathy:inverted')).toEqual(apathyInverted);
expect(getBase16Theme({})).toBe(undefined); expect(getBase16Theme({})).toBeUndefined();
}); });
test('createStyling (default)', () => { test('createStyling (default)', () => {
@ -122,8 +123,8 @@ test('createStyling (custom)', () => {
let customStyling = styling({ let customStyling = styling({
testClass: 'customClass', testClass: 'customClass',
testStyle: { height: 0 }, testStyle: { height: 0 },
testFunc: (styling, arg) => ({ testFunc: (styling: Styling, arg: string) => ({
className: styling.className + ' customClass--' + arg, className: `${styling.className!} customClass--${arg}`,
style: { style: {
...styling.style, ...styling.style,
border: 0, border: 0,

View File

@ -3055,15 +3055,24 @@
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999" resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999"
integrity sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ== integrity sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ==
"@types/color-name@^1.1.1": "@types/color-convert@*":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-1.9.0.tgz#bfa8203e41e7c65471e9841d7e306a7cd8b5172d"
integrity sha512-OKGEfULrvSL2VRbkl/gnjjgbbF7ycIlpSsX7Nkab4MOWi5XxmgBYvuiQ7lcCFY5cPDz7MUNaKgxte2VRmtr4Fg==
dependencies:
"@types/color-name" "*"
"@types/color-name@*", "@types/color-name@^1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/color-string@^1.5.0": "@types/color@^3.0.1":
version "1.5.0" version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/color-string/-/color-string-1.5.0.tgz#0a9f3ee6bc9bd4f89693fc3f2bc30d47ad68afc3" resolved "https://registry.yarnpkg.com/@types/color/-/color-3.0.1.tgz#2900490ed04da8116c5058cd5dba3572d5a25071"
integrity sha512-17/8LWbkfvoKgfnnBvKbUnPTzPtzBlSELf5FPvVt9dRTQW88igOYZZ78us9dmbY1301VtTBEnuPTKspvmtGDxA== integrity sha512-oeUWVaAwI+xINDUx+3F2vJkl/vVB03VChFF/Gl3iQCdbcakjuoJyMOba+3BXRtnBhxZ7uBYqQBi9EpLnvSoztA==
dependencies:
"@types/color-convert" "*"
"@types/eslint-visitor-keys@^1.0.0": "@types/eslint-visitor-keys@^1.0.0":
version "1.0.0" version "1.0.0"
@ -5454,7 +5463,7 @@ color-name@^1.0.0, color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.5.2, color-string@^1.5.3: color-string@^1.5.2:
version "1.5.3" version "1.5.3"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc"
integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==