Generate Tailwind colors from brandColor/accentColor
This commit is contained in:
parent
18bad4a5ab
commit
a42ea0961a
6 changed files with 179 additions and 70 deletions
Binary file not shown.
|
@ -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({
|
||||||
|
|
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,
|
||||||
|
}
|
BIN
app/soapbox/utils/__tests__/colors-test.js
Normal file
BIN
app/soapbox/utils/__tests__/colors-test.js
Normal file
Binary file not shown.
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 };
|
import type { Rgb, Hsl, TailwindColorPalette } from 'soapbox/types/colors';
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken from chromatism.js
|
// Taken from chromatism.js
|
||||||
// https://github.com/graypegg/chromatism/blob/master/src/conversions/rgb.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 r = value.r / 255;
|
||||||
const g = value.g / 255;
|
const g = value.g / 255;
|
||||||
const b = value.b / 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>) => {
|
const parseShades = (obj: Record<string, any>, color: string, shades: Record<string, any>) => {
|
||||||
if (typeof shades === 'string') {
|
if (typeof shades === 'string') {
|
||||||
const { r, g, b } = hexToRgb(shades);
|
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) => {
|
return Object.keys(colors).reduce((obj, color) => {
|
||||||
parseShades(obj, color, colors[color]);
|
parseShades(obj, color, colors[color]);
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const colorsToCss = (colors: Record<string, any>): string => {
|
export const colorsToCss = (colors: TailwindColorPalette): string => {
|
||||||
const parsed = parseColors(colors);
|
const parsed = parseColors(colors);
|
||||||
return Object.keys(parsed).reduce((css, variable) => {
|
return Object.keys(parsed).reduce((css, variable) => {
|
||||||
return css + `${variable}:${parsed[variable]};`;
|
return css + `${variable}:${parsed[variable]};`;
|
||||||
}, '');
|
}, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const brandColorToThemeData = (brandColor: string): ImmutableMap<string, any> => {
|
const legacyColorsToTailwind = (brandColor: string, accentColor: string): TailwindColorPalette => {
|
||||||
const { h, s, l } = rgbToHsl(hexToRgb(brandColor));
|
return {
|
||||||
return ImmutableMap({
|
primary: tintify(brandColor),
|
||||||
'brand-color_h': h,
|
accent: tintify(accentColor ? accentColor : generateAccent(brandColor)),
|
||||||
'brand-color_s': `${s}%`,
|
};
|
||||||
'brand-color_l': `${l}%`,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accentColorToThemeData = (brandColor: string, accentColor: string): ImmutableMap<string, any> => {
|
export const themeColorsToCSS = (brandColor: string, accentColor: string): string => {
|
||||||
if (accentColor) {
|
const colors = legacyColorsToTailwind(brandColor, accentColor);
|
||||||
const { h, s, l } = rgbToHsl(hexToRgb(accentColor));
|
return colorsToCss(colors);
|
||||||
|
|
||||||
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: 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