Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main]
branches: [main, dev]
pull_request:
branches: [main]
branches: [main, dev]

jobs:
test:
Expand Down
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [bdsg 0.2.0] - 2026-01-14

### Added

- **OKLCH Color Space**: New perceptually uniform color space support
- `hexToOklch()` - Convert HEX to OKLCH (L: 0-1, C: 0+, H: 0-360)
- `oklchToHex()` - Convert OKLCH back to HEX
- `interpolateOklch()` - Smooth color interpolation without "muddy middle"
- **Gradient Generation**: New `gradients.ts` module with expandable architecture
- `generateGradient()` - Two-color gradient with OKLCH interpolation
- `generateMultiStopGradient()` - Multi-stop gradients with position control
- `toCssGradient()` - Generate CSS linear/radial/conic gradient strings
- `EASING` presets - `linear`, `easeIn`, `easeOut`, `easeInOut`
- Hue direction control: `shorter`, `longer`, `increasing`, `decreasing`
- **OKLCH Types**: New `oklch.types.ts` with `OKLCH` interface

### Changed

- **Palette Generation**: Refactored `generatePalette()` to use OKLCH instead of HSL for perceptually uniform shade generation
- **Centralized Validation**: Created `schemas.ts` with shared Zod schemas
- `HexColorSchema`, `OklchSchema`, `RgbSchema`, `HslSchema`
- `GradientStopSchema`, `GradientConfigSchema`, `StepsSchema`
- `validateOrThrow()` helper function for cleaner validation code
- **Code Quality**: All modules now use centralized validation schemas

### Tests

- Added 14 new tests for OKLCH conversions (`oklch.test.ts`)
- Added 22 new tests for gradients (`gradients.test.ts`)
- Total: 284 tests across 11 files

[bdsg 0.2.0]: https://github.com/CarlosEduJs/bdsg/releases/tag/bdsg@v0.2.0

## [bdsg-cli 0.2.0] - 2026-01-14

### Added

- **Gradient Command**: New `bdsg generate gradient` command
- Generate color gradients with OKLCH interpolation
- Easing functions: `linear`, `easeIn`, `easeOut`, `easeInOut`
- Hue direction control: `shorter`, `longer`, `increasing`, `decreasing`
- Output CSS variables and gradient strings
- JSON export with gradient metadata

### Changed

- **Dependencies**: Updated `bdsg` dependency to `^0.2.0`

[bdsg-cli 0.2.0]: https://github.com/CarlosEduJs/bdsg/releases/tag/bdsg-cli@v0.2.0

## [bdsg-cli 0.1.2] - 2026-01-12

### Fixed
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Design System Generation library for programmatic token creation with WCAG acces

bdsg provides algorithms for generating design tokens:

