Merge branch 'next-colors' into 'next'
Next: colors See merge request soapbox-pub/soapbox-fe!1126
This commit is contained in:
commit
8970e4e3db
|
@ -1 +0,0 @@
|
||||||
<svg width="1754" height="1336" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter x="-18.1%" y="-15.3%" width="136.3%" height="130.7%" filterUnits="objectBoundingBox" id="c"><feGaussianBlur stdDeviation="50" in="SourceGraphic"/></filter><filter x="-16.5%" y="-11.7%" width="133%" height="123.3%" filterUnits="objectBoundingBox" id="d"><feGaussianBlur stdDeviation="50" in="SourceGraphic"/></filter><path id="a" d="M0 0h1754v1336H0z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path d="M1257.79 335.852C1262 527.117 897.55 530.28 792.32 977.19 600.48 981.41 435.29 545.31 431.08 354.046 426.871 162.782 578.976 4.31 770.815.088c191.844-4.222 482.764 144.5 486.974 335.764Z" fill="#E7F5FF" fill-rule="nonzero" filter="url(#c)" transform="translate(309.54 -367.538)"/><path d="M71.127 1126.654c206.164 179.412 502.452 211.232 661.777 71.072 159.325-140.163 295.165-510.155 8.23-504.412-320.079 6.405-381.35-817.422-540.675-677.258-31 368-335.497 931.182-129.332 1110.598Z" fill="#5448EE" fill-rule="nonzero" filter="url(#d)" transform="translate(309.54 -141.056)" opacity=".1"/></g></g></svg>
|
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -41,6 +41,47 @@ export const makeDefaultConfig = features => {
|
||||||
banner: '',
|
banner: '',
|
||||||
brandColor: '', // Empty
|
brandColor: '', // Empty
|
||||||
accentColor: '',
|
accentColor: '',
|
||||||
|
colors: ImmutableMap({
|
||||||
|
gray: ImmutableMap({
|
||||||
|
50: '#f9fafb',
|
||||||
|
100: '#f3f4f6',
|
||||||
|
200: '#e5e7eb',
|
||||||
|
300: '#d1d5db',
|
||||||
|
400: '#9ca3af',
|
||||||
|
500: '#6b7280',
|
||||||
|
600: '#4b5563',
|
||||||
|
700: '#374151',
|
||||||
|
800: '#1f2937',
|
||||||
|
900: '#111827',
|
||||||
|
}),
|
||||||
|
success: ImmutableMap({
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#22c55e',
|
||||||
|
600: '#16a34a',
|
||||||
|
700: '#15803d',
|
||||||
|
800: '#166534',
|
||||||
|
900: '#14532d',
|
||||||
|
}),
|
||||||
|
danger: ImmutableMap({
|
||||||
|
50: '#fef2f2',
|
||||||
|
100: '#fee2e2',
|
||||||
|
200: '#fecaca',
|
||||||
|
300: '#fca5a5',
|
||||||
|
400: '#f87171',
|
||||||
|
500: '#ef4444',
|
||||||
|
600: '#dc2626',
|
||||||
|
700: '#b91c1c',
|
||||||
|
800: '#991b1b',
|
||||||
|
900: '#7f1d1d',
|
||||||
|
}),
|
||||||
|
'gradient-purple': '#b8a3f9',
|
||||||
|
'gradient-blue': '#9bd5ff',
|
||||||
|
'sea-blue': '#2feecc',
|
||||||
|
}),
|
||||||
customCss: ImmutableList(),
|
customCss: ImmutableList(),
|
||||||
promoPanel: ImmutableMap({
|
promoPanel: ImmutableMap({
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
|
@ -72,7 +113,8 @@ export const getSoapboxConfig = createSelector([
|
||||||
state => state.get('soapbox'),
|
state => state.get('soapbox'),
|
||||||
state => getFeatures(state.get('instance')),
|
state => getFeatures(state.get('instance')),
|
||||||
], (soapbox, features) => {
|
], (soapbox, features) => {
|
||||||
return makeDefaultConfig(features).merge(soapbox);
|
const defaultConfig = makeDefaultConfig(features);
|
||||||
|
return soapbox.mergeDeepWith((o, n) => o || n, defaultConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function rememberSoapboxConfig(host) {
|
export function rememberSoapboxConfig(host) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { createGlobals } from 'soapbox/globals';
|
||||||
import messages from 'soapbox/locales/messages';
|
import messages from 'soapbox/locales/messages';
|
||||||
import { makeGetAccount } from 'soapbox/selectors';
|
import { makeGetAccount } from 'soapbox/selectors';
|
||||||
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||||
|
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||||
|
|
||||||
import { INTRODUCTION_VERSION } from '../actions/onboarding';
|
import { INTRODUCTION_VERSION } from '../actions/onboarding';
|
||||||
import { preload } from '../actions/preload';
|
import { preload } from '../actions/preload';
|
||||||
|
@ -83,6 +84,7 @@ const mapStateToProps = (state) => {
|
||||||
dyslexicFont: settings.get('dyslexicFont'),
|
dyslexicFont: settings.get('dyslexicFont'),
|
||||||
demetricator: settings.get('demetricator'),
|
demetricator: settings.get('demetricator'),
|
||||||
locale: validLocale(locale) ? locale : 'en',
|
locale: validLocale(locale) ? locale : 'en',
|
||||||
|
themeCss: generateThemeCss(soapboxConfig),
|
||||||
brandColor: soapboxConfig.get('brandColor'),
|
brandColor: soapboxConfig.get('brandColor'),
|
||||||
themeMode: settings.get('themeMode'),
|
themeMode: settings.get('themeMode'),
|
||||||
singleUserMode,
|
singleUserMode,
|
||||||
|
@ -103,6 +105,7 @@ class SoapboxMount extends React.PureComponent {
|
||||||
dyslexicFont: PropTypes.bool,
|
dyslexicFont: PropTypes.bool,
|
||||||
demetricator: PropTypes.bool,
|
demetricator: PropTypes.bool,
|
||||||
locale: PropTypes.string.isRequired,
|
locale: PropTypes.string.isRequired,
|
||||||
|
themeCss: PropTypes.string,
|
||||||
themeMode: PropTypes.string,
|
themeMode: PropTypes.string,
|
||||||
brandColor: PropTypes.string,
|
brandColor: PropTypes.string,
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
|
@ -139,7 +142,7 @@ class SoapboxMount extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { me, account, instanceLoaded, locale, singleUserMode } = this.props;
|
const { me, account, instanceLoaded, themeCss, locale, singleUserMode } = this.props;
|
||||||
if (me === null) return null;
|
if (me === null) return null;
|
||||||
if (me && !account) return null;
|
if (me && !account) return null;
|
||||||
if (!instanceLoaded) return null;
|
if (!instanceLoaded) return null;
|
||||||
|
@ -169,6 +172,7 @@ class SoapboxMount extends React.PureComponent {
|
||||||
<Helmet>
|
<Helmet>
|
||||||
{/* <html lang='en' className={this.props.themeMode} /> */}
|
{/* <html lang='en' className={this.props.themeMode} /> */}
|
||||||
<body className={bodyClass} />
|
<body className={bodyClass} />
|
||||||
|
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
|
||||||
<meta name='theme-color' content={this.props.brandColor} />
|
<meta name='theme-color' content={this.props.brandColor} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
import { defaultSettings } from 'soapbox/actions/settings';
|
import { defaultSettings } from 'soapbox/actions/settings';
|
||||||
import { themeColorsToCSS } from 'soapbox/utils/theme';
|
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||||
|
|
||||||
export default function SitePreview({ soapbox }) {
|
export default function SitePreview({ soapbox }) {
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export default function SitePreview({ soapbox }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={bodyClass}>
|
<div className={bodyClass}>
|
||||||
<style>{`.site-preview {${themeColorsToCSS(soapbox.get('brandColor'), soapbox.get('accentColor'))}}`}</style>
|
<style>{`.site-preview {${generateThemeCss(soapbox)}}`}</style>
|
||||||
<div className='app-holder'>
|
<div className='app-holder'>
|
||||||
<div>
|
<div>
|
||||||
<div className='ui'>
|
<div className='ui'>
|
||||||
|
|
|
@ -1,10 +1,28 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<div className='fixed top-0 inset-x-0 flex justify-center overflow-hidden pointer-events-none'>
|
<div className='fixed top-0 inset-x-0 flex justify-center overflow-hidden pointer-events-none'>
|
||||||
<div className='flex-none flex justify-center'>
|
<div className='flex-none flex justify-center'>
|
||||||
<InlineSVG src={require('../../../../images/bg-shape.svg')} className='flex-none max-w-none' />
|
<svg width='1754' height='1336' xmlns='http://www.w3.org/2000/svg'>
|
||||||
|
<defs>
|
||||||
|
<filter x='-18.1%' y='-15.3%' width='136.3%' height='130.7%' filterUnits='objectBoundingBox' id='c'>
|
||||||
|
<feGaussianBlur stdDeviation='50' in='SourceGraphic' />
|
||||||
|
</filter>
|
||||||
|
<filter x='-16.5%' y='-11.7%' width='133%' height='123.3%' filterUnits='objectBoundingBox' id='d'>
|
||||||
|
<feGaussianBlur stdDeviation='50' in='SourceGraphic' />
|
||||||
|
</filter>
|
||||||
|
<path id='a' d='M0 0h1754v1336H0z' />
|
||||||
|
</defs>
|
||||||
|
<g fill='none' fill-rule='evenodd'>
|
||||||
|
<mask id='b' fill='#fff'>
|
||||||
|
<use xlinkHref='#a' />
|
||||||
|
</mask>
|
||||||
|
<g mask='url(#b)'>
|
||||||
|
<path className='fill-bg-shape-1' d='M1257.79 335.852C1262 527.117 897.55 530.28 792.32 977.19 600.48 981.41 435.29 545.31 431.08 354.046 426.871 162.782 578.976 4.31 770.815.088c191.844-4.222 482.764 144.5 486.974 335.764Z' fill-rule='nonzero' filter='url(#c)' transform='translate(309.54 -367.538)' />
|
||||||
|
<path className='fill-bg-shape-2' d='M71.127 1126.654c206.164 179.412 502.452 211.232 661.777 71.072 159.325-140.163 295.165-510.155 8.23-504.412-320.079 6.405-381.35-817.422-540.675-677.258-31 368-335.497 931.182-129.332 1110.598Z' fill-rule='nonzero' filter='url(#d)' transform='translate(309.54 -141.056)' opacity='.1' />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -33,11 +33,13 @@ const Navbar = () => {
|
||||||
<nav className='bg-white shadow z-50 sticky top-0' ref={node}>
|
<nav className='bg-white shadow z-50 sticky top-0' ref={node}>
|
||||||
<div className='max-w-7xl mx-auto px-2 sm:px-6 lg:px-8'>
|
<div className='max-w-7xl mx-auto px-2 sm:px-6 lg:px-8'>
|
||||||
<div className='relative flex justify-between h-12 lg:h-16'>
|
<div className='relative flex justify-between h-12 lg:h-16'>
|
||||||
|
{account && (
|
||||||
<div className='absolute inset-y-0 left-0 flex items-center lg:hidden'>
|
<div className='absolute inset-y-0 left-0 flex items-center lg:hidden'>
|
||||||
<button onClick={onOpenSidebar}>
|
<button onClick={onOpenSidebar}>
|
||||||
<Avatar src={account.avatar} size={34} />
|
<Avatar src={account.avatar} size={34} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export type Rgb = { r: number, g: number, b: number };
|
||||||
|
export type Hsl = { h: number, s: number, l: number };
|
||||||
|
|
||||||
|
export type TailwindColorObject = {
|
||||||
|
[key: number]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TailwindColorPalette = {
|
||||||
|
[key: string]: TailwindColorObject | string,
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import tintify from '../colors';
|
||||||
|
|
||||||
|
const AZURE = '#0482d8';
|
||||||
|
|
||||||
|
describe('tintify()', () => {
|
||||||
|
it('generates tints from a base color', () => {
|
||||||
|
const result = tintify(AZURE);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
'100': '#e6f3fb',
|
||||||
|
'200': '#c0e0f5',
|
||||||
|
'300': '#9bcdef',
|
||||||
|
'400': '#4fa8e4',
|
||||||
|
'50': '#f2f9fd',
|
||||||
|
'500': '#0482d8',
|
||||||
|
'600': '#0475c2',
|
||||||
|
'700': '#0362a2',
|
||||||
|
'800': '#024e82',
|
||||||
|
'900': '#02406a',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,215 @@
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import { toTailwind, fromLegacyColors, expandPalette } from '../tailwind';
|
||||||
|
|
||||||
|
describe('toTailwind()', () => {
|
||||||
|
it('handles empty Soapbox config', () => {
|
||||||
|
const soapboxConfig = ImmutableMap();
|
||||||
|
const result = toTailwind(soapboxConfig);
|
||||||
|
const expected = ImmutableMap({ colors: ImmutableMap() });
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts brandColor into a Tailwind color palette', () => {
|
||||||
|
const soapboxConfig = ImmutableMap({ brandColor: '#0482d8' });
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
brandColor: '#0482d8',
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f2f9fd',
|
||||||
|
100: '#e6f3fb',
|
||||||
|
200: '#c0e0f5',
|
||||||
|
300: '#9bcdef',
|
||||||
|
400: '#4fa8e4',
|
||||||
|
500: '#0482d8',
|
||||||
|
600: '#0475c2',
|
||||||
|
700: '#0362a2',
|
||||||
|
800: '#024e82',
|
||||||
|
900: '#02406a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = toTailwind(soapboxConfig);
|
||||||
|
expect(result.toJS()).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers Tailwind colors object over legacy colors', () => {
|
||||||
|
const soapboxConfig = ImmutableMap({
|
||||||
|
brandColor: '#0482d8',
|
||||||
|
colors: ImmutableMap({
|
||||||
|
primary: ImmutableMap({
|
||||||
|
300: '#ff0000',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
brandColor: '#0482d8',
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f2f9fd',
|
||||||
|
100: '#e6f3fb',
|
||||||
|
200: '#c0e0f5',
|
||||||
|
300: '#ff0000', // <--
|
||||||
|
400: '#4fa8e4',
|
||||||
|
500: '#0482d8',
|
||||||
|
600: '#0475c2',
|
||||||
|
700: '#0362a2',
|
||||||
|
800: '#024e82',
|
||||||
|
900: '#02406a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = toTailwind(soapboxConfig);
|
||||||
|
expect(result.toJS()).toMatchObject(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fromLegacyColors()', () => {
|
||||||
|
it('converts only brandColor', () => {
|
||||||
|
const soapboxConfig = ImmutableMap({ brandColor: '#0482d8' });
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
primary: {
|
||||||
|
50: '#f2f9fd',
|
||||||
|
100: '#e6f3fb',
|
||||||
|
200: '#c0e0f5',
|
||||||
|
300: '#9bcdef',
|
||||||
|
400: '#4fa8e4',
|
||||||
|
500: '#0482d8',
|
||||||
|
600: '#0475c2',
|
||||||
|
700: '#0362a2',
|
||||||
|
800: '#024e82',
|
||||||
|
900: '#02406a',
|
||||||
|
},
|
||||||
|
// Accent color is generated from brandColor
|
||||||
|
accent: {
|
||||||
|
50: '#f3fbfd',
|
||||||
|
100: '#e7f7fa',
|
||||||
|
200: '#c3ecf4',
|
||||||
|
300: '#9fe1ed',
|
||||||
|
400: '#58cadf',
|
||||||
|
500: '#10b3d1',
|
||||||
|
600: '#0ea1bc',
|
||||||
|
700: '#0c869d',
|
||||||
|
800: '#0a6b7d',
|
||||||
|
900: '#085866',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = fromLegacyColors(soapboxConfig);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('converts both legacy colors', () => {
|
||||||
|
const soapboxConfig = ImmutableMap({
|
||||||
|
brandColor: '#0482d8',
|
||||||
|
accentColor: '#2bd110',
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
primary: {
|
||||||
|
50: '#f2f9fd',
|
||||||
|
100: '#e6f3fb',
|
||||||
|
200: '#c0e0f5',
|
||||||
|
300: '#9bcdef',
|
||||||
|
400: '#4fa8e4',
|
||||||
|
500: '#0482d8',
|
||||||
|
600: '#0475c2',
|
||||||
|
700: '#0362a2',
|
||||||
|
800: '#024e82',
|
||||||
|
900: '#02406a',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
50: '#f4fdf3',
|
||||||
|
100: '#eafae7',
|
||||||
|
200: '#caf4c3',
|
||||||
|
300: '#aaed9f',
|
||||||
|
400: '#6bdf58',
|
||||||
|
500: '#2bd110',
|
||||||
|
600: '#27bc0e',
|
||||||
|
700: '#209d0c',
|
||||||
|
800: '#1a7d0a',
|
||||||
|
900: '#156608',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = fromLegacyColors(soapboxConfig);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('expandPalette()', () => {
|
||||||
|
it('expands one color', () => {
|
||||||
|
const palette = { primary: '#0482d8' };
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
primary: {
|
||||||
|
50: '#f2f9fd',
|
||||||
|
100: '#e6f3fb',
|
||||||
|
200: '#c0e0f5',
|
||||||
|
300: '#9bcdef',
|
||||||
|
400: '#4fa8e4',
|
||||||
|
500: '#0482d8',
|
||||||
|
600: '#0475c2',
|
||||||
|
700: '#0362a2',
|
||||||
|
800: '#024e82',
|
||||||
|
900: '#02406a',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = expandPalette(palette);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('expands mixed palette', () => {
|
||||||
|
const palette = {
|
||||||
|
primary: {
|
||||||
|
50: '#f2f9fd',
|
||||||
|
100: '#e6f3fb',
|
||||||
|
200: '#c0e0f5',
|
||||||
|
300: '#9bcdef',
|
||||||
|
400: '#4fa8e4',
|
||||||
|
500: '#0482d8',
|
||||||
|
600: '#0475c2',
|
||||||
|
700: '#0362a2',
|
||||||
|
800: '#024e82',
|
||||||
|
900: '#02406a',
|
||||||
|
},
|
||||||
|
accent: '#2bd110',
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
primary: {
|
||||||
|
50: '#f2f9fd',
|
||||||
|
100: '#e6f3fb',
|
||||||
|
200: '#c0e0f5',
|
||||||
|
300: '#9bcdef',
|
||||||
|
400: '#4fa8e4',
|
||||||
|
500: '#0482d8',
|
||||||
|
600: '#0475c2',
|
||||||
|
700: '#0362a2',
|
||||||
|
800: '#024e82',
|
||||||
|
900: '#02406a',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
50: '#f4fdf3',
|
||||||
|
100: '#eafae7',
|
||||||
|
200: '#caf4c3',
|
||||||
|
300: '#aaed9f',
|
||||||
|
400: '#6bdf58',
|
||||||
|
500: '#2bd110',
|
||||||
|
600: '#27bc0e',
|
||||||
|
700: '#209d0c',
|
||||||
|
800: '#1a7d0a',
|
||||||
|
900: '#156608',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = expandPalette(palette);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Javis V. Pérez
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Adapted from:
|
||||||
|
// https://github.com/javisperez/tailwindcolorshades/blob/master/src/composables/colors.ts
|
||||||
|
|
||||||
|
import type { Rgb, TailwindColorObject } from 'soapbox/types/colors';
|
||||||
|
|
||||||
|
export function hexToRgb(hex: string): Rgb | null {
|
||||||
|
const sanitizedHex = hex.replace(/##/g, '#');
|
||||||
|
const colorParts = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(
|
||||||
|
sanitizedHex,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!colorParts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, r, g, b] = colorParts;
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: parseInt(r, 16),
|
||||||
|
g: parseInt(g, 16),
|
||||||
|
b: parseInt(b, 16),
|
||||||
|
} as Rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex(r: number, g: number, b: number): string {
|
||||||
|
const toHex = (c: number) => `0${c.toString(16)}`.slice(-2);
|
||||||
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTextColor(color: string): '#FFF' | '#333' {
|
||||||
|
const rgbColor = hexToRgb(color);
|
||||||
|
|
||||||
|
if (!rgbColor) {
|
||||||
|
return '#333';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { r, g, b } = rgbColor;
|
||||||
|
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
|
||||||
|
return luma < 120 ? '#FFF' : '#333';
|
||||||
|
}
|
||||||
|
|
||||||
|
function lighten(hex: string, intensity: number): string {
|
||||||
|
const color = hexToRgb(`#${hex}`);
|
||||||
|
|
||||||
|
if (!color) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = Math.round(color.r + (255 - color.r) * intensity);
|
||||||
|
const g = Math.round(color.g + (255 - color.g) * intensity);
|
||||||
|
const b = Math.round(color.b + (255 - color.b) * intensity);
|
||||||
|
|
||||||
|
return rgbToHex(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function darken(hex: string, intensity: number): string {
|
||||||
|
const color = hexToRgb(hex);
|
||||||
|
|
||||||
|
if (!color) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = Math.round(color.r * intensity);
|
||||||
|
const g = Math.round(color.g * intensity);
|
||||||
|
const b = Math.round(color.b * intensity);
|
||||||
|
|
||||||
|
return rgbToHex(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function(baseColor: string): TailwindColorObject {
|
||||||
|
const response: TailwindColorObject = {
|
||||||
|
500: `#${baseColor}`.replace(/##/g, '#'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const intensityMap: {
|
||||||
|
[key: number]: number;
|
||||||
|
} = {
|
||||||
|
50: 0.95,
|
||||||
|
100: 0.9,
|
||||||
|
200: 0.75,
|
||||||
|
300: 0.6,
|
||||||
|
400: 0.3,
|
||||||
|
600: 0.9,
|
||||||
|
700: 0.75,
|
||||||
|
800: 0.6,
|
||||||
|
900: 0.49,
|
||||||
|
};
|
||||||
|
|
||||||
|
[50, 100, 200, 300, 400].forEach(level => {
|
||||||
|
response[level] = lighten(baseColor, intensityMap[level]);
|
||||||
|
});
|
||||||
|
|
||||||
|
[600, 700, 800, 900].forEach(level => {
|
||||||
|
response[level] = darken(baseColor, intensityMap[level]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
|
import tintify from 'soapbox/utils/colors';
|
||||||
|
import { generateAccent } from 'soapbox/utils/theme';
|
||||||
|
|
||||||
|
import type { TailwindColorPalette } from 'soapbox/types/colors';
|
||||||
|
|
||||||
|
type SoapboxConfig = ImmutableMap<string, any>;
|
||||||
|
type SoapboxColors = ImmutableMap<string, any>;
|
||||||
|
|
||||||
|
/** Check if the value is a valid hex color */
|
||||||
|
const isHex = (value: any): boolean => /^#([0-9A-F]{3}){1,2}$/i.test(value);
|
||||||
|
|
||||||
|
/** Expand hex colors into tints */
|
||||||
|
export const expandPalette = (palette: TailwindColorPalette): TailwindColorPalette => {
|
||||||
|
// Generate palette only for present colors
|
||||||
|
return Object.entries(palette).reduce((result: TailwindColorPalette, colorData) => {
|
||||||
|
const [colorName, color] = colorData;
|
||||||
|
|
||||||
|
// Conditionally handle hex color and Tailwind color object
|
||||||
|
if (typeof color === 'string' && isHex(color)) {
|
||||||
|
result[colorName] = tintify(color);
|
||||||
|
} else if (color && typeof color === 'object') {
|
||||||
|
result[colorName] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate accent color only if brandColor is present
|
||||||
|
const maybeGenerateAccentColor = (brandColor: any): string | null => {
|
||||||
|
return isHex(brandColor) ? generateAccent(brandColor) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Build a color object from legacy colors */
|
||||||
|
export const fromLegacyColors = (soapboxConfig: SoapboxConfig): TailwindColorPalette => {
|
||||||
|
const brandColor = soapboxConfig.get('brandColor');
|
||||||
|
const accentColor = soapboxConfig.get('accentColor');
|
||||||
|
|
||||||
|
return expandPalette({
|
||||||
|
primary: isHex(brandColor) ? brandColor : null,
|
||||||
|
accent: isHex(accentColor) ? accentColor : maybeGenerateAccentColor(brandColor),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Convert Soapbox Config into Tailwind colors */
|
||||||
|
export const toTailwind = (soapboxConfig: SoapboxConfig): SoapboxConfig => {
|
||||||
|
const colors: SoapboxColors = ImmutableMap(soapboxConfig.get('colors'));
|
||||||
|
const legacyColors: SoapboxColors = ImmutableMap(fromJS(fromLegacyColors(soapboxConfig)));
|
||||||
|
|
||||||
|
return soapboxConfig.set('colors', legacyColors.mergeDeep(colors));
|
||||||
|
};
|
|
@ -1,102 +0,0 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
export const generateThemeCss = (brandColor, accentColor) => {
|
|
||||||
if (!brandColor) return null;
|
|
||||||
return themeDataToCss(brandColorToThemeData(brandColor).merge(accentColorToThemeData(brandColor, accentColor)));
|
|
||||||
};
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/5624139
|
|
||||||
function hexToRgb(hex) {
|
|
||||||
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
|
|
||||||
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
|
||||||
hex = hex.replace(shorthandRegex, (m, r, g, b) => (
|
|
||||||
r + r + g + g + b + b
|
|
||||||
));
|
|
||||||
|
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
||||||
return result ? {
|
|
||||||
r: parseInt(result[1], 16),
|
|
||||||
g: parseInt(result[2], 16),
|
|
||||||
b: parseInt(result[3], 16),
|
|
||||||
} : {
|
|
||||||
// fall back to Azure
|
|
||||||
r: 4,
|
|
||||||
g: 130,
|
|
||||||
b: 216,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken from chromatism.js
|
|
||||||
// https://github.com/graypegg/chromatism/blob/master/src/conversions/rgb.js
|
|
||||||
const rgbToHsl = value => {
|
|
||||||
const r = value.r / 255;
|
|
||||||
const g = value.g / 255;
|
|
||||||
const b = value.b / 255;
|
|
||||||
const rgbOrdered = [ r, g, b ].sort();
|
|
||||||
const l = ((rgbOrdered[0] + rgbOrdered[2]) / 2) * 100;
|
|
||||||
let s, h;
|
|
||||||
if (rgbOrdered[0] === rgbOrdered[2]) {
|
|
||||||
s = 0;
|
|
||||||
h = 0;
|
|
||||||
} else {
|
|
||||||
if (l >= 50) {
|
|
||||||
s = ((rgbOrdered[2] - rgbOrdered[0]) / ((2.0 - rgbOrdered[2]) - rgbOrdered[0])) * 100;
|
|
||||||
} else {
|
|
||||||
s = ((rgbOrdered[2] - rgbOrdered[0]) / (rgbOrdered[2] + rgbOrdered[0])) * 100;
|
|
||||||
}
|
|
||||||
if (rgbOrdered[2] === r) {
|
|
||||||
h = ((g - b) / (rgbOrdered[2] - rgbOrdered[0])) * 60;
|
|
||||||
} else if (rgbOrdered[2] === g) {
|
|
||||||
h = (2 + ((b - r) / (rgbOrdered[2] - rgbOrdered[0]))) * 60;
|
|
||||||
} else {
|
|
||||||
h = (4 + ((r - g) / (rgbOrdered[2] - rgbOrdered[0]))) * 60;
|
|
||||||
}
|
|
||||||
if (h < 0) {
|
|
||||||
h += 360;
|
|
||||||
} else if (h > 360) {
|
|
||||||
h = h % 360;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
h: h,
|
|
||||||
s: s,
|
|
||||||
l: l,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const brandColorToThemeData = brandColor => {
|
|
||||||
const { h, s, l } = rgbToHsl(hexToRgb(brandColor));
|
|
||||||
return ImmutableMap({
|
|
||||||
'brand-color_h': h,
|
|
||||||
'brand-color_s': `${s}%`,
|
|
||||||
'brand-color_l': `${l}%`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const accentColorToThemeData = (brandColor, accentColor) => {
|
|
||||||
if (accentColor) {
|
|
||||||
const { h, s, l } = rgbToHsl(hexToRgb(accentColor));
|
|
||||||
|
|
||||||
return ImmutableMap({
|
|
||||||
'accent-color_h': h,
|
|
||||||
'accent-color_s': `${s}%`,
|
|
||||||
'accent-color_l': `${l}%`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { h } = rgbToHsl(hexToRgb(brandColor));
|
|
||||||
return ImmutableMap({
|
|
||||||
'accent-color_h': h - 15,
|
|
||||||
'accent-color_s': '86%',
|
|
||||||
'accent-color_l': '44%',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const themeDataToCss = themeData => (
|
|
||||||
themeData
|
|
||||||
.entrySeq()
|
|
||||||
.reduce((acc, cur) => acc + `--${cur[0]}:${cur[1]};`, '')
|
|
||||||
);
|
|
||||||
|
|
||||||
export const themeColorsToCSS = (brandColor, accentColor) => themeDataToCss(brandColorToThemeData(brandColor).merge(accentColorToThemeData(brandColor, accentColor)));
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { hexToRgb } from './colors';
|
||||||
|
import { toTailwind } from './tailwind';
|
||||||
|
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
import type { Rgb, Hsl, TailwindColorPalette, TailwindColorObject } from 'soapbox/types/colors';
|
||||||
|
|
||||||
|
// Taken from chromatism.js
|
||||||
|
// https://github.com/graypegg/chromatism/blob/master/src/conversions/rgb.js
|
||||||
|
const rgbToHsl = (value: Rgb): Hsl => {
|
||||||
|
const r = value.r / 255;
|
||||||
|
const g = value.g / 255;
|
||||||
|
const b = value.b / 255;
|
||||||
|
const rgbOrdered = [ r, g, b ].sort();
|
||||||
|
const l = ((rgbOrdered[0] + rgbOrdered[2]) / 2) * 100;
|
||||||
|
let s, h;
|
||||||
|
if (rgbOrdered[0] === rgbOrdered[2]) {
|
||||||
|
s = 0;
|
||||||
|
h = 0;
|
||||||
|
} else {
|
||||||
|
if (l >= 50) {
|
||||||
|
s = ((rgbOrdered[2] - rgbOrdered[0]) / ((2.0 - rgbOrdered[2]) - rgbOrdered[0])) * 100;
|
||||||
|
} else {
|
||||||
|
s = ((rgbOrdered[2] - rgbOrdered[0]) / (rgbOrdered[2] + rgbOrdered[0])) * 100;
|
||||||
|
}
|
||||||
|
if (rgbOrdered[2] === r) {
|
||||||
|
h = ((g - b) / (rgbOrdered[2] - rgbOrdered[0])) * 60;
|
||||||
|
} else if (rgbOrdered[2] === g) {
|
||||||
|
h = (2 + ((b - r) / (rgbOrdered[2] - rgbOrdered[0]))) * 60;
|
||||||
|
} else {
|
||||||
|
h = (4 + ((r - g) / (rgbOrdered[2] - rgbOrdered[0]))) * 60;
|
||||||
|
}
|
||||||
|
if (h < 0) {
|
||||||
|
h += 360;
|
||||||
|
} else if (h > 360) {
|
||||||
|
h = h % 360;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
h: h,
|
||||||
|
s: s,
|
||||||
|
l: l,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/44134328
|
||||||
|
function hslToHex(color: Hsl): string {
|
||||||
|
const { h, s } = color;
|
||||||
|
let { l } = color;
|
||||||
|
|
||||||
|
l /= 100;
|
||||||
|
const a = s * Math.min(l, 1 - l) / 100;
|
||||||
|
|
||||||
|
const f = (n: number) => {
|
||||||
|
const k = (n + h / 30) % 12;
|
||||||
|
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||||
|
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
|
||||||
|
};
|
||||||
|
|
||||||
|
return `#${f(0)}${f(8)}${f(4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate accent color from brand color
|
||||||
|
export const generateAccent = (brandColor: string): string => {
|
||||||
|
const { h } = rgbToHsl(hexToRgb(brandColor));
|
||||||
|
return hslToHex({ h: h - 15, s: 86, l: 44 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseShades = (obj: Record<string, any>, color: string, shades: Record<string, any>) => {
|
||||||
|
if (typeof shades === 'string') {
|
||||||
|
const { r, g, b } = hexToRgb(shades);
|
||||||
|
return obj[`--color-${color}`] = `${r} ${g} ${b}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(shades).forEach(shade => {
|
||||||
|
const { r, g, b } = hexToRgb(shades[shade]);
|
||||||
|
obj[`--color-${color}-${shade}`] = `${r} ${g} ${b}`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert colors as CSS variables
|
||||||
|
const parseColors = (colors: TailwindColorPalette): TailwindColorPalette => {
|
||||||
|
return Object.keys(colors).reduce((obj, color) => {
|
||||||
|
parseShades(obj, color, colors[color] as TailwindColorObject);
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const colorsToCss = (colors: TailwindColorPalette): string => {
|
||||||
|
const parsed = parseColors(colors);
|
||||||
|
return Object.keys(parsed).reduce((css, variable) => {
|
||||||
|
return css + `${variable}:${parsed[variable]};`;
|
||||||
|
}, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateThemeCss = (soapboxConfig: ImmutableMap<string, any>): string => {
|
||||||
|
return colorsToCss(toTailwind(soapboxConfig).get('colors').toJS() as TailwindColorPalette);
|
||||||
|
};
|
|
@ -82,7 +82,7 @@ body,
|
||||||
--input-border-color: #d1d5db;
|
--input-border-color: #d1d5db;
|
||||||
|
|
||||||
// Typography
|
// Typography
|
||||||
--font-sans: 'Inter', Arial, sans-serif;
|
--font-sans: 'Inter', arial, sans-serif;
|
||||||
--font-weight-heading: 700;
|
--font-weight-heading: 700;
|
||||||
--font-weight-body: 400;
|
--font-weight-body: 400;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
'projects': [
|
|
||||||
'<rootDir>/app/soapbox',
|
|
||||||
],
|
|
||||||
'testPathIgnorePatterns': [
|
'testPathIgnorePatterns': [
|
||||||
'<rootDir>/node_modules/',
|
'<rootDir>/node_modules/',
|
||||||
'<rootDir>/vendor/',
|
'<rootDir>/vendor/',
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
const { parseColorMatrix } = require('./tailwind/colors');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./app/**/*.{html,js,ts,tsx}'],
|
content: ['./app/**/*.{html,js,ts,tsx}'],
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
|
@ -31,63 +33,20 @@ module.exports = {
|
||||||
'Noto Color Emoji',
|
'Noto Color Emoji',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: parseColorMatrix({
|
||||||
gray: {
|
// Define color matrix (of available colors)
|
||||||
50: '#f9fafb',
|
// Colors are configured at runtime with CSS variables in soapbox.json
|
||||||
100: '#f3f4f6',
|
gray: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
200: '#e5e7eb',
|
primary: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
300: '#d1d5db',
|
success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
400: '#9ca3af',
|
danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
500: '#6b7280',
|
accent: [300, 500],
|
||||||
600: '#4b5563',
|
'gradient-purple': true,
|
||||||
700: '#374151',
|
'gradient-blue': true,
|
||||||
800: '#1f2937',
|
'sea-blue': true,
|
||||||
900: '#111827',
|
'bg-shape-1': true,
|
||||||
},
|
'bg-shape-2': true,
|
||||||
primary: {
|
}),
|
||||||
50: '#eef2ff',
|
|
||||||
100: '#e0e7ff',
|
|
||||||
200: '#c7d2fe',
|
|
||||||
300: '#a5b4fc',
|
|
||||||
400: '#818cf8',
|
|
||||||
500: '#6366f1',
|
|
||||||
600: '#5448ee',
|
|
||||||
700: '#4338ca',
|
|
||||||
800: '#3730a3',
|
|
||||||
900: '#312e81',
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
50: '#f0fdf4',
|
|
||||||
100: '#dcfce7',
|
|
||||||
200: '#bbf7d0',
|
|
||||||
300: '#86efac',
|
|
||||||
400: '#4ade80',
|
|
||||||
500: '#22c55e',
|
|
||||||
600: '#16a34a',
|
|
||||||
700: '#15803d',
|
|
||||||
800: '#166534',
|
|
||||||
900: '#14532d',
|
|
||||||
},
|
|
||||||
danger: {
|
|
||||||
50: '#fef2f2',
|
|
||||||
100: '#fee2e2',
|
|
||||||
200: '#fecaca',
|
|
||||||
300: '#fca5a5',
|
|
||||||
400: '#f87171',
|
|
||||||
500: '#ef4444',
|
|
||||||
600: '#dc2626',
|
|
||||||
700: '#b91c1c',
|
|
||||||
800: '#991b1b',
|
|
||||||
900: '#7f1d1d',
|
|
||||||
},
|
|
||||||
accent: {
|
|
||||||
300: '#ff5f87',
|
|
||||||
500: '#ff4775',
|
|
||||||
},
|
|
||||||
'gradient-purple': '#b8a3f9',
|
|
||||||
'gradient-blue': '#9bd5ff',
|
|
||||||
'sea-blue': '#2feecc',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import {
|
||||||
|
withOpacityValue,
|
||||||
|
parseColorMatrix,
|
||||||
|
} from '../colors';
|
||||||
|
|
||||||
|
describe('withOpacityValue()', () => {
|
||||||
|
it('returns a Tailwind color function with alpha support', () => {
|
||||||
|
const result = withOpacityValue('--color-primary-500');
|
||||||
|
|
||||||
|
// It returns a function
|
||||||
|
expect(typeof result).toBe('function');
|
||||||
|
|
||||||
|
// Test calling the function
|
||||||
|
expect(result({})).toBe('rgb(var(--color-primary-500))');
|
||||||
|
expect(result({ opacityValue: .5 })).toBe('rgb(var(--color-primary-500) / 0.5)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseColorMatrix()', () => {
|
||||||
|
it('returns a Tailwind color object', () => {
|
||||||
|
const colorMatrix = {
|
||||||
|
gray: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
|
primary: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
|
success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
|
danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
|
accent: [300, 500],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = parseColorMatrix(colorMatrix);
|
||||||
|
|
||||||
|
// Colors are mapped to functions which return CSS values
|
||||||
|
expect(result.primary[500]({})).toEqual('rgb(var(--color-primary-500))');
|
||||||
|
expect(result.accent[300]({ opacityValue: .3 })).toEqual('rgb(var(--color-accent-300) / 0.3)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses single-tint values', () => {
|
||||||
|
const colorMatrix = {
|
||||||
|
gray: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
|
primary: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
|
success: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
|
danger: [50, 100, 200, 300, 400, 500, 600, 700, 800, 900],
|
||||||
|
accent: [300, 500],
|
||||||
|
'gradient-purple': true,
|
||||||
|
'gradient-blue': true,
|
||||||
|
'sea-blue': true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = parseColorMatrix(colorMatrix);
|
||||||
|
|
||||||
|
expect(result['sea-blue']({})).toEqual('rgb(var(--color-sea-blue))');
|
||||||
|
expect(result['gradient-purple']({ opacityValue: .7 })).toEqual('rgb(var(--color-gradient-purple) / 0.7)');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
// https://tailwindcss.com/docs/customizing-colors#using-css-variables
|
||||||
|
function withOpacityValue(variable) {
|
||||||
|
return ({ opacityValue }) => {
|
||||||
|
if (opacityValue === undefined) {
|
||||||
|
return `rgb(var(${variable}))`;
|
||||||
|
}
|
||||||
|
return `rgb(var(${variable}) / ${opacityValue})`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a single color as a CSS variable
|
||||||
|
const toColorVariable = (colorName, tint = null) => {
|
||||||
|
const suffix = tint ? `-${tint}` : '';
|
||||||
|
const variable = `--color-${colorName}${suffix}`;
|
||||||
|
|
||||||
|
return withOpacityValue(variable);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse list of tints into Tailwind function with CSS variables
|
||||||
|
const parseTints = (colorName, tints) => {
|
||||||
|
return tints.reduce((colorObj, tint) => {
|
||||||
|
colorObj[tint] = toColorVariable(colorName, tint);
|
||||||
|
return colorObj;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse color matrix into Tailwind color palette
|
||||||
|
const parseColorMatrix = colorMatrix => {
|
||||||
|
return Object.entries(colorMatrix).reduce((palette, colorData) => {
|
||||||
|
const [colorName, tints] = colorData;
|
||||||
|
|
||||||
|
// Conditionally parse array or single-tint colors
|
||||||
|
if (Array.isArray(tints)) {
|
||||||
|
palette[colorName] = parseTints(colorName, tints);
|
||||||
|
} else if (tints === true) {
|
||||||
|
palette[colorName] = toColorVariable(colorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return palette;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
withOpacityValue,
|
||||||
|
parseColorMatrix,
|
||||||
|
};
|
Loading…
Reference in New Issue