feat: implemented the "css mixin" command
This commit is contained in:
parent
0b3414bf86
commit
bbcd649577
13 changed files with 283 additions and 13 deletions
5
Justfile
5
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 \
|
||||
|
|
34
README.md
34
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).
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
:theme {
|
||||
@mixin theme 0, 7;
|
||||
background-color: var(--color-priduck-c0b0);
|
||||
color: var(--color-priduck-c0b7);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
:theme {
|
||||
@mixin theme 0, 9;
|
||||
background-color: var(--color-priduck-c0b0);
|
||||
color: var(--color-priduck-c0b9);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
:theme {
|
||||
@mixin theme 0, 8;
|
||||
background-color: var(--color-priduck-c0b0);
|
||||
color: var(--color-priduck-c0b8);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
:theme {
|
||||
@mixin theme 9, 2;
|
||||
background-color: var(--color-priduck-c0b9);
|
||||
color: var(--color-priduck-c0b2);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
:theme {
|
||||
@mixin theme 9, 0;
|
||||
background-color: var(--color-priduck-c0b9);
|
||||
color: var(--color-priduck-c0b0);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
:theme {
|
||||
@mixin theme 9, 1;
|
||||
background-color: var(--color-priduck-c0b9);
|
||||
color: var(--color-priduck-c0b1);
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
@import url("../colors.css");
|
||||
@import url("../theme.css");
|
||||
@import url("./theme.css");
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
231
src/cmds/css/mixin.mjs
Executable 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;
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue