feat(ui): consolidate and simplify how we use colors

* reconciles latest color variables from Figma
* defines the primary colors and the color system in tailwind config.
* refines how we generate utilities within the color system for `bg`, `fg` and `border`.
* removes custom box shadows, instead leaning on direct definition in CSS and `theme()`.

This change removes duplicate color information being defined as CSS variables in `index.css`
and co-locates all style information with the component
whilst retaining the ability to tie in to the color system when mapping styles from Figma.
This commit is contained in:
Brian McGee
2025-06-18 17:10:12 +01:00
parent 4c0ad55e35
commit 519b24ba0f
13 changed files with 306 additions and 342 deletions

View File

@@ -1,94 +1,267 @@
import plugin from "tailwindcss/plugin";
// @ts-expect-error: lib of tailwind has no types
import { parseColor } from "tailwindcss/lib/util/color";
import { CSSRuleObject, RecursiveKeyValuePair } from "tailwindcss/types/config";
/* Converts HEX color to RGB */
const toRGB = (value: string) =>
"rgb(" + parseColor(value).color.join(" ") + ")";
const mkBorderUtils = (
theme: (n: string) => unknown,
prefix: string,
cssProperty: string,
) => ({
// - def colors
[`.${prefix}-def-1`]: {
[cssProperty]: theme("colors.secondary.100"),
const primaries = {
off: {
white: toRGB("#ffffff"),
black: toRGB("#000000"),
},
[`.${prefix}-def-2`]: {
[cssProperty]: theme("colors.secondary.200"),
primary: {
50: toRGB("#f4f9f9"),
100: toRGB("#dbeceb"),
200: toRGB("#b6d9d6"),
300: toRGB("#8abebc"),
400: toRGB("#478585"),
500: toRGB("#526f6f"),
600: toRGB("#4b6767"),
700: toRGB("#345253"),
800: toRGB("#2e4a4b"),
900: toRGB("#203637"),
950: toRGB("#162324"),
},
[`.${prefix}-def-3`]: {
[cssProperty]: theme("colors.secondary.300"),
secondary: {
50: toRGB("#f7f9fa"),
100: toRGB("#e7f2f4"),
200: toRGB("#d8e8eb"),
300: toRGB("#afc6ca"),
400: toRGB("#90b2b7"),
500: toRGB("#7b9b9f"),
600: toRGB("#4f747a"),
700: toRGB("#415e63"),
800: toRGB("#446065"),
900: toRGB("#2c4347"),
950: toRGB("#0d1416"),
},
[`.${prefix}-def-4`]: {
[cssProperty]: theme("colors.secondary.400"),
error: {
50: toRGB("#fff0f4"),
100: toRGB("#ffe2ea"),
200: toRGB("#ffcadb"),
300: toRGB("#ff9fbd"),
400: toRGB("#ff699b"),
500: toRGB("#ff2c78"),
600: toRGB("#ed116b"),
700: toRGB("#c8085b"),
800: toRGB("#a80953"),
900: toRGB("#8f0c4d"),
950: toRGB("#500126"),
},
// - def acc colors
[`.${prefix}-def-acc-1`]: {
[cssProperty]: theme("colors.secondary.500"),
info: {
50: toRGB("#eff9ff"),
100: toRGB("#d6ebff"),
200: toRGB("#b5deff"),
300: toRGB("#83cbff"),
400: toRGB("#48adff"),
500: toRGB("#1e88ff"),
600: toRGB("#0666ff"),
700: toRGB("#0051ff"),
800: toRGB("#083fc5"),
900: toRGB("#0d3a9b"),
950: toRGB("#0e245d"),
},
[`.${prefix}-def-acc-2`]: {
[cssProperty]: theme("colors.secondary.900"),
success: {
50: toRGB("#eefff3"),
100: toRGB("#d7ffe4"),
200: toRGB("#b2ffcb"),
300: toRGB("#76ffa4"),
400: toRGB("#34f475"),
500: toRGB("#0ae856"),
600: toRGB("#01b83f"),
700: toRGB("#059035"),
800: toRGB("#0a712e"),
900: toRGB("#0b5c29"),
950: toRGB("#003414"),
},
[`.${prefix}-def-acc-3`]: {
[cssProperty]: theme("colors.secondary.900"),
},
[`.${prefix}-def-acc-4`]: {
[cssProperty]: theme("colors.secondary.950"),
},
// - inverse colors
[`.${prefix}-inv-1`]: {
[cssProperty]: theme("colors.secondary.700"),
},
[`.${prefix}-inv-2`]: {
[cssProperty]: theme("colors.secondary.800"),
},
[`.${prefix}-inv-3`]: {
[cssProperty]: theme("colors.secondary.900"),
},
[`.${prefix}-inv-4`]: {
[cssProperty]: theme("colors.secondary.950"),
},
// - inverse acc
[`.${prefix}-inv-acc-1`]: {
[cssProperty]: theme("colors.secondary.300"),
},
[`.${prefix}-inv-acc-2`]: {
[cssProperty]: theme("colors.secondary.200"),
},
[`.${prefix}-inv-acc-3`]: {
[cssProperty]: theme("colors.secondary.100"),
},
[`.${prefix}-inv-acc-4`]: {
[cssProperty]: theme("colors.secondary.50"),
},
[`.${prefix}-int-1`]: {
[cssProperty]: theme("colors.info.500"),
},
[`.${prefix}-int-2`]: {
[cssProperty]: theme("colors.info.600"),
},
[`.${prefix}-int-3`]: {
[cssProperty]: theme("colors.info.700"),
},
[`.${prefix}-int-4`]: {
[cssProperty]: theme("colors.info.800"),
warning: {
50: toRGB("#feffe4"),
100: toRGB("#faffc4"),
200: toRGB("#faffc4"),
300: toRGB("#e8ff50"),
400: toRGB("#d4ff00"),
500: toRGB("#b9e600"),
600: toRGB("#90b800"),
700: toRGB("#6c8b00"),
800: toRGB("#556d07"),
900: toRGB("#485c0b"),
950: toRGB("#253400"),
},
};
[`.${prefix}-semantic-1`]: {
[cssProperty]: theme("colors.error.500"),
const colorSystem = {
bg: {
def: {
1: primaries.off.white,
2: primaries.secondary["50"],
3: primaries.secondary["100"],
4: primaries.secondary["200"],
acc: {
1: primaries.primary["50"],
2: primaries.secondary["100"],
3: primaries.secondary["200"],
4: primaries.secondary["300"],
},
},
semantic: {
error: {
1: primaries.error["50"],
2: primaries.error["100"],
3: primaries.error["200"],
4: primaries.error["300"],
},
info: {
1: primaries.info["50"],
2: primaries.info["100"],
3: primaries.info["200"],
4: primaries.info["300"],
},
success: {
1: primaries.success["50"],
2: primaries.success["100"],
3: primaries.success["200"],
4: primaries.success["300"],
},
warning: {
1: primaries.warning["50"],
2: primaries.warning["100"],
3: primaries.warning["200"],
4: primaries.warning["300"],
},
},
inv: {
1: primaries.primary["600"],
2: primaries.primary["700"],
3: primaries.primary["800"],
4: primaries.primary["900"],
acc: {
1: primaries.secondary["500"],
2: primaries.secondary["600"],
3: primaries.secondary["900"],
4: primaries.secondary["950"],
},
},
},
[`.${prefix}-semantic-2`]: {
[cssProperty]: theme("colors.error.600"),
border: {
def: {
1: primaries.secondary["100"],
2: primaries.secondary["200"],
3: primaries.secondary["300"],
4: primaries.secondary["400"],
acc: {
1: primaries.secondary["500"],
2: primaries.secondary["900"],
3: primaries.secondary["900"],
4: primaries.secondary["950"],
},
},
semantic: {
error: {
1: primaries.error["100"],
2: primaries.error["200"],
3: primaries.error["300"],
4: primaries.error["400"],
},
info: {
1: primaries.info["100"],
2: primaries.info["200"],
3: primaries.info["300"],
4: primaries.info["400"],
},
success: {
1: primaries.success["100"],
2: primaries.success["200"],
3: primaries.success["300"],
4: primaries.success["400"],
},
warning: {
1: primaries.warning["100"],
2: primaries.warning["200"],
3: primaries.warning["300"],
4: primaries.warning["400"],
},
},
inv: {
1: primaries.secondary["700"],
2: primaries.secondary["800"],
3: primaries.secondary["900"],
4: primaries.secondary["950"],
acc: {
1: primaries.secondary["300"],
2: primaries.secondary["200"],
3: primaries.secondary["100"],
4: primaries.secondary["50"],
},
},
},
[`.${prefix}-semantic-3`]: {
[cssProperty]: theme("colors.error.700"),
fg: {
def: {
1: primaries.secondary["950"],
2: primaries.secondary["900"],
3: primaries.secondary["700"],
4: primaries.secondary["400"],
},
inv: {
1: primaries.off.white,
2: primaries.secondary["100"],
3: primaries.secondary["300"],
4: primaries.secondary["400"],
},
semantic: {
error: {
1: primaries.error["500"],
2: primaries.error["600"],
3: primaries.error["700"],
4: primaries.error["800"],
},
info: {
1: primaries.info["500"],
2: primaries.info["600"],
3: primaries.info["700"],
4: primaries.info["800"],
},
success: {
1: primaries.success["500"],
2: primaries.success["600"],
3: primaries.success["700"],
4: primaries.success["800"],
},
warning: {
1: primaries.warning["500"],
2: primaries.warning["600"],
3: primaries.warning["700"],
4: primaries.warning["800"],
},
},
},
[`.${prefix}-semantic-4`]: {
[cssProperty]: theme("colors.error.800"),
},
});
};
function isString(value: unknown): value is string {
return typeof value === "string";
}
const mkColorUtil = (
path: string[],
property: string,
config: string | RecursiveKeyValuePair,
): CSSRuleObject => {
if (isString(config)) {
return {
[`.${path.join("-")}`]: {
[`${property}`]: config,
},
};
}
return Object.entries(config as RecursiveKeyValuePair)
.map(([subKey, subConfig]) =>
mkColorUtil([...path, subKey], property, subConfig),
)
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
};
export default plugin.withOptions(
(_options = {}) =>
@@ -100,219 +273,24 @@ export default plugin.withOptions(
return `.${e(`popover-open${separator}${className}`)}:popover-open`;
});
});
addUtilities({
// Background colors
// default
".bg-def-1": {
backgroundColor: theme("colors.white"),
},
".bg-def-2": {
backgroundColor: theme("colors.secondary.50"),
},
".bg-def-3": {
backgroundColor: theme("colors.secondary.100"),
},
".bg-def-4": {
backgroundColor: theme("colors.secondary.200"),
},
// default accessible
".bg-def-acc-1": {
backgroundColor: theme("colors.primary.50"),
},
".bg-def-acc-2": {
backgroundColor: theme("colors.secondary.100"),
},
".bg-def-acc-3": {
backgroundColor: theme("colors.secondary.200"),
},
".bg-def-acc-4": {
backgroundColor: theme("colors.secondary.300"),
},
// bg inverse
".bg-inv-1": {
backgroundColor: theme("colors.primary.600"),
},
".bg-inv-2": {
backgroundColor: theme("colors.primary.700"),
},
".bg-inv-3": {
backgroundColor: theme("colors.primary.800"),
},
".bg-inv-4": {
backgroundColor: theme("colors.primary.900"),
},
// bg inverse accessible
".bg-inv-acc-1": {
backgroundColor: theme("colors.secondary.500"),
},
".bg-inv-acc-2": {
backgroundColor: theme("colors.secondary.600"),
},
".bg-inv-acc-3": {
backgroundColor: theme("colors.secondary.700"),
},
".bg-inv-acc-4": {
backgroundColor: theme("colors.primary.950"),
},
const { bg, fg, border } = colorSystem;
// bg inverse accent
".bg-semantic-1": {
backgroundColor: theme("colors.error.50"),
},
".bg-semantic-2": {
backgroundColor: theme("colors.error.100"),
},
".bg-semantic-3": {
backgroundColor: theme("colors.error.200"),
},
".bg-semantic-4": {
backgroundColor: theme("colors.error.300"),
},
// Text colors
".fg-def-1": {
color: theme("colors.secondary.950"),
},
".fg-def-2": {
color: theme("colors.secondary.900"),
},
".fg-def-3": {
color: theme("colors.secondary.700"),
},
".fg-def-4": {
color: theme("colors.secondary.400"),
},
// fg inverse
".fg-inv-1": {
color: theme("colors.white"),
},
".fg-inv-2": {
color: theme("colors.secondary.100"),
},
".fg-inv-3": {
color: theme("colors.secondary.300"),
},
".fg-inv-4": {
color: theme("colors.secondary.400"),
},
".fg-semantic-1": {
color: theme("colors.error.500"),
},
".fg-semantic-2": {
color: theme("colors.error.600"),
},
".fg-semantic-3": {
color: theme("colors.error.700"),
},
".fg-semantic-4": {
color: theme("colors.error.800"),
},
...mkBorderUtils(theme, "border", "borderColor"),
...mkBorderUtils(theme, "border-b", "borderBottom"),
...mkBorderUtils(theme, "border-t", "borderTop"),
...mkBorderUtils(theme, "border-l", "borderLeft"),
...mkBorderUtils(theme, "border-r", "borderRight"),
// Example: dark mode utilities (all elements within <html class="dark"> )
// ".dark .bg-def-1": {
// backgroundColor: theme("colors.black"),
// },
// ".dark .bg-def-2": {
// backgroundColor: theme("colors.primary.900"),
// },
// ".dark .bg-def-3": {
// backgroundColor: theme("colors.primary.800"),
// },
// ".dark .bg-def-4": {
// backgroundColor: theme("colors.primary.700"),
// },
// ".dark .bg-def-5": {
// backgroundColor: theme("colors.primary.600"),
// },
});
// add more base styles
addUtilities(mkColorUtil(["bg"], "backgroundColor", bg));
addUtilities(mkColorUtil(["fg"], "color", fg));
addUtilities(mkColorUtil(["border"], "borderColor", border));
addUtilities(mkColorUtil(["border-t"], "borderTop", border));
addUtilities(mkColorUtil(["border-r"], "borderRight", border));
addUtilities(mkColorUtil(["border-b"], "borderBottom", border));
addUtilities(mkColorUtil(["border-l"], "borderLeft", border));
},
// add configuration which is merged with the final config
() => ({
theme: {
extend: {
colors: {
white: toRGB("#ffffff"),
black: toRGB("#000000"),
primary: {
50: toRGB("#f4f9f9"),
100: toRGB("#dbeceb"),
200: toRGB("#b6d9d6"),
300: toRGB("#8abebc"),
400: toRGB("#478585"),
500: toRGB("#526f6f"),
600: toRGB("#4b6767"),
700: toRGB("#345253"),
800: toRGB("#2e4a4b"),
900: toRGB("#203637"),
950: toRGB("#162324"),
},
secondary: {
50: toRGB("#f7f9fA"),
100: toRGB("#e7f2f4"),
200: toRGB("#d8e8eb"),
300: toRGB("#afc6ca"),
400: toRGB("#90b2b7"),
500: toRGB("#7b9b9f"),
600: toRGB("#4f747A"),
700: toRGB("#415e63"),
800: toRGB("#446065"),
900: toRGB("#2c4347"),
950: toRGB("#0d1416"),
},
info: {
50: toRGB("#eff9ff"),
100: toRGB("#dff2ff"),
200: toRGB("#b8e8ff"),
300: toRGB("#78d6ff"),
400: toRGB("#2cc0ff"),
500: toRGB("#06aaf1"),
600: toRGB("#006ca7"),
700: toRGB("#006ca7"),
800: toRGB("#025b8a"),
900: toRGB("#084c72"),
950: toRGB("#06304b"),
},
error: {
50: toRGB("#fcf3f8"),
100: toRGB("#f9eaf4"),
200: toRGB("#f5d5e9"),
300: toRGB("#ea9ecb"),
400: toRGB("#e383ba"),
500: toRGB("#d75d9f"),
600: toRGB("#c43e81"),
700: toRGB("#a82e67"),
800: toRGB("#8c2855"),
900: toRGB("#75264a"),
950: toRGB("#461129"),
},
},
boxShadow: {
"input-active": "0px 0px 0px 1px #FFF, 0px 0px 0px 2px #203637",
"button-primary":
"2px 2px 0px 0px var(--clr-bg-inv-acc-3, #415E63) inset",
"button-primary-hover":
"2px 2px 0px 0px var(--clr-bg-inv-acc-2, #4F747A) inset",
"button-primary-focus":
"0px 0px 0px 1px #FFF, 0px 0px 0px 2px var(--clr-border-def-sem-inf-1, #06AAF1), 2px 2px 0px 0px var(--clr-bg-inv-acc-2, #4F747A) inset",
"button-primary-active":
"0px 0px 0px 1px #FFF, 0px 0px 0px 2px var(--clr-bg-inv-acc-4, #203637), -2px -2px 0px 0px var(--clr-bg-inv-acc-1, #7B9B9F) inset",
"button-secondary":
"-2px -2px 0px 0px #CEDFE2 inset, 2px 2px 0px 0px white inset",
"button-secondary-hover":
"-2px -2px 0px 0px #CEDFE2 inset, 2px 2px 0px 0px #FFF inset",
"button-secondary-focus":
"0px 0px 0px 1px #FFF, 0px 0px 0px 2px var(--clr-border-def-sem-inf-1, #06AAF1), -2px -2px 0px 0px #CEDFE2 inset, 2px 2px 0px 0px #FFF inset",
"button-secondary-active":
"0px 0px 0px 1px white, 0px 0px 0px 2px var(--clr-bg-inv-acc-4, #203637), 2px 2px 0px 0px var(--clr-bg-inv-acc-2, #4F747A) inset",
...primaries,
...colorSystem,
},
},
},