eva-colors 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +219 -0
  2. package/cli.js +169 -0
  3. package/package.json +45 -0
  4. package/src/index.js +149 -0
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # @eva/colors
2
+
3
+ > OKLCH color utilities for EVA CSS framework
4
+
5
+ Powerful color conversion and manipulation tools focused on the perceptually uniform OKLCH color space.
6
+
7
+ ## 🎯 Features
8
+
9
+ - **Hex ↔ OKLCH Conversion**: Seamless conversion between color formats
10
+ - **Palette Generation**: Create harmonious color palettes from a base color
11
+ - **Theme Generator**: Generate EVA CSS themes from color configs
12
+ - **Accessibility Checks**: WCAG contrast validation
13
+ - **CLI & Programmatic API**: Use via command line or in your code
14
+
15
+ ## 📦 Installation
16
+
17
+ ```bash
18
+ npm install @eva/colors
19
+ # or
20
+ pnpm add @eva/colors
21
+ # or
22
+ yarn add @eva/colors
23
+ ```
24
+
25
+ ## 🚀 Usage
26
+
27
+ ### CLI Usage
28
+
29
+ ```bash
30
+ # Convert hex to OKLCH
31
+ eva-color convert #ff0000
32
+
33
+ # Convert OKLCH to hex
34
+ eva-color to-hex 62.8 0.258 29.23
35
+
36
+ # Generate a 7-step palette
37
+ eva-color palette #ff0000 7
38
+
39
+ # Generate theme CSS
40
+ eva-color theme my-theme.json
41
+
42
+ # Check color contrast
43
+ eva-color contrast #ffffff #000000
44
+ ```
45
+
46
+ ### Programmatic Usage
47
+
48
+ ```javascript
49
+ import {
50
+ hexToOklch,
51
+ oklchToHex,
52
+ generatePalette,
53
+ generateTheme,
54
+ checkAccessibility
55
+ } from '@eva/colors';
56
+
57
+ // Convert hex to OKLCH
58
+ const oklch = hexToOklch('#ff0000');
59
+ console.log(oklch);
60
+ // {
61
+ // l: 62.8,
62
+ // c: 0.258,
63
+ // h: 29.23,
64
+ // css: 'oklch(62.8% 0.258 29.23)',
65
+ // scss: {
66
+ // lightness: '62.8%',
67
+ // chroma: '0.258',
68
+ // hue: '29.23'
69
+ // }
70
+ // }
71
+
72
+ // Convert OKLCH to hex
73
+ const hex = oklchToHex({ l: 62.8, c: 0.258, h: 29.23 });
74
+ console.log(hex); // '#ff0000'
75
+
76
+ // Generate palette
77
+ const palette = generatePalette('#ff0000', 5);
78
+ console.log(palette);
79
+ // [
80
+ // { hex: '#...', oklch: {...}, name: 'step-1' },
81
+ // { hex: '#...', oklch: {...}, name: 'step-2' },
82
+ // ...
83
+ // ]
84
+
85
+ // Generate theme
86
+ const theme = generateTheme({
87
+ name: 'my-theme',
88
+ brand: '#ff0000',
89
+ accent: '#7300ff',
90
+ extra: '#ffe500',
91
+ light: '#f3f3f3',
92
+ dark: '#252525'
93
+ });
94
+ console.log(theme);
95
+ // .theme-my-theme {
96
+ // --brand-lightness: 62.8%;
97
+ // --brand-chroma: 0.258;
98
+ // --brand-hue: 29.23;
99
+ // ...
100
+ // }
101
+
102
+ // Check accessibility
103
+ const result = checkAccessibility('#ffffff', '#000000', 'AA');
104
+ console.log(result);
105
+ // {
106
+ // pass: true,
107
+ // contrast: '0.821',
108
+ // level: 'AA'
109
+ // }
110
+ ```
111
+
112
+ ## 📚 API Reference
113
+
114
+ ### `hexToOklch(hex)`
115
+
116
+ Convert hex color to OKLCH format.
117
+
118
+ **Parameters:**
119
+ - `hex` (string): Hex color code (e.g., "#ff0000")
120
+
121
+ **Returns:** Object with OKLCH values and CSS/SCSS formats, or `null` if invalid
122
+
123
+ ### `oklchToHex({ l, c, h })`
124
+
125
+ Convert OKLCH color to hex format.
126
+
127
+ **Parameters:**
128
+ - `l` (number): Lightness (0-100)
129
+ - `c` (number): Chroma (0-0.4)
130
+ - `h` (number): Hue (0-360)
131
+
132
+ **Returns:** Hex color string, or `null` if invalid
133
+
134
+ ### `generatePalette(baseColor, steps = 5)`
135
+
136
+ Generate a color palette from a base color.
137
+
138
+ **Parameters:**
139
+ - `baseColor` (string): Base hex color
140
+ - `steps` (number): Number of palette steps (default: 5)
141
+
142
+ **Returns:** Array of color objects
143
+
144
+ ### `generateTheme(config)`
145
+
146
+ Generate EVA CSS theme variables from config.
147
+
148
+ **Parameters:**
149
+ - `config` (object): Theme configuration
150
+ - `name` (string): Theme name
151
+ - `brand` (string): Brand color hex
152
+ - `accent` (string): Accent color hex
153
+ - `extra` (string): Extra color hex
154
+ - `light` (string): Light color hex
155
+ - `dark` (string): Dark color hex
156
+
157
+ **Returns:** CSS string with theme variables
158
+
159
+ ### `checkAccessibility(foreground, background, level = 'AA')`
160
+
161
+ Check WCAG contrast requirements.
162
+
163
+ **Parameters:**
164
+ - `foreground` (string): Foreground hex color
165
+ - `background` (string): Background hex color
166
+ - `level` (string): 'AA' or 'AAA' (default: 'AA')
167
+
168
+ **Returns:** Object with pass/fail result and contrast value
169
+
170
+ ## 🎨 Example: Figma to EVA Workflow
171
+
172
+ ```bash
173
+ # 1. Extract colors from Figma
174
+ # (via Figma MCP or manually)
175
+
176
+ # 2. Convert to OKLCH
177
+ eva-color convert #ff5733
178
+
179
+ # 3. Create theme config (theme.json)
180
+ {
181
+ "name": "my-project",
182
+ "brand": "#ff5733",
183
+ "accent": "#33ff57",
184
+ "extra": "#3357ff",
185
+ "light": "#f5f5f5",
186
+ "dark": "#1a1a1a"
187
+ }
188
+
189
+ # 4. Generate theme CSS
190
+ eva-color theme theme.json > my-theme.scss
191
+
192
+ # 5. Use in your EVA CSS project
193
+ ```
194
+
195
+ ## 🌈 Why OKLCH?
196
+
197
+ OKLCH is a perceptually uniform color space that provides:
198
+
199
+ - **Better color interpolation**: Smooth gradients without muddy midtones
200
+ - **Predictable lightness**: L values directly correlate to perceived brightness
201
+ - **Wide gamut support**: Access to vibrant colors beyond sRGB
202
+ - **Consistent chroma**: Colors with same C value have similar saturation
203
+
204
+ ## 📄 License
205
+
206
+ MIT © [Michaël Tati](https://ulysse-2029.com/)
207
+
208
+ ## 👨‍💻 Author
209
+
210
+ **Michaël Tati**
211
+ - Portfolio: [ulysse-2029.com](https://ulysse-2029.com/)
212
+ - LinkedIn: [linkedin.com/in/mtati](https://www.linkedin.com/in/mtati/)
213
+ - Website: [eva-css.xyz](https://eva-css.xyz/)
214
+
215
+ ## 🔗 Related Packages
216
+
217
+ - [@eva/css](https://www.npmjs.com/package/@eva/css) - Fluid design framework
218
+ - [@eva/purge](https://www.npmjs.com/package/@eva/purge) - CSS optimization tool
219
+ - [@eva/mcp-server](https://www.npmjs.com/package/@eva/mcp-server) - Figma to HTML MCP server
package/cli.js ADDED
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ===========================================
4
+ // EVA Colors CLI
5
+ // ===========================================
6
+
7
+ import { hexToOklch, oklchToHex, generatePalette, generateTheme, checkAccessibility } from './src/index.js';
8
+
9
+ const args = process.argv.slice(2);
10
+ const command = args[0];
11
+
12
+ function printHelp() {
13
+ console.log(`
14
+ EVA Colors CLI - OKLCH Color Utilities
15
+
16
+ Usage:
17
+ eva-color <command> [options]
18
+
19
+ Commands:
20
+ convert <hex> Convert hex to OKLCH
21
+ to-hex <l> <c> <h> Convert OKLCH to hex
22
+ palette <hex> [steps] Generate color palette (default 5 steps)
23
+ theme <config.json> Generate theme from JSON config
24
+ contrast <hex1> <hex2> Check color contrast
25
+ help Show this help message
26
+
27
+ Examples:
28
+ eva-color convert #ff0000
29
+ eva-color to-hex 62.8 0.258 29.23
30
+ eva-color palette #ff0000 7
31
+ eva-color contrast #ffffff #000000
32
+
33
+ Theme config example (theme.json):
34
+ {
35
+ "name": "my-theme",
36
+ "brand": "#ff0000",
37
+ "accent": "#7300ff",
38
+ "extra": "#ffe500",
39
+ "light": "#f3f3f3",
40
+ "dark": "#252525"
41
+ }
42
+ `);
43
+ }
44
+
45
+ // Command handlers
46
+ switch (command) {
47
+ case 'convert': {
48
+ const hex = args[1];
49
+ if (!hex) {
50
+ console.error('❌ Error: Hex color required');
51
+ console.log('Usage: eva-color convert #ff0000');
52
+ process.exit(1);
53
+ }
54
+
55
+ const result = hexToOklch(hex);
56
+ if (!result) {
57
+ console.error(`❌ Error: Invalid hex color "${hex}"`);
58
+ process.exit(1);
59
+ }
60
+
61
+ console.log(`\n🎨 Conversion: ${hex} → OKLCH\n`);
62
+ console.log(`CSS: ${result.css}`);
63
+ console.log(`\nSCSS Variables:`);
64
+ console.log(` --lightness: ${result.scss.lightness};`);
65
+ console.log(` --chroma: ${result.scss.chroma};`);
66
+ console.log(` --hue: ${result.scss.hue};`);
67
+ console.log();
68
+ break;
69
+ }
70
+
71
+ case 'to-hex': {
72
+ const l = parseFloat(args[1]);
73
+ const c = parseFloat(args[2]);
74
+ const h = parseFloat(args[3]);
75
+
76
+ if (isNaN(l) || isNaN(c) || isNaN(h)) {
77
+ console.error('❌ Error: Invalid OKLCH values');
78
+ console.log('Usage: eva-color to-hex 62.8 0.258 29.23');
79
+ process.exit(1);
80
+ }
81
+
82
+ const hex = oklchToHex({ l, c, h });
83
+ if (!hex) {
84
+ console.error('❌ Error: Conversion failed');
85
+ process.exit(1);
86
+ }
87
+
88
+ console.log(`\n🎨 Conversion: OKLCH → ${hex}\n`);
89
+ break;
90
+ }
91
+
92
+ case 'palette': {
93
+ const hex = args[1];
94
+ const steps = parseInt(args[2]) || 5;
95
+
96
+ if (!hex) {
97
+ console.error('❌ Error: Base hex color required');
98
+ console.log('Usage: eva-color palette #ff0000 [steps]');
99
+ process.exit(1);
100
+ }
101
+
102
+ const palette = generatePalette(hex, steps);
103
+ if (palette.length === 0) {
104
+ console.error(`❌ Error: Invalid hex color "${hex}"`);
105
+ process.exit(1);
106
+ }
107
+
108
+ console.log(`\n🎨 Generated ${steps}-step palette from ${hex}\n`);
109
+ palette.forEach((color, i) => {
110
+ console.log(`${i + 1}. ${color.hex} → ${color.oklch.css}`);
111
+ });
112
+ console.log();
113
+ break;
114
+ }
115
+
116
+ case 'theme': {
117
+ const configPath = args[1];
118
+ if (!configPath) {
119
+ console.error('❌ Error: Config file required');
120
+ console.log('Usage: eva-color theme config.json');
121
+ process.exit(1);
122
+ }
123
+
124
+ try {
125
+ const fs = await import('fs');
126
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
127
+ const css = generateTheme(config);
128
+
129
+ console.log(`\n🎨 Generated theme: ${config.name}\n`);
130
+ console.log(css);
131
+ console.log();
132
+ } catch (error) {
133
+ console.error(`❌ Error: ${error.message}`);
134
+ process.exit(1);
135
+ }
136
+ break;
137
+ }
138
+
139
+ case 'contrast': {
140
+ const hex1 = args[1];
141
+ const hex2 = args[2];
142
+
143
+ if (!hex1 || !hex2) {
144
+ console.error('❌ Error: Two hex colors required');
145
+ console.log('Usage: eva-color contrast #ffffff #000000');
146
+ process.exit(1);
147
+ }
148
+
149
+ const resultAA = checkAccessibility(hex1, hex2, 'AA');
150
+ const resultAAA = checkAccessibility(hex1, hex2, 'AAA');
151
+
152
+ console.log(`\n🎨 Contrast Check: ${hex1} vs ${hex2}\n`);
153
+ console.log(`Contrast Value: ${resultAA.contrast}`);
154
+ console.log(`WCAG AA: ${resultAA.pass ? '✅ Pass' : '❌ Fail'}`);
155
+ console.log(`WCAG AAA: ${resultAAA.pass ? '✅ Pass' : '❌ Fail'}`);
156
+ console.log();
157
+ break;
158
+ }
159
+
160
+ case 'help':
161
+ case undefined:
162
+ printHelp();
163
+ break;
164
+
165
+ default:
166
+ console.error(`❌ Error: Unknown command "${command}"`);
167
+ printHelp();
168
+ process.exit(1);
169
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "eva-colors",
3
+ "version": "1.0.0",
4
+ "description": "OKLCH color utilities for EVA CSS framework",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "bin": {
11
+ "eva-color": "./cli.js"
12
+ },
13
+ "files": [
14
+ "src/",
15
+ "cli.js",
16
+ "README.md"
17
+ ],
18
+ "keywords": [
19
+ "color",
20
+ "oklch",
21
+ "css",
22
+ "eva",
23
+ "color-conversion",
24
+ "hex-to-oklch",
25
+ "palette-generator"
26
+ ],
27
+ "scripts": {
28
+ "test": "node --test",
29
+ "dev": "node --watch cli.js"
30
+ },
31
+ "dependencies": {
32
+ "culori": "^4.0.2"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/nkdeus/eva.git",
37
+ "directory": "packages/eva-colors"
38
+ },
39
+ "author": {
40
+ "name": "Michaël Tati",
41
+ "url": "https://ulysse-2029.com/"
42
+ },
43
+ "homepage": "https://eva-css.xyz/",
44
+ "license": "MIT"
45
+ }
package/src/index.js ADDED
@@ -0,0 +1,149 @@
1
+ // ===========================================
2
+ // EVA Colors - OKLCH Color Utilities
3
+ // ===========================================
4
+
5
+ import { parse, oklch, formatHex, differenceEuclidean } from 'culori';
6
+
7
+ /**
8
+ * Convert hex color to OKLCH format
9
+ * @param {string} hex - Hex color code (e.g., "#ff0000")
10
+ * @returns {Object|null} OKLCH color object with CSS string
11
+ */
12
+ export function hexToOklch(hex) {
13
+ const color = parse(hex);
14
+ if (!color) return null;
15
+
16
+ const oklchColor = oklch(color);
17
+ if (!oklchColor) return null;
18
+
19
+ const { l, c, h } = oklchColor;
20
+
21
+ return {
22
+ l: Math.round(l * 1000) / 10, // Percentage with 1 decimal
23
+ c: Math.round(c * 1000) / 1000, // 3 decimals
24
+ h: h !== undefined ? Math.round(h * 100) / 100 : 0, // 2 decimals
25
+ css: `oklch(${(l * 100).toFixed(1)}% ${c.toFixed(3)} ${h !== undefined ? h.toFixed(2) : '0'})`,
26
+ scss: {
27
+ lightness: `${(l * 100).toFixed(1)}%`,
28
+ chroma: c.toFixed(3),
29
+ hue: (h !== undefined ? h.toFixed(2) : '0')
30
+ }
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Convert OKLCH to hex color
36
+ * @param {Object} oklchColor - {l: 0-100, c: 0-0.4, h: 0-360}
37
+ * @returns {string|null} Hex color code
38
+ */
39
+ export function oklchToHex({ l, c, h }) {
40
+ try {
41
+ return formatHex({ mode: 'oklch', l: l / 100, c, h });
42
+ } catch (error) {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Generate a palette of colors based on a base color
49
+ * @param {string} baseColor - Base hex color
50
+ * @param {number} steps - Number of palette steps (default: 5)
51
+ * @returns {Array} Array of color objects
52
+ */
53
+ export function generatePalette(baseColor, steps = 5) {
54
+ const base = hexToOklch(baseColor);
55
+ if (!base) return [];
56
+
57
+ const palette = [];
58
+ const lightnessRange = 90 - 10; // From 10% to 90%
59
+ const stepSize = lightnessRange / (steps - 1);
60
+
61
+ for (let i = 0; i < steps; i++) {
62
+ const lightness = 10 + (i * stepSize);
63
+ const color = oklchToHex({
64
+ l: lightness,
65
+ c: base.c * (lightness / base.l), // Adjust chroma based on lightness
66
+ h: base.h
67
+ });
68
+
69
+ palette.push({
70
+ hex: color,
71
+ oklch: hexToOklch(color),
72
+ name: `step-${i + 1}`
73
+ });
74
+ }
75
+
76
+ return palette;
77
+ }
78
+
79
+ /**
80
+ * Generate theme CSS variables from config
81
+ * @param {Object} config - Theme configuration
82
+ * @returns {string} CSS variables
83
+ */
84
+ export function generateTheme(config) {
85
+ const {
86
+ name = 'custom',
87
+ brand,
88
+ accent,
89
+ extra,
90
+ light,
91
+ dark
92
+ } = config;
93
+
94
+ const colors = { brand, accent, extra, light, dark };
95
+ let css = `.theme-${name} {\n`;
96
+
97
+ for (const [colorName, hexColor] of Object.entries(colors)) {
98
+ if (!hexColor) continue;
99
+
100
+ const oklchColor = hexToOklch(hexColor);
101
+ if (!oklchColor) continue;
102
+
103
+ css += ` --${colorName}-lightness: ${oklchColor.scss.lightness};\n`;
104
+ css += ` --${colorName}-chroma: ${oklchColor.scss.chroma};\n`;
105
+ css += ` --${colorName}-hue: ${oklchColor.scss.hue};\n`;
106
+ css += `\n`;
107
+ }
108
+
109
+ css += `}`;
110
+ return css;
111
+ }
112
+
113
+ /**
114
+ * Calculate color contrast ratio
115
+ * @param {string} hex1 - First hex color
116
+ * @param {string} hex2 - Second hex color
117
+ * @returns {number|null} Contrast ratio
118
+ */
119
+ export function getContrast(hex1, hex2) {
120
+ const color1 = parse(hex1);
121
+ const color2 = parse(hex2);
122
+
123
+ if (!color1 || !color2) return null;
124
+
125
+ // Use Euclidean distance in OKLCH space as a simple contrast metric
126
+ return differenceEuclidean()(oklch(color1), oklch(color2));
127
+ }
128
+
129
+ /**
130
+ * Check if color meets WCAG contrast requirements
131
+ * @param {string} foreground - Foreground hex color
132
+ * @param {string} background - Background hex color
133
+ * @param {string} level - 'AA' or 'AAA' (default: 'AA')
134
+ * @returns {Object} Accessibility check result
135
+ */
136
+ export function checkAccessibility(foreground, background, level = 'AA') {
137
+ const contrast = getContrast(foreground, background);
138
+ if (contrast === null) return { pass: false, contrast: null };
139
+
140
+ // Simplified check using Euclidean distance
141
+ // Threshold values are approximations
142
+ const threshold = level === 'AAA' ? 0.15 : 0.10;
143
+
144
+ return {
145
+ pass: contrast >= threshold,
146
+ contrast: contrast.toFixed(3),
147
+ level
148
+ };
149
+ }