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.
- package/README.md +219 -0
- package/cli.js +169 -0
- package/package.json +45 -0
- 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
|
+
}
|