Switch Tailwind color theme to use customisable CSS variables

Co-authored-by: Scott Cranfill <scott@scottcranfill.com>
pull/8882/head
Thibaud Colas 2022-07-09 06:58:26 +01:00
rodzic 9b0c9010bd
commit 8f96a5669d
4 zmienionych plików z 291 dodań i 19 usunięć

Wyświetl plik

@ -0,0 +1,90 @@
/**
* Generate a CSS calc() expression so one HSL component is derived from another.
* For example, with a reference color of `hsl(66 50% 25%)`, and a derived color of hsl(66 65% 20%),
* - The hue components are identical, so the derived hue should just refer to the reference hue.
* - Saturation for the derived color is higher, so should be `calc(var(--ref-saturation) + 15%)`
* - Lightness for the derived color is lower, so should be `calc(var(--ref-lightness) - 5%)`.
*/
const calcHSLDifference = (refVariable, refValue, value, unit = '') => {
const ref = Number(refValue);
const val = Number(value);
// If the value is equal to the reference, there is nothing to calc.
if (ref === val) {
return `var(${refVariable})`;
}
// Either add or remove the difference based on whether its positive or negative.
const diff = ref - val;
const operation = `${diff > 0 ? '-' : '+'} ${Math.abs(diff)}${unit}`;
return `calc(var(${refVariable}) ${operation})`;
};
/**
* Generate customisable CSS variables for a color palette, with override-able HSL components.
*
* For each shade of a color, we want to generate four variables:
* - One for each HSL component of the color (Hue, Saturation, Lightness).
* - A valid HSL color value combining the three components.
*
* A shades HSL components need to be derived from the reference colors HSL components,
* so site implementers can change all shades of a color at once by setting the HSL components of the "reference" color.
*
* For example, for a "light red" color derived from "red", this will create:
* --red-light-hue: var(--red-hue);
* --red-light-saturation: var(--red-saturation + 15%);
* --red-light-lightness: calc(var(--red-lightness) - 5%);
* --red-light: hsl(var(--red-light-hue) var(--red-light-saturation) var(--red-light-lightness));
*
* For the red reference color defined as `hsl(66 50% 25%)`, this will create:
* --red-hue: 66;
* --red-saturation: 50%;
* --red-lightness: 25%;
* --red: hsl(var(--red-hue) var(--red-saturation) var(--red-lightness));
*
*/
const generateColorVariables = (colors) => {
/* eslint-disable no-param-reassign, id-length */
const colorVariables = Object.values(colors).reduce((root, hues) => {
// Use the DEFAULT hue as a reference to derive others from, or the darkest if there is no defined default.
const darkestHue = Object.keys(hues).sort((a, b) => b - a)[0];
const reference = hues.DEFAULT || hues[darkestHue];
const [refH, refS, refL] = reference.hsl.match(/\d+/g);
const refVar = reference.cssVariable;
// Generate color variables for all individual color shades, based on the reference.
Object.values(hues).forEach((shade) => {
// CSS variables will we generate.
const vars = {
hsl: shade.cssVariable,
h: `${shade.cssVariable}-hue`,
s: `${shade.cssVariable}-saturation`,
l: `${shade.cssVariable}-lightness`,
};
const [h, s, l] = shade.hsl.match(/\d+/g);
const isReferenceShade = reference.hex === shade.hex;
if (isReferenceShade) {
// If this is the reference shade, we use its HSL values as-is.
root[vars.h] = h;
root[vars.s] = `${s}%`;
root[vars.l] = `${l}%`;
} else {
// If this is a derived shade, we will derive its HSL values from the reference.
root[vars.h] = calcHSLDifference(`${refVar}-hue`, refH, h);
root[vars.s] = calcHSLDifference(`${refVar}-saturation`, refS, s, '%');
root[vars.l] = calcHSLDifference(`${refVar}-lightness`, refL, l, '%');
}
root[vars.hsl] = `hsl(var(${vars.h}) var(${vars.s}) var(${vars.l}))`;
});
return root;
}, {});
return colorVariables;
};
module.exports = {
generateColorVariables,
};

