feat: implemented the "css mixin" command

This commit is contained in:
D. Moonfire 2024-05-10 01:37:29 -05:00
parent 0b3414bf86
commit bbcd649577
13 changed files with 283 additions and 13 deletions

View file

@ -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 \

View file

@ -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).

View file

@ -1,3 +1,4 @@
:theme {
@mixin theme 0, 7;
background-color: var(--color-priduck-c0b0);
color: var(--color-priduck-c0b7);
}

View file

@ -1,3 +1,4 @@
:theme {
@mixin theme 0, 9;
background-color: var(--color-priduck-c0b0);
color: var(--color-priduck-c0b9);
}

View file

@ -1,3 +1,4 @@
:theme {
@mixin theme 0, 8;
background-color: var(--color-priduck-c0b0);
color: var(--color-priduck-c0b8);
}

View file

@ -1,3 +1,4 @@
:theme {
@mixin theme 9, 2;
background-color: var(--color-priduck-c0b9);
color: var(--color-priduck-c0b2);
}

View file

@ -1,3 +1,4 @@
:theme {
@mixin theme 9, 0;
background-color: var(--color-priduck-c0b9);
color: var(--color-priduck-c0b0);
}

View file

@ -1,3 +1,4 @@
:theme {
@mixin theme 9, 1;
background-color: var(--color-priduck-c0b9);
color: var(--color-priduck-c0b1);
}

View file

@ -1,2 +1,2 @@
@import url("../colors.css");
@import url("../theme.css");
@import url("./theme.css");

View file

@ -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",

View file

@ -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) {}

231
src/cmds/css/mixin.mjs Executable file
View file

@ -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;
}

View file

@ -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) {