- OKLCH color space for perceptually uniform palettes `v0.2.0`
- Gradient generation with easing and hue control `v0.2.0`
- Color palettes with automatic text color calculation
- Typography scales using musical ratios
- Spacing scales (Fibonacci, linear, exponential)
Expand Down Expand Up @@ -91,6 +93,8 @@ const shadows = generateShadows({
| Module | Functions |
|--------|-----------|
| color-utils | `hexToRgb`, `rgbToHex`, `hexToHsl`, `hslToHex`, `rgbToHsl`, `hslToRgb` |
| oklch | `hexToOklch`, `oklchToHex`, `interpolateOklch` |
| gradients | `generateGradient`, `generateMultiStopGradient`, `toCssGradient`, `EASING` |
| contrast | `calculateContrast`, `getRelativeLuminance`, `meetsWCAG`, `getWCAGCompliance` |
| adjust | `adjustColorForContrast`, `generateAccessibleVariations` |
| palette | `generatePalette`, `generatePaletteTokens` |
Expand Down
46 changes: 46 additions & 0 deletions packages/bdsg-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ bdsg generate palette "#3B82F6" -n primary
bdsg generate typography -r golden-ratio
bdsg generate spacing -m fibonacci
bdsg generate shadows -s material
bdsg generate gradient "#FF0000" "#0000FF" -s 5

# Validate WCAG contrast
bdsg validate "#3B82F6" "#FFFFFF"
Expand Down Expand Up @@ -159,6 +160,51 @@ Options:

Available styles: `material`, `soft`, `hard`

#### generate gradient

*New in v0.2.0*

```bash
bdsg generate gradient <startColor> <endColor> [options]

Arguments:
startColor Start color in hex format (e.g., #FF0000)
endColor End color in hex format (e.g., #0000FF)

Options:
-n, --name <name> Gradient name (default: "gradient")
-s, --steps <steps> Number of color steps (default: "5")
-e, --easing <easing> Easing function (default: "linear")
-d, --direction <dir> Hue direction (default: "shorter")
-o, --output <dir> Output directory (default: "./tokens")
-f, --format <fmt> Output format: css, json (default: "css")
```

Available easing: `linear`, `easeIn`, `easeOut`, `easeInOut`

Available hue directions: `shorter`, `longer`, `increasing`, `decreasing`

**Example:**

```
$ bdsg generate gradient "#FF0000" "#0000FF" -s 5 -e easeInOut

✔ Gradient generated!

File: ./tokens/gradient.css

Gradient colors:
1: #ff0000
2: #e8007b
3: #ba00c2
4: #7a00f4
5: #0000ff

CSS Gradients:
linear: linear-gradient(90deg, #ff0000, #e8007b, #ba00c2, #7a00f4, #0000ff)
radial: radial-gradient(circle, #ff0000, #e8007b, #ba00c2, #7a00f4, #0000ff)
```

### validate

Validate WCAG contrast between two colors with suggestions for accessibility compliance.
Expand Down
4 changes: 2 additions & 2 deletions packages/bdsg-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bdsg-cli",
"version": "0.1.2",
"version": "0.2.0",
"description": "CLI for design system generation",
"type": "module",
"bin": {
Expand All @@ -11,7 +11,7 @@
"dev": "tsc --watch"
},
"dependencies": {
"bdsg": "^0.1.3",
"bdsg": "^0.2.0",
"commander": "^12.0.0",
"inquirer": "^9.2.0",
"chalk": "^5.3.0",
Expand Down
117 changes: 117 additions & 0 deletions packages/bdsg-cli/src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import {
EASING,
generateGradient,
generatePalette,
generateShadows,
generateSpacingScale,
generateTypographyScale,
toCssGradient,
} from "bdsg";
import chalk from "chalk";
import { Command } from "commander";
Expand Down Expand Up @@ -233,4 +236,118 @@ export const generateCommand = new Command("generate")
process.exit(1);
}
}),
)
.addCommand(
new Command("gradient")
.description("Generate a color gradient")
.argument("<startColor>", "Start color in hex format (e.g., #FF0000)")
.argument("<endColor>", "End color in hex format (e.g., #0000FF)")
.option("-n, --name <name>", "Gradient name", "gradient")
.option("-s, --steps <steps>", "Number of color steps", "5")
.option(
"-e, --easing <easing>",
"Easing function (linear, easeIn, easeOut, easeInOut)",
"linear",
)
.option(
"-d, --direction <direction>",
"Hue direction (shorter, longer, increasing, decreasing)",
"shorter",
)
.option("-o, --output <dir>", "Output directory", "./tokens")
.option("-f, --format <format>", "Output format (css, json)", "css")
.action(async (startColor, endColor, options) => {
const spinner = ora("Generating gradient...").start();

try {
const steps = Number.parseInt(options.steps, 10);
if (Number.isNaN(steps)) {
spinner.fail(chalk.red("Invalid steps value"));
console.error(
chalk.yellow(` Steps must be a number, got: "${options.steps}"`),
);
process.exit(1);
}

const validEasings = ["linear", "easeIn", "easeOut", "easeInOut"];
if (!validEasings.includes(options.easing)) {
spinner.fail(chalk.red("Invalid easing function"));
console.error(
chalk.yellow(
` Valid options: ${validEasings.join(", ")}\n Got: "${options.easing}"`,
),
);
process.exit(1);
}
const easingFn = EASING[options.easing as keyof typeof EASING];

const validFormats = ["css", "json"];
if (!validFormats.includes(options.format)) {
spinner.fail(chalk.red("Invalid output format"));
console.error(
chalk.yellow(
` Valid options: ${validFormats.join(", ")}\n Got: "${options.format}"`,
),
);
process.exit(1);
}

const colors = generateGradient(startColor, endColor, steps, {
easing: easingFn,
hueDirection: options.direction,
});

await mkdir(options.output, { recursive: true });

if (options.format === "json") {
const gradient: Record<string, unknown> = {
name: options.name,
start: startColor,
end: endColor,
steps,
colors,
css: {
linear: toCssGradient("linear", colors, 90),
radial: toCssGradient("radial", colors),
},
};
await writeFile(
join(options.output, `${options.name}.json`),
JSON.stringify({ gradient }, null, 2),
);
} else {
let css = ":root {\n";
for (let i = 0; i < colors.length; i++) {
css += ` --${options.name}-${i + 1}: ${colors[i]};\n`;
}
css += ` --${options.name}-linear: ${toCssGradient("linear", colors, 90)};\n`;
css += ` --${options.name}-radial: ${toCssGradient("radial", colors)};\n`;
css += "}\n";
await writeFile(join(options.output, `${options.name}.css`), css);
}

spinner.succeed(chalk.green("Gradient generated!"));
console.log(
chalk.dim(
`\nFile: ${options.output}/${options.name}.${options.format}`,
),
);
console.log();

// Show preview
console.log(chalk.bold("Gradient colors:"));
for (let i = 0; i < colors.length; i++) {
console.log(` ${i + 1}: ${colors[i]}`);
}
console.log();
console.log(chalk.bold("CSS Gradients:"));
console.log(` linear: ${toCssGradient("linear", colors, 90)}`);
console.log(` radial: ${toCssGradient("radial", colors)}`);
console.log();
} catch (error) {
spinner.fail(chalk.red("Failed to generate gradient"));
console.error(error);
process.exit(1);
}
}),
);
85 changes: 85 additions & 0 deletions packages/bdsg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Design System Generation library. Algorithms for generating design tokens progra
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [Color Utilities](#color-utilities)
- [OKLCH Color Space](#oklch-color-space)
- [Gradients](#gradients)
- [Contrast](#contrast)
- [Color Adjustment](#color-adjustment)
- [Palette Generation](#palette-generation)
Expand Down Expand Up @@ -88,6 +90,89 @@ hexToHsl("#3B82F6"); // { h: 217, s: 91, l: 60 }
hslToHex({ h: 0, s: 100, l: 50 }); // "#ff0000"
```

### OKLCH Color Space - ^0.2.0

OKLCH is a perceptually uniform color space that provides better gradient interpolation and more intuitive color manipulation.

```typescript
import { hexToOklch, oklchToHex, interpolateOklch } from "bdsg";

// Convert between HEX and OKLCH
const oklch = hexToOklch("#3B82F6");
// { l: 0.623, c: 0.185, h: 259.5 }
// l: lightness (0-1), c: chroma (0+), h: hue (0-360)

oklchToHex({ l: 0.623, c: 0.185, h: 259.5 }); // "#3b82f6"

// Interpolate colors without "muddy middle"
const red = hexToOklch("#FF0000");
const green = hexToOklch("#00FF00");
const middle = interpolateOklch(red, green, 0.5);
// Produces vibrant yellow, not muddy brown like RGB interpolation
```

Why OKLCH?
- **Perceptually uniform**: Equal lightness values look equally bright across all hues
- **No muddy gradients**: Interpolation stays vibrant, avoiding the brown/gray zone
- **Intuitive**: Hue, chroma, and lightness are independent

### Gradients - ^0.2.0

Generate smooth color gradients using OKLCH interpolation with easing and hue direction control.

```typescript
import {
generateGradient,
generateMultiStopGradient,
toCssGradient,
EASING
} from "bdsg";

// Simple two-color gradient
const gradient = generateGradient("#FF0000", "#0000FF", 5);
// ["#ff0000", "#c5007c", "#9100c9", "#5c00ed", "#0000ff"]

// With easing function
const smooth = generateGradient("#000000", "#FFFFFF", 5, {
easing: EASING.easeInOut
});

// Control hue direction (shorter or longer path around color wheel)
const rainbow = generateGradient("#FF0000", "#FF8800", 5, {
hueDirection: "longer" // Takes the long way through blue/purple
});

// Multi-stop gradient
const sunset = generateMultiStopGradient([
{ color: "#FF0000", position: 0 },
{ color: "#FFFF00", position: 0.3 },
{ color: "#00FF00", position: 1 }
], 10);

// Generate CSS gradient string
const colors = generateGradient("#FF0000", "#0000FF", 3);
toCssGradient("linear", colors, 45);
// "linear-gradient(45deg, #ff0000, #800080, #0000ff)"

toCssGradient("radial", colors);
// "radial-gradient(circle, #ff0000, #800080, #0000ff)"

toCssGradient("conic", colors, 90);
// "conic-gradient(from 90deg, #ff0000, #800080, #0000ff)"
```

Available easing functions:
- `EASING.linear` — Constant speed
- `EASING.easeIn` — Slow start, accelerates
- `EASING.easeOut` — Fast start, decelerates
- `EASING.easeInOut` — Slow start and end

Hue direction options:
- `"shorter"` — Takes shortest path around color wheel (default)
- `"longer"` — Takes longer path for rainbow effects
- `"increasing"` — Always increases hue
- `"decreasing"` — Always decreases hue

### Contrast

Calculate and validate WCAG 2.1 contrast ratios.
Expand Down
Loading