Generate Tailwind colors from brandColor/accentColor
This commit is contained in:
parent
18bad4a5ab
commit
a42ea0961a
6 changed files with 203 additions and 72 deletions
|
@ -25,7 +25,7 @@ import { createGlobals } from 'soapbox/globals';
|
|||
import messages from 'soapbox/locales/messages';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||
import { colorsToCss } from 'soapbox/utils/theme';
|
||||
import { themeColorsToCSS } from 'soapbox/utils/theme';
|
||||
|
||||
import { INTRODUCTION_VERSION } from '../actions/onboarding';
|
||||
import { preload } from '../actions/preload';
|
||||
|
@ -84,7 +84,7 @@ const mapStateToProps = (state) => {
|
|||
dyslexicFont: settings.get('dyslexicFont'),
|
||||
demetricator: settings.get('demetricator'),
|
||||
locale: validLocale(locale) ? locale : 'en',
|
||||
themeCss: colorsToCss(soapboxConfig.get('colors').toJS()),
|
||||
themeCss: themeColorsToCSS(soapboxConfig.get('brandColor') || '#0482d8', soapboxConfig.get('accentColor', '')),
|
||||
brandColor: soapboxConfig.get('brandColor'),
|
||||
themeMode: settings.get('themeMode'),
|
||||
singleUserMode,
|
||||
|
|
|
@ -33,11 +33,13 @@ const Navbar = () => {
|
|||
<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='relative flex justify-between h-12 lg:h-16'>
|
||||
<div className='absolute inset-y-0 left-0 flex items-center lg:hidden'>
|
||||
<button onClick={onOpenSidebar}>
|
||||
<Avatar src={account.avatar} size={34} />
|
||||
</button>
|
||||
</div>
|
||||
{account && (
|
||||
<div className='absolute inset-y-0 left-0 flex items-center lg:hidden'>
|
||||
<button onClick={onOpenSidebar}>
|
||||
<Avatar src={account.avatar} size={34} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={classNames({
|
||||
|
|
10
app/soapbox/types/colors.ts
Normal file
10
app/soapbox/types/colors.ts
Normal file
|
@ -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,
|
||||
}
|
22
app/soapbox/utils/__tests__/colors-test.js
Normal file
22
app/soapbox/utils/__tests__/colors-test.js
Normal file
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
123
app/soapbox/utils/colors.ts
Normal file
123
app/soapbox/utils/colors.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -1,37 +1,10 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import tintify, { hexToRgb } from './colors';
|
||||
|
||||
type RGB = { r: number, g: number, b: number };
|
||||
type HSL = { h: number, s: number, l: number };
|
||||
|
||||
export const generateThemeCss = (brandColor: string, accentColor: string): string => {
|
||||
if (!brandColor) return null;
|
||||
return themeDataToCss(brandColorToThemeData(brandColor).merge(accentColorToThemeData(brandColor, accentColor)));
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/a/5624139
|
||||
function hexToRgb(hex: string): RGB {
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
import type { Rgb, Hsl, TailwindColorPalette } 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 rgbToHsl = (value: Rgb): Hsl => {
|
||||
const r = value.r / 255;
|
||||
const g = value.g / 255;
|
||||
const b = value.b / 255;
|
||||
|
@ -68,6 +41,31 @@ const rgbToHsl = (value: RGB): HSL => {
|
|||
};
|
||||
};
|
||||
|
||||
// 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
|
||||
const generateAccent = (brandColor: string): string => {
|
||||
console.log(brandColor);
|
||||
console.log(hexToRgb(brandColor));
|
||||
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);
|
||||
|
@ -80,53 +78,29 @@ const parseShades = (obj: Record<string, any>, color: string, shades: Record<str
|
|||
});
|
||||
};
|
||||
|
||||
const parseColors = (colors: Record<string, any>): Record<string, any> => {
|
||||
// Convert colors as CSS variables
|
||||
const parseColors = (colors: TailwindColorPalette): TailwindColorPalette => {
|
||||
return Object.keys(colors).reduce((obj, color) => {
|
||||
parseShades(obj, color, colors[color]);
|
||||
return obj;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const colorsToCss = (colors: Record<string, any>): string => {
|
||||
export const colorsToCss = (colors: TailwindColorPalette): string => {
|
||||
const parsed = parseColors(colors);
|
||||
return Object.keys(parsed).reduce((css, variable) => {
|
||||
return css + `${variable}:${parsed[variable]};`;
|
||||
}, '');
|
||||
};
|
||||
|
||||
export const brandColorToThemeData = (brandColor: string): ImmutableMap<string, any> => {
|
||||
const { h, s, l } = rgbToHsl(hexToRgb(brandColor));
|
||||
return ImmutableMap({
|
||||
'brand-color_h': h,
|
||||
'brand-color_s': `${s}%`,
|
||||
'brand-color_l': `${l}%`,
|
||||
});
|
||||
const legacyColorsToTailwind = (brandColor: string, accentColor: string): TailwindColorPalette => {
|
||||
return {
|
||||
primary: tintify(brandColor),
|
||||
accent: tintify(accentColor ? accentColor : generateAccent(brandColor)),
|
||||
};
|
||||
};
|
||||
|
||||
export const accentColorToThemeData = (brandColor: string, accentColor: string): ImmutableMap<string, any> => {
|
||||
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 themeColorsToCSS = (brandColor: string, accentColor: string): string => {
|
||||
const colors = legacyColorsToTailwind(brandColor, accentColor);
|
||||
return colorsToCss(colors);
|
||||
};
|
||||
|
||||
export const themeDataToCss = (themeData: ImmutableMap<string, any>): string => (
|
||||
themeData
|
||||
.entrySeq()
|
||||
.reduce((acc, cur) => acc + `--${cur[0]}:${cur[1]};`, '')
|
||||
);
|
||||
|
||||
export const themeColorsToCSS = (brandColor: string, accentColor: string): string =>
|
||||
themeDataToCss(brandColorToThemeData(brandColor).merge(accentColorToThemeData(brandColor, accentColor)));
|
||||
|
|
Loading…
Reference in a new issue