Merge branch 'next-colors' into 'next'
Next: colors See merge request soapbox-pub/soapbox-fe!1126
This commit is contained in:
commit
8970e4e3db
18 changed files with 292 additions and 7 deletions
|
@ -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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
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'>
|
||||||
<div className='absolute inset-y-0 left-0 flex items-center lg:hidden'>
|
{account && (
|
||||||
<button onClick={onOpenSidebar}>
|
<div className='absolute inset-y-0 left-0 flex items-center lg:hidden'>
|
||||||
<Avatar src={account.avatar} size={34} />
|
<button onClick={onOpenSidebar}>
|
||||||
</button>
|
<Avatar src={account.avatar} size={34} />
|
||||||
</div>
|
</button>
|
||||||
|
</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 | string,
|
||||||
|
}
|
BIN
app/soapbox/utils/__tests__/colors-test.js
Normal file
BIN
app/soapbox/utils/__tests__/colors-test.js
Normal file
Binary file not shown.
BIN
app/soapbox/utils/__tests__/tailwind-test.js
Normal file
BIN
app/soapbox/utils/__tests__/tailwind-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;
|
||||||
|
}
|
53
app/soapbox/utils/tailwind.ts
Normal file
53
app/soapbox/utils/tailwind.ts
Normal file
|
@ -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));
|
||||||
|
};
|
Binary file not shown.
98
app/soapbox/utils/theme.ts
Normal file
98
app/soapbox/utils/theme.ts
Normal file
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
BIN
jest.config.js
BIN
jest.config.js
Binary file not shown.
Binary file not shown.
BIN
tailwind/__tests__/colors-test.js
Normal file
BIN
tailwind/__tests__/colors-test.js
Normal file
Binary file not shown.
BIN
tailwind/colors.js
Normal file
BIN
tailwind/colors.js
Normal file
Binary file not shown.
Loading…
Reference in a new issue