From bbcd649577c3cd5023ddeb9efd24cce495dfffa0 Mon Sep 17 00:00:00 2001 From: "D. Moonfire" Date: Fri, 10 May 2024 01:37:29 -0500 Subject: [PATCH] feat: implemented the "css mixin" command --- Justfile | 5 +- README.md | 34 ++++- examples/dark-less-contrast.css | 3 +- examples/dark-more-contrast.css | 3 +- examples/dark.css | 3 +- examples/light-less-contrast.css | 3 +- examples/light-more-contrast.css | 3 +- examples/light.css | 3 +- examples/style.css | 2 +- package.json | 1 - src/cmds/css.mjs | 3 +- src/cmds/css/mixin.mjs | 231 +++++++++++++++++++++++++++++++ src/cmds/css/variables.mjs | 2 +- 13 files changed, 283 insertions(+), 13 deletions(-) create mode 100755 src/cmds/css/mixin.mjs diff --git a/Justfile b/Justfile index 048db82..50b49cf 100644 --- a/Justfile +++ b/Justfile @@ -1,3 +1,5 @@ +export DEBUG := "*" + @default: just --choose @@ -17,7 +19,8 @@ colors: format # Generate examples/theme.css examples: format - priduck-color-theme build \ + node src/cli.mjs css mixin \ + --selector :root \ --output examples/theme.css \ --light examples/light.css \ --light-more-contrast examples/light-more-contrast.css \ diff --git a/README.md b/README.md index 1012891..df480c7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,37 @@ Once installed, this will produce a CLI in the `node_modules/.bin` folder called ## Commands -### "css variables [--hue HUE] [--output PATH] [--selector SELECTOR]" +### css mixin + +Combines a number of CSS style fragments together and intersperses them in a manner that supports `prefers-color-scheme` and `prefers-contrast` in the various methods to support no preferences or a preference in one or more of the options. + +For purposes of the read me file, an example of use has to suffice. + +```shell +priduck css mixin --selector :root --output examples/theme.css --light examples/light.css --light-more-contrast examples/light-more-contrast.css --light-less-contrast examples/light-less-contrast.css --dark examples/dark.css --dark-more-contrast examples/dark-more-contrast.css --dark-less-contrast examples/dark-less-contrast.css +``` + +If a fragment isn't included, then it isn't used. So to generate an output file that defaults to dark mode and honors `prefers-color-scheme`, the command + +```shell +priduck css mixin --selector :root --output examples/theme.css --light examples/light.css --dark examples/dark.css --output style.css +``` + +The fragment files can either be a bare set of CSS properties, or they can use a pseudo-selector of `:theme`. + +```css +:theme { + color: red; +} +``` + +```css +color: red; +``` + +There is no parsing of CSS, so this can also be used with Sass or Less or Postcss. + +### css variables [--hue HUE] [--output PATH] [--selector SELECTOR] Generate a CSS file using variables. The thirty colors all follow the pattern of `--color-priduck-cXbY` where `X` is a number between 0 and 9 (inclusive) and `Y` is the brightness (also in the range of 0 to 9, inclusive). @@ -44,7 +74,7 @@ If `--selector` is included, the colors will be wrapped in a selector and slight } ``` -### "palette gpl --hue HUE [--output PATH] [--secondary 5] [--tertiary 8]" +### palette gpl --hue HUE [--output PATH] [--secondary 5] [--tertiary 8] Generate a GNU Imp or Inkscape file. `--hue` is required in this situation. This assumes up to two additional colors (secondary, and tertiary). diff --git a/examples/dark-less-contrast.css b/examples/dark-less-contrast.css index 8790fc1..0ffaebe 100644 --- a/examples/dark-less-contrast.css +++ b/examples/dark-less-contrast.css @@ -1,3 +1,4 @@ :theme { - @mixin theme 0, 7; + background-color: var(--color-priduck-c0b0); + color: var(--color-priduck-c0b7); } diff --git a/examples/dark-more-contrast.css b/examples/dark-more-contrast.css index 820fb51..62cb40e 100644 --- a/examples/dark-more-contrast.css +++ b/examples/dark-more-contrast.css @@ -1,3 +1,4 @@ :theme { - @mixin theme 0, 9; + background-color: var(--color-priduck-c0b0); + color: var(--color-priduck-c0b9); } diff --git a/examples/dark.css b/examples/dark.css index e25ffe1..6a7a48e 100644 --- a/examples/dark.css +++ b/examples/dark.css @@ -1,3 +1,4 @@ :theme { - @mixin theme 0, 8; + background-color: var(--color-priduck-c0b0); + color: var(--color-priduck-c0b8); } diff --git a/examples/light-less-contrast.css b/examples/light-less-contrast.css index 0cc3b8b..c0291f9 100644 --- a/examples/light-less-contrast.css +++ b/examples/light-less-contrast.css @@ -1,3 +1,4 @@ :theme { - @mixin theme 9, 2; + background-color: var(--color-priduck-c0b9); + color: var(--color-priduck-c0b2); } diff --git a/examples/light-more-contrast.css b/examples/light-more-contrast.css index 0a49295..2fb46fa 100644 --- a/examples/light-more-contrast.css +++ b/examples/light-more-contrast.css @@ -1,3 +1,4 @@ :theme { - @mixin theme 9, 0; + background-color: var(--color-priduck-c0b9); + color: var(--color-priduck-c0b0); } diff --git a/examples/light.css b/examples/light.css index c5ea387..0e37588 100644 --- a/examples/light.css +++ b/examples/light.css @@ -1,3 +1,4 @@ :theme { - @mixin theme 9, 1; + background-color: var(--color-priduck-c0b9); + color: var(--color-priduck-c0b1); } diff --git a/examples/style.css b/examples/style.css index 4047bc2..e93d00c 100644 --- a/examples/style.css +++ b/examples/style.css @@ -1,2 +1,2 @@ @import url("../colors.css"); -@import url("../theme.css"); +@import url("./theme.css"); diff --git a/package.json b/package.json index dbd5f1f..0c4b28e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "priduck": "./src/cli.mjs" }, "dependencies": { - "@priduck-color-theme/cli": "^0.0.1", "colorjs.io": "^0.5.0", "debug": "^4.3.4", "mkdirp": "^3.0.1", diff --git a/src/cmds/css.mjs b/src/cmds/css.mjs index cb2bc4b..3ee168c 100644 --- a/src/cmds/css.mjs +++ b/src/cmds/css.mjs @@ -1,10 +1,11 @@ import * as variablesCmd from "./css/variables.mjs"; +import * as mixinCmd from "./css/mixin.mjs"; export const command = "css"; export const desc = "Generate CSS files"; export function builder(yargv) { - yargv.command(variablesCmd).demand(2); + yargv.command(variablesCmd).command(mixinCmd).demand(2); } export function handler(argv) {} diff --git a/src/cmds/css/mixin.mjs b/src/cmds/css/mixin.mjs new file mode 100755 index 0000000..b8a506c --- /dev/null +++ b/src/cmds/css/mixin.mjs @@ -0,0 +1,231 @@ +import { write } from "../../output.mjs"; +import debug from "debug"; +import * as fs from "fs"; + +/** @type {object} */ +const log = debug("css").extend("mixin"); + +export const command = "mixin"; +export const desc = + "Generate a CSS file that mixes in various prefers-* selectors"; + +export function builder(argv) { + argv + .option("o", { + alias: "output", + describe: "The output path for colors.css", + type: "string", + }) + .option("s", { + alias: "selector", + describe: "The CSS selector to wrap the colors", + demandOption: true, + type: "string", + }) + .option("default", { + type: "string", + choices: ["light", "dark"], + default: "light", + describe: "The default theme if none are provided", + }) + .option("light", { + type: "string", + describe: "The path to CSS that provides the base light theme", + }) + .option("light-more-contrast", { + type: "string", + describe: "The path to CSS that provides more contrast for light theme", + }) + .option("light-less-contrast", { + type: "string", + describe: "The path to CSS that provides less contrast for light theme", + }) + .option("dark", { + type: "string", + describe: "The path to CSS that provides the base dark theme", + }) + .option("dark-more-contrast", { + type: "string", + describe: "The path to CSS that provides more contrast for dark theme", + }) + .option("dark-less-contrast", { + type: "string", + describe: "The path to CSS that provides less contrast for dark theme", + }); +} + +export async function handler(argv) { + // We like some noise. + log("generating CSS mixin file"); + + // Load in all the files. + const light = load(argv, "light"); + const lightMoreContrast = load(argv, "light-more-contrast"); + const lightLessContrast = load(argv, "light-less-contrast"); + const dark = load(argv, "dark"); + const darkMoreContrast = load(argv, "dark-more-contrast"); + const darkLessContrast = load(argv, "dark-less-contrast"); + + // Figure out the default. + const auto = argv.default === "light" ? light : dark; + + const autoMoreContrast = + argv.default === "light" ? lightMoreContrast : darkMoreContrast; + + const autoLessContrast = + argv.default === "light" ? lightLessContrast : darkLessContrast; + + // Build up the components of the style. + let lines = [ + block(argv, argv.selector, auto), + block(argv, '[data-prefers-contrast="more"]', autoMoreContrast), + block(argv, '[data-prefers-contrast="less"]', autoLessContrast), + media( + argv, + "@media (prefers-color-scheme: light)", + block(argv, argv.selector, light), + block(argv, '[data-prefers-contrast="more"]', lightMoreContrast), + block(argv, '[data-prefers-contrast="less"]', lightLessContrast), + ), + media( + argv, + "@media (prefers-color-scheme: dark)", + block(argv, argv.selector, dark), + block(argv, '[data-prefers-contrast="more"]', darkMoreContrast), + block(argv, '[data-prefers-contrast="less"]', darkLessContrast), + ), + block(argv, '[data-prefers-color-scheme="light"]', light), + block( + argv, + '[data-prefers-color-scheme="light"][data-prefers-contrast="more"]', + lightMoreContrast, + ), + block( + argv, + '[data-prefers-color-scheme="light"][data-prefers-contrast="less"]', + lightLessContrast, + ), + block(argv, '[data-prefers-color-scheme="dark"]', dark), + block( + argv, + '[data-prefers-color-scheme="dark"][data-prefers-contrast="more"]', + darkMoreContrast, + ), + block( + argv, + '[data-prefers-color-scheme="dark"][data-prefers-contrast="less"]', + darkLessContrast, + ), + ].flat(); + + // Write out the results to the console or a file. + await write(argv, lines); +} + +/** + * Generates a number of CSS blocks. + * + * @param argv The arguments. + * @param {string} selector The CSS selector to put in. + * @param {string[]} lines The text to put inside the block. + */ +function media(argv, selector, ...lines) { + // If we have a blank line, then we don't want to write the section. + lines = lines.flat(); + + if (!lines || !lines[0]) { + return ""; + } + + // We include the selector, which is either a @media query or the user's + // selector. If we have a @media selector, we want to add the user's selector + // as the inner block. + let blockLines = [`${selector} {`]; + + // Insert all the lines while indenting them. + for (const line of lines) { + blockLines.push(` ${line}`); + } + + // Finish up the blocks. + blockLines.push(`}`); + + return blockLines; +} + +/** + * Generates a number of CSS blocks. + * + * @param argv The arguments. + * @param {string} selector The CSS selector to put in. + * @param {string[]} lines The text to put inside the block. + */ +function block(argv, selector, ...lines) { + // If we have a blank line, then we don't want to write the section. + lines = lines.flat(); + + if (!lines || !lines[0]) { + return ""; + } + + // We include the selector, which is either a @media query or the user's + // selector. If we have a @media selector, we want to add the user's selector + // as the inner block. + let blockLines = [`${selector} {`]; + + // Insert the lines with a little indent. + for (const line of lines) { + blockLines.push(` ${line}`); + } + + // Finish up the blocks. + blockLines.push(`}`); + + return blockLines; +} + +/** + * Loads a CSS file and removes the pseudo-selector, :theme. + * + * @param {object} argv + * @param {string} key + * @returns {string | undefined} + */ +function load(argv, key) { + // See if we have a file to load. + const value = argv[key]; + + if (!value || value.trim() === "") { + log(`no value provided for --${key}`); + + return undefined; + } + + // Load the file into memory so we can format it. + log(`reading --${key}=${argv[key]}`); + + let text = fs.readFileSync(argv[key]).toString().replace(/\r\n/g, "\n"); + const hasThemeSelector = text.match(/^\s*:theme/); + + // If we have a theme selector, we need to get rid of it and the trailing + // brace but leave any whitespace in front of the line so we can outdent + // each of the characters. + if (hasThemeSelector) { + text = text.replace(/^\s*:theme\s*{\s*\n/, "").replace(/\s*}\s*$/, ""); + } + + // If we only have one line, then trim it and return it. + const lines = text.split("\n"); + + if (lines.length <= 1) { + return [lines[0].trim()]; + } + + // Otherwise, remove the whitespace in front of each of the lines so the + // resulting CSS is "prettier". + const prefix = lines[0].replace(/^(\s*).*$/, "$1"); + const replace = new RegExp(`^${prefix}`); + const normalized = lines.map((line) => line.replace(replace, "")); + + return normalized; +} diff --git a/src/cmds/css/variables.mjs b/src/cmds/css/variables.mjs index 6b7f55e..9877012 100644 --- a/src/cmds/css/variables.mjs +++ b/src/cmds/css/variables.mjs @@ -5,7 +5,7 @@ import debug from "debug"; /** @type {object} */ const log = debug("css").extend("variables"); -export const command = "variables [path.css]"; +export const command = "variables"; export const desc = "Generate a CSS file that uses variables to generate"; export function builder(argv) {