Wyświetl plik

@ -0,0 +1,121 @@
const colors = require('./colors');
const { generateColorVariables } = require('./colorVariables');
describe('generateColorVariables', () => {
it('generates all variables', () => {
const colorVariables = generateColorVariables(colors);
const generatedVariables = Object.keys(colorVariables);
Object.values(colors).forEach((hues) => {
Object.values(hues).forEach((shade) => {
expect(generatedVariables).toContain(shade.cssVariable);
});
});
});
/**
* If this test breaks, it means weve either changed our color palette, or changed how we make each of the colors customisable.
* If the change is intentional, we will then need to update our `custom_user_interface_colours` documentation.
* - Open Storybooks color customisation story in a browser
* - Use your browsers DevTools to copy the relevant story markup to our Markdown documentation.
* - Leave the copied content exactly as-is when pasting, to avoid any Markdown formatting issues.
*/
it('is stable (update custom_user_interface_colours documentation when this changes)', () => {
const colorVariables = generateColorVariables(colors);
expect(colorVariables).toMatchInlineSnapshot(`
Object {
"--w-color-black": "hsl(var(--w-color-black-hue) var(--w-color-black-saturation) var(--w-color-black-lightness))",
"--w-color-black-hue": "0",
"--w-color-black-lightness": "0%",
"--w-color-black-saturation": "0%",
"--w-color-critical-100": "hsl(var(--w-color-critical-100-hue) var(--w-color-critical-100-saturation) var(--w-color-critical-100-lightness))",
"--w-color-critical-100-hue": "calc(var(--w-color-critical-200-hue) + 355)",
"--w-color-critical-100-lightness": "calc(var(--w-color-critical-200-lightness) + 13%)",
"--w-color-critical-100-saturation": "calc(var(--w-color-critical-200-saturation) + 40%)",
"--w-color-critical-200": "hsl(var(--w-color-critical-200-hue) var(--w-color-critical-200-saturation) var(--w-color-critical-200-lightness))",
"--w-color-critical-200-hue": "0",
"--w-color-critical-200-lightness": "54%",
"--w-color-critical-200-saturation": "58%",
"--w-color-critical-50": "hsl(var(--w-color-critical-50-hue) var(--w-color-critical-50-saturation) var(--w-color-critical-50-lightness))",
"--w-color-critical-50-hue": "var(--w-color-critical-200-hue)",
"--w-color-critical-50-lightness": "calc(var(--w-color-critical-200-lightness) + 41%)",
"--w-color-critical-50-saturation": "calc(var(--w-color-critical-200-saturation) + 25%)",
"--w-color-grey-100": "hsl(var(--w-color-grey-100-hue) var(--w-color-grey-100-saturation) var(--w-color-grey-100-lightness))",
"--w-color-grey-100-hue": "var(--w-color-grey-600-hue)",
"--w-color-grey-100-lightness": "calc(var(--w-color-grey-600-lightness) + 73%)",
"--w-color-grey-100-saturation": "var(--w-color-grey-600-saturation)",
"--w-color-grey-200": "hsl(var(--w-color-grey-200-hue) var(--w-color-grey-200-saturation) var(--w-color-grey-200-lightness))",
"--w-color-grey-200-hue": "var(--w-color-grey-600-hue)",
"--w-color-grey-200-lightness": "calc(var(--w-color-grey-600-lightness) + 42%)",
"--w-color-grey-200-saturation": "var(--w-color-grey-600-saturation)",
"--w-color-grey-400": "hsl(var(--w-color-grey-400-hue) var(--w-color-grey-400-saturation) var(--w-color-grey-400-lightness))",
"--w-color-grey-400-hue": "var(--w-color-grey-600-hue)",
"--w-color-grey-400-lightness": "calc(var(--w-color-grey-600-lightness) + 21%)",
"--w-color-grey-400-saturation": "var(--w-color-grey-600-saturation)",
"--w-color-grey-50": "hsl(var(--w-color-grey-50-hue) var(--w-color-grey-50-saturation) var(--w-color-grey-50-lightness))",
"--w-color-grey-50-hue": "calc(var(--w-color-grey-600-hue) + 240)",
"--w-color-grey-50-lightness": "calc(var(--w-color-grey-600-lightness) + 82%)",
"--w-color-grey-50-saturation": "calc(var(--w-color-grey-600-saturation) + 12%)",
"--w-color-grey-600": "hsl(var(--w-color-grey-600-hue) var(--w-color-grey-600-saturation) var(--w-color-grey-600-lightness))",
"--w-color-grey-600-hue": "0",
"--w-color-grey-600-lightness": "15%",
"--w-color-grey-600-saturation": "0%",
"--w-color-info-100": "hsl(var(--w-color-info-100-hue) var(--w-color-info-100-saturation) var(--w-color-info-100-lightness))",
"--w-color-info-100-hue": "194",
"--w-color-info-100-lightness": "36%",
"--w-color-info-100-saturation": "66%",
"--w-color-info-50": "hsl(var(--w-color-info-50-hue) var(--w-color-info-50-saturation) var(--w-color-info-50-lightness))",
"--w-color-info-50-hue": "calc(var(--w-color-info-100-hue) + 2)",
"--w-color-info-50-lightness": "calc(var(--w-color-info-100-lightness) + 58%)",
"--w-color-info-50-saturation": "calc(var(--w-color-info-100-saturation) + 15%)",
"--w-color-positive-100": "hsl(var(--w-color-positive-100-hue) var(--w-color-positive-100-saturation) var(--w-color-positive-100-lightness))",
"--w-color-positive-100-hue": "162",
"--w-color-positive-100-lightness": "32%",
"--w-color-positive-100-saturation": "66%",
"--w-color-positive-50": "hsl(var(--w-color-positive-50-hue) var(--w-color-positive-50-saturation) var(--w-color-positive-50-lightness))",
"--w-color-positive-50-hue": "calc(var(--w-color-positive-100-hue) + 2)",
"--w-color-positive-50-lightness": "calc(var(--w-color-positive-100-lightness) + 61%)",
"--w-color-positive-50-saturation": "calc(var(--w-color-positive-100-saturation) + 11%)",
"--w-color-primary": "hsl(var(--w-color-primary-hue) var(--w-color-primary-saturation) var(--w-color-primary-lightness))",
"--w-color-primary-200": "hsl(var(--w-color-primary-200-hue) var(--w-color-primary-200-saturation) var(--w-color-primary-200-lightness))",
"--w-color-primary-200-hue": "var(--w-color-primary-hue)",
"--w-color-primary-200-lightness": "calc(var(--w-color-primary-lightness) - 5%)",
"--w-color-primary-200-saturation": "var(--w-color-primary-saturation)",
"--w-color-primary-hue": "254",
"--w-color-primary-lightness": "25%",
"--w-color-primary-saturation": "50%",
"--w-color-secondary": "hsl(var(--w-color-secondary-hue) var(--w-color-secondary-saturation) var(--w-color-secondary-lightness))",
"--w-color-secondary-100": "hsl(var(--w-color-secondary-100-hue) var(--w-color-secondary-100-saturation) var(--w-color-secondary-100-lightness))",
"--w-color-secondary-100-hue": "var(--w-color-secondary-hue)",
"--w-color-secondary-100-lightness": "calc(var(--w-color-secondary-lightness) + 10%)",
"--w-color-secondary-100-saturation": "var(--w-color-secondary-saturation)",
"--w-color-secondary-400": "hsl(var(--w-color-secondary-400-hue) var(--w-color-secondary-400-saturation) var(--w-color-secondary-400-lightness))",
"--w-color-secondary-400-hue": "calc(var(--w-color-secondary-hue) + 2)",
"--w-color-secondary-400-lightness": "calc(var(--w-color-secondary-lightness) - 7%)",
"--w-color-secondary-400-saturation": "var(--w-color-secondary-saturation)",
"--w-color-secondary-50": "hsl(var(--w-color-secondary-50-hue) var(--w-color-secondary-50-saturation) var(--w-color-secondary-50-lightness))",
"--w-color-secondary-50-hue": "var(--w-color-secondary-hue)",
"--w-color-secondary-50-lightness": "calc(var(--w-color-secondary-lightness) + 72%)",
"--w-color-secondary-50-saturation": "calc(var(--w-color-secondary-saturation) - 37%)",
"--w-color-secondary-600": "hsl(var(--w-color-secondary-600-hue) var(--w-color-secondary-600-saturation) var(--w-color-secondary-600-lightness))",
"--w-color-secondary-600-hue": "calc(var(--w-color-secondary-hue) + 2)",
"--w-color-secondary-600-lightness": "calc(var(--w-color-secondary-lightness) - 11%)",
"--w-color-secondary-600-saturation": "var(--w-color-secondary-saturation)",
"--w-color-secondary-hue": "180",
"--w-color-secondary-lightness": "25%",
"--w-color-secondary-saturation": "100%",
"--w-color-warning-100": "hsl(var(--w-color-warning-100-hue) var(--w-color-warning-100-saturation) var(--w-color-warning-100-lightness))",
"--w-color-warning-100-hue": "40",
"--w-color-warning-100-lightness": "49%",
"--w-color-warning-100-saturation": "100%",
"--w-color-warning-50": "hsl(var(--w-color-warning-50-hue) var(--w-color-warning-50-saturation) var(--w-color-warning-50-lightness))",
"--w-color-warning-50-hue": "calc(var(--w-color-warning-100-hue) - 3)",
"--w-color-warning-50-lightness": "calc(var(--w-color-warning-100-lightness) + 42%)",
"--w-color-warning-50-saturation": "calc(var(--w-color-warning-100-saturation) - 21%)",
"--w-color-white": "hsl(var(--w-color-white-hue) var(--w-color-white-saturation) var(--w-color-white-lightness))",
"--w-color-white-hue": "0",
"--w-color-white-lightness": "100%",
"--w-color-white-saturation": "0%",
}
`);
});
});

