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}>
|
||||
<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 | 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;
|
||||
|
||||
// Typography
|
||||
--font-sans: 'Inter', Arial, sans-serif;
|
||||
--font-sans: 'Inter', arial, sans-serif;
|
||||
--font-weight-heading: 700;
|
||||
--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