From 7f28dd7299a533c2939360262403556c59a916a3 Mon Sep 17 00:00:00 2001 From: madrilene Date: Fri, 10 Jan 2025 19:17:33 +0100 Subject: [PATCH] implement color workflow with palettes and fixed colors --- package-lock.json | 8 ++ package.json | 2 + src/_config/setup/create-colors.js | 102 ++++++++++++++++++ src/_data/designTokens/colors.json | 58 ++++++++-- src/_data/designTokens/colorsBase.json | 25 +++++ src/assets/css/bundle/pagination.css | 2 +- src/assets/css/global/base/variables.css | 130 +++++++---------------- src/assets/css/global/blocks/button.css | 2 + src/docs/design-tokens.md | 23 +++- src/pages/blog.njk | 2 +- src/pages/index.njk | 2 +- src/pages/styleguide.njk | 11 +- 12 files changed, 258 insertions(+), 109 deletions(-) create mode 100644 src/_config/setup/create-colors.js create mode 100644 src/_data/designTokens/colorsBase.json diff --git a/package-lock.json b/package-lock.json index 069bdfd..4256bdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "devDependencies": { "@toycode/markdown-it-class": "^1.2.4", "autoprefixer": "^10.4.13", + "colorjs.io": "^0.5.2", "cross-env": "^7.0.3", "cssnano": "^7.0.1", "dayjs": "^1.11.5", @@ -2207,6 +2208,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", diff --git a/package.json b/package.json index 9bee784..cf70c23 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "scripts": { "clean": "rimraf dist src/_includes/css src/_includes/scripts", "favicons": "node ./src/_config/setup/generate-favicons.js", + "colors": "node ./src/_config/setup/create-colors.js", "screenshots": "node ./src/_config/setup/generate-screenshots.js", "dev:clean": "rimraf src/assets/og-images", "dev:11ty": "cross-env ELEVENTY_ENV=development eleventy --serve --watch", @@ -37,6 +38,7 @@ "devDependencies": { "@toycode/markdown-it-class": "^1.2.4", "autoprefixer": "^10.4.13", + "colorjs.io": "^0.5.2", "cross-env": "^7.0.3", "cssnano": "^7.0.1", "dayjs": "^1.11.5", diff --git a/src/_config/setup/create-colors.js b/src/_config/setup/create-colors.js new file mode 100644 index 0000000..7d586c1 --- /dev/null +++ b/src/_config/setup/create-colors.js @@ -0,0 +1,102 @@ +import fs from 'node:fs'; +import Color from 'colorjs.io'; + +const colorsBase = JSON.parse(fs.readFileSync('./src/_data/designTokens/colorsBase.json', 'utf-8')); + +const generatePalette = (baseColorHex, steps) => { + const baseColor = new Color(baseColorHex).to('oklch'); + + return steps.map(step => { + const color = new Color('oklch', [step.lightness, baseColor.c * step.chromaFactor, baseColor.h]).to( + 'srgb' + ); + + const [r, g, b] = color.coords.map(value => Math.round(Math.min(Math.max(value * 255, 0), 255))); + + const hexValue = `#${r.toString(16).padStart(2, '0')}${g + .toString(16) + .padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + + return { + name: `${step.label}`, + value: hexValue + }; + }); +}; + +const vibrantSteps = [ + {label: '100', lightness: 0.96, chromaFactor: 0.19}, + {label: '200', lightness: 0.94, chromaFactor: 0.45}, + {label: '300', lightness: 0.86, chromaFactor: 0.78}, + {label: '400', lightness: 0.75, chromaFactor: 0.9}, + {label: '500', lightness: 0.62, chromaFactor: 1}, + {label: '600', lightness: 0.5, chromaFactor: 1}, + {label: '700', lightness: 0.42, chromaFactor: 1}, + {label: '800', lightness: 0.36, chromaFactor: 0.85}, + {label: '900', lightness: 0.2, chromaFactor: 0.55} +]; + +const neutralSteps = [ + {label: '100', lightness: 0.98, chromaFactor: 0.12}, + {label: '200', lightness: 0.92, chromaFactor: 0.14}, + {label: '300', lightness: 0.85, chromaFactor: 0.14}, + {label: '400', lightness: 0.7, chromaFactor: 0.25}, + {label: '500', lightness: 0.6, chromaFactor: 0.3}, + {label: '600', lightness: 0.48, chromaFactor: 0.35}, + {label: '700', lightness: 0.4, chromaFactor: 0.3}, + {label: '800', lightness: 0.3, chromaFactor: 0.27}, + {label: '900', lightness: 0.2, chromaFactor: 0.25} +]; + +const colorTokens = { + title: colorsBase.title, + description: colorsBase.description, + items: [] +}; + +colorsBase.neutral.forEach(color => { + const palette = generatePalette(color.value, neutralSteps); + palette.forEach(shade => { + colorTokens.items.push({ + name: `${color.name} ${shade.name}`, + value: shade.value + }); + }); +}); + +colorsBase.vibrant.forEach(color => { + const palette = generatePalette(color.value, vibrantSteps); + palette.forEach(shade => { + colorTokens.items.push({ + name: `${color.name} ${shade.name}`, + value: shade.value + }); + }); +}); + +colorsBase.fixed.forEach(color => { + colorTokens.items.push({ + name: color.name, + value: color.value + }); + + const fixedColor = new Color(color.value).to('oklch'); + const subduedColor = new Color('oklch', [ + fixedColor.l, + fixedColor.c * 0.8, // reduce chroma by 20% + fixedColor.h + ]).to('srgb'); + + const [r, g, b] = subduedColor.coords.map(value => Math.round(Math.min(Math.max(value * 255, 0), 255))); + + const subduedHex = `#${r.toString(16).padStart(2, '0')}${g + .toString(16) + .padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; + + colorTokens.items.push({ + name: `${color.name} Subdued`, + value: subduedHex + }); +}); + +fs.writeFileSync('./src/_data/designTokens/colors.json', JSON.stringify(colorTokens, null, 2)); diff --git a/src/_data/designTokens/colors.json b/src/_data/designTokens/colors.json index 309a4d1..a3237d5 100644 --- a/src/_data/designTokens/colors.json +++ b/src/_data/designTokens/colors.json @@ -1,26 +1,66 @@ { "title": "Colors", - "description": "Hex color codes that can be shared, cross-platform. They can be converted at point of usage, such as HSL for web or CMYK for print. The first two colors are used by the theme toggle script to set the theme color.", + "description": "Hex color codes that can be shared, cross-platform. They can be converted at point of usage, such as HSL for web or CMYK for print. neutral and vibrant colors are converted to color palettes, fixed colors are kept as they are", "items": [ { - "name": "Base Dark", - "value": "#343434" + "name": "Gray 100", + "value": "#f8f8f8" }, { - "name": "Base Light", - "value": "#FBFBFB" + "name": "Gray 200", + "value": "#e4e4e4" }, { - "name": "Primary Highlight", + "name": "Gray 300", + "value": "#cecece" + }, + { + "name": "Gray 400", + "value": "#9e9e9e" + }, + { + "name": "Gray 500", + "value": "#808080" + }, + { + "name": "Gray 600", + "value": "#5d5d5d" + }, + { + "name": "Gray 700", + "value": "#484848" + }, + { + "name": "Gray 800", + "value": "#2e2e2e" + }, + { + "name": "Gray 900", + "value": "#161616" + }, + { + "name": "Pink", "value": "#dd4462" }, { - "name": "Secondary Highlight", + "name": "Pink Subdued", + "value": "#ce5769" + }, + { + "name": "Blue", "value": "#4467dd" }, { - "name": "Tertiary Highlight", + "name": "Blue Subdued", + "value": "#4d6cc8" + }, + { + "name": "Gold", "value": "#fbbe25" + }, + { + "name": "Gold Subdued", + "value": "#f1c15b" } ] -} +} \ No newline at end of file diff --git a/src/_data/designTokens/colorsBase.json b/src/_data/designTokens/colorsBase.json new file mode 100644 index 0000000..99c35fa --- /dev/null +++ b/src/_data/designTokens/colorsBase.json @@ -0,0 +1,25 @@ +{ + "title": "Colors", + "description": "Hex color codes that can be shared, cross-platform. They can be converted at point of usage, such as HSL for web or CMYK for print. neutral and vibrant colors are converted to color palettes, fixed colors are kept as they are", + "neutral": [ + { + "name": "Gray", + "value": "#888888" + } + ], + "vibrant": [], + "fixed": [ + { + "name": "Pink", + "value": "#dd4462" + }, + { + "name": "Blue", + "value": "#4467dd" + }, + { + "name": "Gold", + "value": "#fbbe25" + } + ] +} diff --git a/src/assets/css/bundle/pagination.css b/src/assets/css/bundle/pagination.css index 8edc420..b13c3c7 100644 --- a/src/assets/css/bundle/pagination.css +++ b/src/assets/css/bundle/pagination.css @@ -37,5 +37,5 @@ .pagination li:has(a[aria-current='page']) { --pagination-bg: var(--color-secondary); --pagination-border: var(--color-secondary); - --pagination-text: var(--color-base-light); + --pagination-text: var(--color-light); } diff --git a/src/assets/css/global/base/variables.css b/src/assets/css/global/base/variables.css index 80bb62b..e4ad35d 100644 --- a/src/assets/css/global/base/variables.css +++ b/src/assets/css/global/base/variables.css @@ -1,6 +1,4 @@ /* Global variables. */ -/* This turned out a little too complex, and probably is overly specific. I'm still fiddling with this. Should work by itself though, once you define your design token colors. -Adjust fallbacks if modern color syntax not supported for text and bg variants. */ /* Basic variable definitions for color schemes */ :root { @@ -20,108 +18,58 @@ Adjust fallbacks if modern color syntax not supported for text and bg variants. ); --gradient-stripes: linear-gradient( 45deg, - var(--color-base-dark) 0 75%, + var(--color-gray-900) 0 75%, var(--color-primary) 0 85%, var(--color-secondary) 0 92%, var(--color-tertiary) 0 100% ); + + --color-light: var(--color-gray-100); + --color-dark: var(--color-gray-900); } -/* Default / Light theme specific variables */ +/* Light theme */ :root, :root[data-theme='light'] { - --color-dark: var(--color-base-dark); - --color-light: var(--color-base-light); - --color-text: var(--color-base-dark); - --color-bg: var(--color-base-light); - --color-base: var(--color-base-dark); - --color-primary: var(--color-primary-highlight); - --color-secondary: var(--color-secondary-highlight); - --color-tertiary: var(--color-tertiary-highlight); - /* Fallbacks if modern color syntax not supported */ - --color-text-accent: #505050; - --color-bg-accent: #fdfdfd; - --color-bg-accent-2: #f7f7f7; + --color-text: var(--color-gray-800); + --color-bg: var(--color-gray-100); + --color-base: var(--color-gray-800); + + --color-primary: var(--color-pink); + --color-secondary: var(--color-blue); + --color-tertiary: var(--color-gold); + + --color-text-accent: var(--color-gray-700); + --color-bg-accent: var(--color-gray-200); + --color-bg-accent-2: var(--color-gray-300); } -/* Dark theme specific variables based on system preference */ +/* Dark theme */ @media (prefers-color-scheme: dark) { :root { - --color-text: var(--color-base-light); - --color-bg: var(--color-base-dark); - --color-base: var(--color-base-light); - --color-primary: var(--color-primary-highlight); - --color-secondary: var(--color-secondary-highlight); - --color-tertiary: var(--color-tertiary-highlight); - /* Fallbacks if modern color syntax not supported */ - --color-text-accent: #a8a8a8; - --color-bg-accent: #383838; - --color-bg-accent-2: #404040; + --color-text: var(--color-gray-100); + --color-bg: var(--color-gray-800); + --color-base: var(--color-gray-100); + + --color-primary: var(--color-pink-subdued); + --color-secondary: var(--color-blue-subdued); + --color-tertiary: var(--color-gold-subdued); + + --color-text-accent: var(--color-gray-300); + --color-bg-accent: var(--color-gray-700); + --color-bg-accent-2: var(--color-gray-600); } } - -/* Dark theme specific variables based on manual override */ :root[data-theme='dark'] { - --color-text: var(--color-base-light); - --color-bg: var(--color-base-dark); - --color-base: var(--color-base-light); - --color-primary: var(--color-primary-highlight); - --color-secondary: var(--color-secondary-highlight); - --color-tertiary: var(--color-tertiary-highlight); - /* Fallbacks if modern color syntax not supported */ - --color-text-accent: #a8a8a8; - --color-bg-accent: #383838; - --color-bg-accent-2: #404040; -} - -/* Enhancements for browsers that support color-mix */ -@supports (background: color-mix(in srgb, red 50%, blue)) { - :root, - :root[data-theme='light'] { - --color-text-accent: color-mix(in oklab, var(--color-base-dark) 80%, var(--color-bg)); - --color-bg-accent: color-mix(in oklab, var(--color-bg) 90%, var(--color-text)); - --color-bg-accent-2: color-mix(in oklab, var(--color-bg) 70%, var(--color-text)); - --color-primary-sub-20: color-mix(in oklab, var(--color-primary-highlight), var(--color-bg) 20%); - --color-secondary-sub-20: color-mix(in oklab, var(--color-secondary-highlight), var(--color-bg) 20%); - --color-tertiary-sub-20: color-mix(in oklab, var(--color-tertiary-highlight), var(--color-bg) 20%); - } - - @media (prefers-color-scheme: dark) { - :root { - --color-text-accent: color-mix(in oklab, var(--color-base-light) 70%, var(--color-bg)); - --color-bg-accent: color-mix(in oklab, var(--color-bg) 92%, var(--color-text)); - --color-bg-accent-2: color-mix(in oklab, var(--color-bg) 80%, var(--color-text)); - /* add opacity with color-mix */ - --color-primary: var(--color-primary-sub-20); - --color-secondary: var(--color-secondary-sub-20); - --color-tertiary: var(--color-tertiary-sub-20); - } - } - - :root[data-theme='dark'] { - --color-text-accent: color-mix(in oklab, var(--color-base-light) 70%, var(--color-bg)); - --color-bg-accent: color-mix(in oklab, var(--color-bg) 92%, var(--color-text)); - --color-bg-accent-2: color-mix(in oklab, var(--color-bg) 80%, var(--color-text)); - /* add opacity with color-mix */ - --color-primary: var(--color-primary-sub-20); - --color-secondary: var(--color-secondary-sub-20); - --color-tertiary: var(--color-tertiary-sub-20); - } -} - -/* Enhancements for browsers that support relative color values */ -@supports (color: hsl(from red h s l)) { - @media (prefers-color-scheme: dark) { - :root { - --color-primary: hsl(from var(--color-primary-highlight) h calc(s / 1.2) l); - --color-secondary: hsl(from var(--color-secondary-highlight) h calc(s / 1.2) l); - --color-tertiary: hsl(from var(--color-tertiary-highlight) h calc(s / 1.2) l); - } - } - - :root[data-theme='dark'] { - --color-primary: hsl(from var(--color-primary-highlight) h calc(s / 1.2) l); - --color-secondary: hsl(from var(--color-secondary-highlight) h calc(s / 1.2) l); - --color-tertiary: hsl(from var(--color-tertiary-highlight) h calc(s / 1.2) l); - } + --color-text: var(--color-gray-100); + --color-bg: var(--color-gray-800); + --color-base: var(--color-gray-100); + + --color-primary: var(--color-pink-subdued); + --color-secondary: var(--color-blue-subdued); + --color-tertiary: var(--color-gold-subdued); + + --color-text-accent: var(--color-gray-300); + --color-bg-accent: var(--color-gray-700); + --color-bg-accent-2: var(--color-gray-600); } diff --git a/src/assets/css/global/blocks/button.css b/src/assets/css/global/blocks/button.css index 70c99b4..5676d5d 100644 --- a/src/assets/css/global/blocks/button.css +++ b/src/assets/css/global/blocks/button.css @@ -1,3 +1,5 @@ +/* based on Andy Bell's article: https://piccalil.li/blog/how-i-build-a-button-component/ */ + .button { --button-bg: var(--color-text); --button-color: color-mix(in oklab, var(--button-bg) 10%, var(--color-bg)); diff --git a/src/docs/design-tokens.md b/src/docs/design-tokens.md index c238526..4392ad8 100644 --- a/src/docs/design-tokens.md +++ b/src/docs/design-tokens.md @@ -4,7 +4,26 @@ title: Design tokens Edit all your preferences (colors, fluid text sizes etc.) in `src/_data/designTokens/*.json`. -Additional colors, variants and gradients for custom properties are automatically created in `src/assets/css/global/base/variables.css` based on the colors set in `colors.json`. If you change names you should edit `variables.css` as well and check if the names are used elsewhere in the template. -Admittedly, I went a little bit crazy there with the new CSS color syntax stuff. +Additional colors, variants and gradients for custom properties are automatically created in `src/assets/css/global/base/variables.css` based on the colors set in `colors.json`. In the [style guide](/styleguide/) you can see how everything turns out. + +**Special case: colors** + +As of version 4.0, you can create colors dynamically. Run `npm run colors` after setting your color values in `src/_data/designTokens/colorsBase.json`. This will create / overwrite the required `colors.json` file in the same directory. These colors become custom properties (e.g. `--color-gray-100`) and utility classes similar to the Tailwind CSS syntax (for example `bg-gray-100`, `text-gray-900`). + +If you want to adjust how the colors turn out, edit `src/_config/setup/create-colors.js`. + +Placed under `neutral` or `vibrant`, colors are converted into scalable palettes. `neutral` is better for grayish / monochromatic colors, while `vibrant` is better for colorful palettes. Colors listed under `fixed` remain as standalone values without generating shades. Each color placed here also generates a "subdued" version for the dark theme. + +```js +// this creates a palette with shades of green, 100 to 900 + "vibrant": [ + { + "name": "green", + "value": "#008000" + } + ], +``` + +Important: If you change the color names, you must edit `src/assets/css/global/base/variables.css` with your color names. The rest of the CSS files should only reference custom properties set in `variables.css`. \ No newline at end of file diff --git a/src/pages/blog.njk b/src/pages/blog.njk index 0be46ac..f605149 100644 --- a/src/pages/blog.njk +++ b/src/pages/blog.njk @@ -11,7 +11,7 @@ permalink: 'blog/{% if pagination.pageNumber >=1 %}page-{{ pagination.pageNumber
-

{{ title }}

+

{{ title }}

{% svg "divider/edgy", null, "seperator" %} diff --git a/src/pages/index.njk b/src/pages/index.njk index 02310c8..26b18ac 100644 --- a/src/pages/index.njk +++ b/src/pages/index.njk @@ -11,7 +11,7 @@ blog:
-

{{ title }}

+

{{ title }}

{% svg "divider/waves", null, "seperator" %} diff --git a/src/pages/styleguide.njk b/src/pages/styleguide.njk index 7a2d534..343879c 100644 --- a/src/pages/styleguide.njk +++ b/src/pages/styleguide.njk @@ -37,13 +37,13 @@ customGradients: Default button
  • - blue button + pink button
  • - red button + blue button
  • - yellow button + gold button
  • @@ -70,7 +70,10 @@ customGradients:

    - var(--color-{{ color.name | slugify }}) + #{{ color.value | slugify }}
    + var(--color-{{ color.name | slugify }})

  • {%- endfor -%}