Wyświetl plik

@ -19,8 +19,10 @@ const colors = {
black: {
DEFAULT: {
hex: '#000000',
hsl: 'hsl(0, 0%, 0%)',
bgUtility: 'w-bg-black',
textUtility: 'w-text-black',
cssVariable: '--w-color-black',
usage: 'Shadows only',
contrastText: 'white',
},
@ -28,36 +30,46 @@ const colors = {
grey: {
600: {
hex: '#262626',
hsl: 'hsl(0, 0%, 15%)',
bgUtility: 'w-bg-grey-600',
textUtility: 'w-text-grey-600',
cssVariable: '--w-color-grey-600',
usage: 'Body copy, user content',
contrastText: 'white',
},
400: {
hex: '#5C5C5C',
hsl: 'hsl(0, 0%, 36%)',
bgUtility: 'w-bg-grey-400',
textUtility: 'w-text-grey-400',
cssVariable: '--w-color-grey-400',
usage: 'Help text, placeholders, meta text, neutral state indicators',
contrastText: 'white',
},
200: {
hex: '#929292',
hsl: 'hsl(0, 0%, 57%)',
bgUtility: 'w-bg-grey-200',
textUtility: 'w-text-grey-200',
cssVariable: '--w-color-grey-200',
usage: 'Dividers, button borders',
contrastText: 'primary',
},
100: {
hex: '#E0E0E0',
hsl: 'hsl(0, 0%, 88%)',
bgUtility: 'w-bg-grey-100',
textUtility: 'w-text-grey-100',
cssVariable: '--w-color-grey-100',
usage: 'Dividers, field borders, panel borders',
contrastText: 'primary',
},
50: {
hex: '#F6F6F8',
hsl: 'hsl(240, 12%, 97%)',
bgUtility: 'w-bg-grey-50',
textUtility: 'w-text-grey-50',
cssVariable: '--w-color-grey-50',
usage: 'Background for panels, row highlights',
contrastText: 'primary',
},
@ -65,61 +77,77 @@ const colors = {
white: {
DEFAULT: {
hex: '#FFFFFF',
hsl: 'hsl(0, 0%, 100%)',
bgUtility: 'w-bg-white',
textUtility: 'w-text-white',
cssVariable: '--w-color-white',
usage: 'Page backgrounds, Panels, Button text',
contrastText: 'primary',
},
},
teal: {
secondary: {
600: {
hex: '#004345',
bgUtility: 'w-bg-teal-600',
textUtility: 'w-text-teal-600',
hsl: 'hsl(182, 100%, 14%)',
bgUtility: 'w-bg-secondary-600',
textUtility: 'w-text-secondary-600',
cssVariable: '--w-color-secondary-600',
usage: 'Hover states for two-tone buttons',
contrastText: 'white',
},
400: {
hex: '#005B5E',
bgUtility: 'w-bg-teal-400',
textUtility: 'w-text-teal-400',
hsl: 'hsl(182, 100%, 18%)',
bgUtility: 'w-bg-secondary-400',
textUtility: 'w-text-secondary-400',
cssVariable: '--w-color-secondary-400',
usage: 'Two-tone buttons, hover states',
contrastText: 'white',
},
200: {
DEFAULT: {
hex: '#007D7E',
bgUtility: 'w-bg-teal-200',
textUtility: 'w-text-teal-200',
hsl: 'hsl(180, 100%, 25%)',
bgUtility: 'w-bg-secondary',
textUtility: 'w-text-secondary',
cssVariable: '--w-color-secondary',
usage: 'Primary buttons, action links',
contrastText: 'white',
},
100: {
hex: '#00B0B1',
bgUtility: 'w-bg-teal-100',
textUtility: 'w-text-teal-100',
hsl: 'hsl(180, 100%, 35%)',
bgUtility: 'w-bg-secondary-100',
textUtility: 'w-text-secondary-100',
cssVariable: '--w-color-secondary-100',
usage: 'UI element highlights',
contrastText: 'white',
},
50: {
hex: '#F2FCFC',
bgUtility: 'w-bg-teal-50',
textUtility: 'w-text-teal-50',
hsl: 'hsl(180, 63%, 97%)',
bgUtility: 'w-bg-secondary-50',
textUtility: 'w-text-secondary-50',
cssVariable: '--w-color-secondary-50',
usage: 'Button backgrounds, highlighted fields background',
contrastText: 'teal-200',
contrastText: 'secondary',
},
},
primary: {
DEFAULT: {
hex: '#2E1F5E',
hsl: 'hsl(254, 50%, 25%)',
bgUtility: 'w-bg-primary',
textUtility: 'w-text-primary',
cssVariable: '--w-color-primary',
usage: 'Wagtail branding, Panels, Headings, Buttons, Labels',
contrastText: 'white',
},
200: {
hex: '#261A4E',
hsl: 'hsl(254, 50%, 20%)',
bgUtility: 'w-bg-primary-200',
textUtility: 'w-text-primary-200',
cssVariable: '--w-color-primary-200',
usage:
'Accent for elements used in conjunction with primary colour in sidebar',
contrastText: 'white',
@ -128,15 +156,19 @@ const colors = {
info: {
100: {
hex: '#1F7E9A',
hsl: 'hsl(194, 66%, 36%)',
bgUtility: 'w-bg-info-100',
textUtility: 'w-text-info-100',
cssVariable: '--w-color-info-100',
usage: 'Background and icons for information messages',
contrastText: 'white',
},
50: {
hex: '#E2F5FC',
hsl: 'hsl(196, 81%, 94%)',
bgUtility: 'w-bg-info-50',
textUtility: 'w-text-info-50',
cssVariable: '--w-color-info-50',
usage: 'Background only, for information messages',
contrastText: 'primary',
},
@ -144,15 +176,19 @@ const colors = {
positive: {
100: {
hex: '#1B8666',
hsl: 'hsl(162, 66%, 32%)',
bgUtility: 'w-bg-positive-100',
textUtility: 'w-text-positive-100',
cssVariable: '--w-color-positive-100',
usage: 'Positive states',
contrastText: 'white',
},
50: {
hex: '#E0FBF4',
hsl: 'hsl(164, 77%, 93%)',
bgUtility: 'w-bg-positive-50',
textUtility: 'w-text-positive-50',
cssVariable: '--w-color-positive-50',
usage: 'Background only, for positive states',
contrastText: 'primary',
},
@ -160,15 +196,19 @@ const colors = {
warning: {
100: {
hex: '#FAA500',
hsl: 'hsl(40, 100%, 49%)',
bgUtility: 'w-bg-warning-100',
textUtility: 'w-text-warning-100',
cssVariable: '--w-color-warning-100',
usage: 'Background and icons for potentially dangerous states',
contrastText: 'primary',
},
50: {
hex: '#FAECD5',
hsl: 'hsl(37, 79%, 91%)',
bgUtility: 'w-bg-warning-50',
textUtility: 'w-text-warning-50',
cssVariable: '--w-color-warning-50',
usage: 'Background only, for potentially dangerous states',
contrastText: 'primary',
},
@ -176,22 +216,28 @@ const colors = {
critical: {
200: {
hex: '#CD4444',
hsl: 'hsl(0, 58%, 54%)',
bgUtility: 'w-bg-critical-200',
textUtility: 'w-text-critical-200',
cssVariable: '--w-color-critical-200',
usage: 'Dangerous actions or states (over light background), errors',
contrastText: 'white',
},
100: {
hex: '#FD5765',
hsl: 'hsl(355, 98%, 67%)',
bgUtility: 'w-bg-critical-100',
textUtility: 'w-text-critical-100',
cssVariable: '--w-color-critical-100',
usage: 'Dangerous actions or states (over dark background)',
contrastText: 'primary',
},
50: {
hex: '#FDE9E9',
hsl: 'hsl(0, 83%, 95%)',
bgUtility: 'w-bg-critical-50',
textUtility: 'w-text-critical-50',
cssVariable: '--w-color-critical-50',
usage: 'Background only, for dangerous states',
contrastText: 'primary',
},

Wyświetl plik

@ -4,6 +4,7 @@ const vanillaRTL = require('tailwindcss-vanilla-rtl');
* Design Tokens
*/
const colors = require('./src/tokens/colors');
const { generateColorVariables } = require('./src/tokens/colorVariables');
const {
fontFamily,
fontSize,
@ -32,7 +33,10 @@ const scrollbarThin = require('./src/plugins/scrollbarThin');
const themeColors = Object.fromEntries(
Object.entries(colors).map(([key, hues]) => {
const shades = Object.fromEntries(
Object.entries(hues).map(([k, shade]) => [k, shade.hex]),
Object.entries(hues).map(([k, shade]) => [
k,
`var(${shade.cssVariable})`,
]),
);
return [key, shades];
}),
@ -49,12 +53,22 @@ module.exports = {
},
colors: {
...themeColors,
inherit: 'inherit',
current: 'currentColor',
transparent: 'transparent',
// Fades of white and black are not customisable.
'white-15': 'rgba(255, 255, 255, 0.15)',
'white-50': 'rgba(255, 255, 255, 0.50)',
'white-80': 'rgba(255, 255, 255, 0.80)',
'white-85': 'rgba(255, 255, 255, 0.85)',
'black-10': 'rgba(0, 0, 0, 0.10)',
'black-20': 'rgba(0, 0, 0, 0.20)',
'black-35': 'rgba(0, 0, 0, 0.35)',
'black-50': 'rgba(0, 0, 0, 0.50)',
// Color keywords.
'inherit': 'inherit',
'current': 'currentColor',
'transparent': 'transparent',
/* allow system colours https://www.w3.org/TR/css-color-4/#css-system-colors */
LinkText: 'LinkText',
ButtonText: 'ButtonText',
'LinkText': 'LinkText',
'ButtonText': 'ButtonText',
},
fontFamily: {
sans: 'var(--w-font-sans)',
@ -135,6 +149,7 @@ module.exports = {
':root': {
'--w-font-sans': fontFamily.sans.join(', '),
'--w-font-mono': fontFamily.mono.join(', '),
...generateColorVariables(colors),
},
});
}),