oklchtohex 0.1.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 +79 -0
- package/package.json +33 -0
- package/src/cli.js +152 -0
- package/src/converter.js +243 -0
- package/src/tailwind-default-hex.js +300 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# oklchtohex
|
|
2
|
+
|
|
3
|
+
`oklchtohex` helps teams using Tailwind v4 convert `oklch(...)` colors into HEX.
|
|
4
|
+
|
|
5
|
+
This is useful when:
|
|
6
|
+
- clients compare implementation colors against Figma tokens shown in HEX
|
|
7
|
+
- QA wants quick visual checks from DevTools
|
|
8
|
+
- you want an optional HEX-only CSS artifact at build time
|
|
9
|
+
|
|
10
|
+
## Why this approach
|
|
11
|
+
|
|
12
|
+
A build-time package is the most practical fix:
|
|
13
|
+
- no Tailwind fork required
|
|
14
|
+
- works for any CSS output (including generated Tailwind files)
|
|
15
|
+
- can run in CI and produce a deterministic `*.hex.css` artifact
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm i -D oklchtohex
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## CLI usage
|
|
24
|
+
|
|
25
|
+
Convert one value:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx oklchtohex --value "oklch(70.4% 0.191 22.216)"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Convert a CSS file and write to a new file:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx oklchtohex ./dist/app.css -o ./dist/app.hex.css
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Print transformed CSS to stdout:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx oklchtohex ./dist/app.css --stdout
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Options
|
|
44
|
+
|
|
45
|
+
- `--gamut fit|clip`
|
|
46
|
+
`clip` (default) hard-clamps channels; `fit` reduces chroma to stay in sRGB.
|
|
47
|
+
- `--alpha auto|always|never`
|
|
48
|
+
defaults to `auto`.
|
|
49
|
+
- `--upper`
|
|
50
|
+
outputs uppercase HEX.
|
|
51
|
+
|
|
52
|
+
When converting Tailwind CSS, known default variables like `--color-red-500` are replaced with the exact Tailwind HEX palette value first. Unknown variables and raw `oklch(...)` values use functional conversion.
|
|
53
|
+
|
|
54
|
+
## Tailwind v4 integration (Inspect Element workflow)
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build:css": "tailwindcss -i ./src/input.css -o ./dist/tailwind.css",
|
|
60
|
+
"build:css:hex": "oklchtohex ./dist/tailwind.css -o ./dist/tailwind.css",
|
|
61
|
+
"build": "npm run build:css && npm run build:css:hex"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This rewrites Tailwind output in-place, so your existing HTML `<link>` remains unchanged and DevTools "Styles" pane shows HEX declarations.
|
|
67
|
+
|
|
68
|
+
If you want two files instead, output to `./dist/tailwind.hex.css` and link that file.
|
|
69
|
+
|
|
70
|
+
Note: some browsers still show `rgb(...)` in the "Computed" pane. The "Styles" pane reflects your source declaration format.
|
|
71
|
+
|
|
72
|
+
## JS API
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import { oklchToHex, replaceOklchInText } from "oklchtohex";
|
|
76
|
+
|
|
77
|
+
const hex = oklchToHex("oklch(70.4% 0.191 22.216)");
|
|
78
|
+
const css = replaceOklchInText("color: oklch(70.4% 0.191 22.216);");
|
|
79
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oklchtohex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Convert OKLCH colors to HEX and rewrite CSS at build time.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/converter.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/converter.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"oklchtohex": "./src/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"check": "node ./src/cli.js --help"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"oklch",
|
|
22
|
+
"hex",
|
|
23
|
+
"tailwind",
|
|
24
|
+
"tailwindcss",
|
|
25
|
+
"color",
|
|
26
|
+
"converter"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { oklchToHex, replaceOklchInText } from "./converter.js";
|
|
7
|
+
|
|
8
|
+
function printHelp() {
|
|
9
|
+
const help = `
|
|
10
|
+
oklchtohex
|
|
11
|
+
|
|
12
|
+
Convert OKLCH values to HEX, or rewrite an entire CSS file.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
oklchtohex --value "oklch(70.4% 0.191 22.216)"
|
|
16
|
+
oklchtohex ./dist/app.css -o ./dist/app.hex.css
|
|
17
|
+
oklchtohex ./dist/app.css --stdout
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--value <oklch> Convert a single OKLCH value to HEX
|
|
21
|
+
-o, --output <file> Output file path for transformed CSS
|
|
22
|
+
--stdout Print transformed CSS to stdout
|
|
23
|
+
--gamut <fit|clip> Gamut strategy (default: clip)
|
|
24
|
+
--alpha <auto|always|never>
|
|
25
|
+
Include alpha in HEX (default: auto)
|
|
26
|
+
--upper Uppercase HEX output
|
|
27
|
+
-h, --help Show this help text
|
|
28
|
+
`;
|
|
29
|
+
process.stdout.write(help);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const parsed = {
|
|
34
|
+
input: undefined,
|
|
35
|
+
output: undefined,
|
|
36
|
+
value: undefined,
|
|
37
|
+
stdout: false,
|
|
38
|
+
gamut: "clip",
|
|
39
|
+
includeAlpha: "auto",
|
|
40
|
+
uppercase: false,
|
|
41
|
+
help: false
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
45
|
+
const arg = argv[i];
|
|
46
|
+
|
|
47
|
+
if (arg === "-h" || arg === "--help") {
|
|
48
|
+
parsed.help = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (arg === "--value") {
|
|
52
|
+
parsed.value = argv[i + 1];
|
|
53
|
+
i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === "-o" || arg === "--output") {
|
|
57
|
+
parsed.output = argv[i + 1];
|
|
58
|
+
i += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (arg === "--stdout") {
|
|
62
|
+
parsed.stdout = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg === "--gamut") {
|
|
66
|
+
parsed.gamut = argv[i + 1];
|
|
67
|
+
i += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === "--alpha") {
|
|
71
|
+
parsed.includeAlpha = argv[i + 1];
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg === "--upper") {
|
|
76
|
+
parsed.uppercase = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!arg.startsWith("-") && !parsed.input) {
|
|
80
|
+
parsed.input = arg;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeIncludeAlpha(value) {
|
|
91
|
+
if (!value || value === "auto") {
|
|
92
|
+
return "auto";
|
|
93
|
+
}
|
|
94
|
+
if (value === "always") {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (value === "never") {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Invalid --alpha value: ${value}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeGamut(value) {
|
|
104
|
+
if (value === "fit" || value === "clip") {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`Invalid --gamut value: ${value}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function run() {
|
|
111
|
+
const args = parseArgs(process.argv.slice(2));
|
|
112
|
+
if (args.help) {
|
|
113
|
+
printHelp();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const options = {
|
|
118
|
+
gamut: normalizeGamut(args.gamut),
|
|
119
|
+
includeAlpha: normalizeIncludeAlpha(args.includeAlpha),
|
|
120
|
+
uppercase: args.uppercase
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (args.value) {
|
|
124
|
+
const hex = oklchToHex(args.value, options);
|
|
125
|
+
process.stdout.write(`${hex}\n`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!args.input) {
|
|
130
|
+
throw new Error("Missing input. Provide --value or a CSS file path.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const inputPath = path.resolve(process.cwd(), args.input);
|
|
134
|
+
const css = await fs.readFile(inputPath, "utf8");
|
|
135
|
+
const transformed = replaceOklchInText(css, options);
|
|
136
|
+
|
|
137
|
+
if (args.stdout || !args.output) {
|
|
138
|
+
process.stdout.write(transformed);
|
|
139
|
+
if (!transformed.endsWith("\n")) {
|
|
140
|
+
process.stdout.write("\n");
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const outputPath = path.resolve(process.cwd(), args.output);
|
|
146
|
+
await fs.writeFile(outputPath, transformed, "utf8");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
run().catch((error) => {
|
|
150
|
+
process.stderr.write(`${error.message}\n`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
package/src/converter.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { getTailwindDefaultHexForVar } from "./tailwind-default-hex.js";
|
|
2
|
+
const OKLCH_REGEX = /oklch\(([^)]*)\)/gi;
|
|
3
|
+
|
|
4
|
+
function clamp(value, min, max) {
|
|
5
|
+
return Math.min(max, Math.max(min, value));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseNumericToken(token) {
|
|
9
|
+
const value = Number.parseFloat(token);
|
|
10
|
+
if (Number.isNaN(value)) {
|
|
11
|
+
throw new Error(`Invalid numeric token: "${token}"`);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseLightness(token) {
|
|
17
|
+
const trimmed = token.trim().toLowerCase();
|
|
18
|
+
if (trimmed.endsWith("%")) {
|
|
19
|
+
return clamp(parseNumericToken(trimmed.slice(0, -1)) / 100, 0, 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const value = parseNumericToken(trimmed);
|
|
23
|
+
if (value > 1 && value <= 100) {
|
|
24
|
+
return clamp(value / 100, 0, 1);
|
|
25
|
+
}
|
|
26
|
+
return clamp(value, 0, 1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseHue(token) {
|
|
30
|
+
const trimmed = token.trim().toLowerCase();
|
|
31
|
+
if (trimmed.endsWith("deg")) {
|
|
32
|
+
return parseNumericToken(trimmed.slice(0, -3));
|
|
33
|
+
}
|
|
34
|
+
if (trimmed.endsWith("rad")) {
|
|
35
|
+
return (parseNumericToken(trimmed.slice(0, -3)) * 180) / Math.PI;
|
|
36
|
+
}
|
|
37
|
+
if (trimmed.endsWith("turn")) {
|
|
38
|
+
return parseNumericToken(trimmed.slice(0, -4)) * 360;
|
|
39
|
+
}
|
|
40
|
+
return parseNumericToken(trimmed);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseAlpha(token) {
|
|
44
|
+
if (typeof token !== "string") {
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const trimmed = token.trim().toLowerCase();
|
|
49
|
+
if (trimmed.endsWith("%")) {
|
|
50
|
+
return clamp(parseNumericToken(trimmed.slice(0, -1)) / 100, 0, 1);
|
|
51
|
+
}
|
|
52
|
+
return clamp(parseNumericToken(trimmed), 0, 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeHueDegrees(hue) {
|
|
56
|
+
const normalized = hue % 360;
|
|
57
|
+
return normalized < 0 ? normalized + 360 : normalized;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseOklch(input) {
|
|
61
|
+
if (typeof input !== "string") {
|
|
62
|
+
throw new Error("parseOklch expects a string like oklch(70% 0.2 30)");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const trimmed = input.trim();
|
|
66
|
+
const lower = trimmed.toLowerCase();
|
|
67
|
+
if (!lower.startsWith("oklch(") || !lower.endsWith(")")) {
|
|
68
|
+
throw new Error(`Invalid OKLCH function: "${input}"`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const inner = trimmed.slice(trimmed.indexOf("(") + 1, -1).trim();
|
|
72
|
+
const slashIndex = inner.indexOf("/");
|
|
73
|
+
const colorPart = slashIndex === -1 ? inner : inner.slice(0, slashIndex).trim();
|
|
74
|
+
const alphaPart = slashIndex === -1 ? undefined : inner.slice(slashIndex + 1).trim();
|
|
75
|
+
|
|
76
|
+
const parts = colorPart
|
|
77
|
+
.replace(/,/g, " ")
|
|
78
|
+
.split(/\s+/)
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
|
|
81
|
+
if (parts.length < 3) {
|
|
82
|
+
throw new Error(`Invalid OKLCH channels: "${input}"`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const l = parseLightness(parts[0]);
|
|
86
|
+
const c = Math.max(0, parseNumericToken(parts[1]));
|
|
87
|
+
const h = normalizeHueDegrees(parseHue(parts[2]));
|
|
88
|
+
const alpha = parseAlpha(alphaPart);
|
|
89
|
+
|
|
90
|
+
return { l, c, h, alpha };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function oklabToLinearSrgb(l, a, b) {
|
|
94
|
+
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
|
|
95
|
+
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
|
|
96
|
+
const s_ = l - 0.0894841775 * a - 1.291485548 * b;
|
|
97
|
+
|
|
98
|
+
const l3 = l_ * l_ * l_;
|
|
99
|
+
const m3 = m_ * m_ * m_;
|
|
100
|
+
const s3 = s_ * s_ * s_;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
r: +4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3,
|
|
104
|
+
g: -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3,
|
|
105
|
+
b: -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function oklchToLinearSrgb(l, c, hDegrees) {
|
|
110
|
+
const radians = (hDegrees * Math.PI) / 180;
|
|
111
|
+
const a = c * Math.cos(radians);
|
|
112
|
+
const b = c * Math.sin(radians);
|
|
113
|
+
return oklabToLinearSrgb(l, a, b);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isInSrgbGamut(linearRgb) {
|
|
117
|
+
return (
|
|
118
|
+
linearRgb.r >= 0 &&
|
|
119
|
+
linearRgb.r <= 1 &&
|
|
120
|
+
linearRgb.g >= 0 &&
|
|
121
|
+
linearRgb.g <= 1 &&
|
|
122
|
+
linearRgb.b >= 0 &&
|
|
123
|
+
linearRgb.b <= 1
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fitChromaToSrgb(l, c, h) {
|
|
128
|
+
const initial = oklchToLinearSrgb(l, c, h);
|
|
129
|
+
if (isInSrgbGamut(initial)) {
|
|
130
|
+
return c;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let low = 0;
|
|
134
|
+
let high = c;
|
|
135
|
+
for (let i = 0; i < 24; i += 1) {
|
|
136
|
+
const mid = (low + high) / 2;
|
|
137
|
+
const rgb = oklchToLinearSrgb(l, mid, h);
|
|
138
|
+
if (isInSrgbGamut(rgb)) {
|
|
139
|
+
low = mid;
|
|
140
|
+
} else {
|
|
141
|
+
high = mid;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return low;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function linearToSrgbChannel(channel) {
|
|
148
|
+
if (channel <= 0.0031308) {
|
|
149
|
+
return 12.92 * channel;
|
|
150
|
+
}
|
|
151
|
+
return 1.055 * channel ** (1 / 2.4) - 0.055;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function channelToHex(channel) {
|
|
155
|
+
const clamped = clamp(channel, 0, 1);
|
|
156
|
+
const byte = Math.round(clamped * 255);
|
|
157
|
+
return byte.toString(16).padStart(2, "0");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeInput(input) {
|
|
161
|
+
if (typeof input === "string") {
|
|
162
|
+
return parseOklch(input);
|
|
163
|
+
}
|
|
164
|
+
if (input && typeof input === "object") {
|
|
165
|
+
if (
|
|
166
|
+
typeof input.l === "number" &&
|
|
167
|
+
typeof input.c === "number" &&
|
|
168
|
+
typeof input.h === "number"
|
|
169
|
+
) {
|
|
170
|
+
return {
|
|
171
|
+
l: clamp(input.l, 0, 1),
|
|
172
|
+
c: Math.max(0, input.c),
|
|
173
|
+
h: normalizeHueDegrees(input.h),
|
|
174
|
+
alpha: clamp(input.alpha ?? 1, 0, 1)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw new Error("Input must be an OKLCH string or { l, c, h, alpha } object");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function oklchToHex(input, options = {}) {
|
|
182
|
+
const { gamut = "clip", includeAlpha = "auto", uppercase = false } = options;
|
|
183
|
+
const parsed = normalizeInput(input);
|
|
184
|
+
|
|
185
|
+
const c =
|
|
186
|
+
gamut === "fit"
|
|
187
|
+
? fitChromaToSrgb(parsed.l, parsed.c, parsed.h)
|
|
188
|
+
: parsed.c;
|
|
189
|
+
|
|
190
|
+
const linear = oklchToLinearSrgb(parsed.l, c, parsed.h);
|
|
191
|
+
const r = linearToSrgbChannel(linear.r);
|
|
192
|
+
const g = linearToSrgbChannel(linear.g);
|
|
193
|
+
const b = linearToSrgbChannel(linear.b);
|
|
194
|
+
|
|
195
|
+
let hex = `#${channelToHex(r)}${channelToHex(g)}${channelToHex(b)}`;
|
|
196
|
+
const shouldIncludeAlpha =
|
|
197
|
+
includeAlpha === true || includeAlpha === "always" || (includeAlpha === "auto" && parsed.alpha < 1);
|
|
198
|
+
|
|
199
|
+
if (shouldIncludeAlpha) {
|
|
200
|
+
hex += channelToHex(parsed.alpha);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return uppercase ? hex.toUpperCase() : hex;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function replaceTailwindDefaultColorVariables(text) {
|
|
207
|
+
const declarationRegex =
|
|
208
|
+
/(--color-[a-z]+-\d{2,3})(\s*:\s*)oklch\(([^)]*)\)/gi;
|
|
209
|
+
|
|
210
|
+
return text.replace(
|
|
211
|
+
declarationRegex,
|
|
212
|
+
(fullMatch, variableName, separator) => {
|
|
213
|
+
const mappedHex = getTailwindDefaultHexForVar(variableName.toLowerCase());
|
|
214
|
+
if (!mappedHex) {
|
|
215
|
+
return fullMatch;
|
|
216
|
+
}
|
|
217
|
+
return `${variableName}${separator}${mappedHex}`;
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function replaceOklchInText(text, options = {}) {
|
|
223
|
+
if (typeof text !== "string") {
|
|
224
|
+
throw new Error("replaceOklchInText expects a string");
|
|
225
|
+
}
|
|
226
|
+
const { onError = "preserve", ...convertOptions } = options;
|
|
227
|
+
const withMappedTailwindDefaults = replaceTailwindDefaultColorVariables(text);
|
|
228
|
+
|
|
229
|
+
return withMappedTailwindDefaults.replace(OKLCH_REGEX, (match) => {
|
|
230
|
+
try {
|
|
231
|
+
return oklchToHex(match, convertOptions);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (onError === "throw") {
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
return match;
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function convertTailwindCssToHex(css, options = {}) {
|
|
242
|
+
return replaceOklchInText(css, options);
|
|
243
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
const TAILWIND_HEX_COLORS = {
|
|
2
|
+
slate: {
|
|
3
|
+
50: "#f8fafc",
|
|
4
|
+
100: "#f1f5f9",
|
|
5
|
+
200: "#e2e8f0",
|
|
6
|
+
300: "#cbd5e1",
|
|
7
|
+
400: "#94a3b8",
|
|
8
|
+
500: "#64748b",
|
|
9
|
+
600: "#475569",
|
|
10
|
+
700: "#334155",
|
|
11
|
+
800: "#1e293b",
|
|
12
|
+
900: "#0f172a",
|
|
13
|
+
950: "#020617"
|
|
14
|
+
},
|
|
15
|
+
gray: {
|
|
16
|
+
50: "#f9fafb",
|
|
17
|
+
100: "#f3f4f6",
|
|
18
|
+
200: "#e5e7eb",
|
|
19
|
+
300: "#d1d5db",
|
|
20
|
+
400: "#9ca3af",
|
|
21
|
+
500: "#6b7280",
|
|
22
|
+
600: "#4b5563",
|
|
23
|
+
700: "#374151",
|
|
24
|
+
800: "#1f2937",
|
|
25
|
+
900: "#111827",
|
|
26
|
+
950: "#030712"
|
|
27
|
+
},
|
|
28
|
+
zinc: {
|
|
29
|
+
50: "#fafafa",
|
|
30
|
+
100: "#f4f4f5",
|
|
31
|
+
200: "#e4e4e7",
|
|
32
|
+
300: "#d4d4d8",
|
|
33
|
+
400: "#a1a1aa",
|
|
34
|
+
500: "#71717a",
|
|
35
|
+
600: "#52525b",
|
|
36
|
+
700: "#3f3f46",
|
|
37
|
+
800: "#27272a",
|
|
38
|
+
900: "#18181b",
|
|
39
|
+
950: "#09090b"
|
|
40
|
+
},
|
|
41
|
+
neutral: {
|
|
42
|
+
50: "#fafafa",
|
|
43
|
+
100: "#f5f5f5",
|
|
44
|
+
200: "#e5e5e5",
|
|
45
|
+
300: "#d4d4d4",
|
|
46
|
+
400: "#a3a3a3",
|
|
47
|
+
500: "#737373",
|
|
48
|
+
600: "#525252",
|
|
49
|
+
700: "#404040",
|
|
50
|
+
800: "#262626",
|
|
51
|
+
900: "#171717",
|
|
52
|
+
950: "#0a0a0a"
|
|
53
|
+
},
|
|
54
|
+
stone: {
|
|
55
|
+
50: "#fafaf9",
|
|
56
|
+
100: "#f5f5f4",
|
|
57
|
+
200: "#e7e5e4",
|
|
58
|
+
300: "#d6d3d1",
|
|
59
|
+
400: "#a8a29e",
|
|
60
|
+
500: "#78716c",
|
|
61
|
+
600: "#57534e",
|
|
62
|
+
700: "#44403c",
|
|
63
|
+
800: "#292524",
|
|
64
|
+
900: "#1c1917",
|
|
65
|
+
950: "#0c0a09"
|
|
66
|
+
},
|
|
67
|
+
red: {
|
|
68
|
+
50: "#fef2f2",
|
|
69
|
+
100: "#fee2e2",
|
|
70
|
+
200: "#fecaca",
|
|
71
|
+
300: "#fca5a5",
|
|
72
|
+
400: "#f87171",
|
|
73
|
+
500: "#ef4444",
|
|
74
|
+
600: "#dc2626",
|
|
75
|
+
700: "#b91c1c",
|
|
76
|
+
800: "#991b1b",
|
|
77
|
+
900: "#7f1d1d",
|
|
78
|
+
950: "#450a0a"
|
|
79
|
+
},
|
|
80
|
+
orange: {
|
|
81
|
+
50: "#fff7ed",
|
|
82
|
+
100: "#ffedd5",
|
|
83
|
+
200: "#fed7aa",
|
|
84
|
+
300: "#fdba74",
|
|
85
|
+
400: "#fb923c",
|
|
86
|
+
500: "#f97316",
|
|
87
|
+
600: "#ea580c",
|
|
88
|
+
700: "#c2410c",
|
|
89
|
+
800: "#9a3412",
|
|
90
|
+
900: "#7c2d12",
|
|
91
|
+
950: "#431407"
|
|
92
|
+
},
|
|
93
|
+
amber: {
|
|
94
|
+
50: "#fffbeb",
|
|
95
|
+
100: "#fef3c7",
|
|
96
|
+
200: "#fde68a",
|
|
97
|
+
300: "#fcd34d",
|
|
98
|
+
400: "#fbbf24",
|
|
99
|
+
500: "#f59e0b",
|
|
100
|
+
600: "#d97706",
|
|
101
|
+
700: "#b45309",
|
|
102
|
+
800: "#92400e",
|
|
103
|
+
900: "#78350f",
|
|
104
|
+
950: "#451a03"
|
|
105
|
+
},
|
|
106
|
+
yellow: {
|
|
107
|
+
50: "#fefce8",
|
|
108
|
+
100: "#fef9c3",
|
|
109
|
+
200: "#fef08a",
|
|
110
|
+
300: "#fde047",
|
|
111
|
+
400: "#facc15",
|
|
112
|
+
500: "#eab308",
|
|
113
|
+
600: "#ca8a04",
|
|
114
|
+
700: "#a16207",
|
|
115
|
+
800: "#854d0e",
|
|
116
|
+
900: "#713f12",
|
|
117
|
+
950: "#422006"
|
|
118
|
+
},
|
|
119
|
+
lime: {
|
|
120
|
+
50: "#f7fee7",
|
|
121
|
+
100: "#ecfccb",
|
|
122
|
+
200: "#d9f99d",
|
|
123
|
+
300: "#bef264",
|
|
124
|
+
400: "#a3e635",
|
|
125
|
+
500: "#84cc16",
|
|
126
|
+
600: "#65a30d",
|
|
127
|
+
700: "#4d7c0f",
|
|
128
|
+
800: "#3f6212",
|
|
129
|
+
900: "#365314",
|
|
130
|
+
950: "#1a2e05"
|
|
131
|
+
},
|
|
132
|
+
green: {
|
|
133
|
+
50: "#f0fdf4",
|
|
134
|
+
100: "#dcfce7",
|
|
135
|
+
200: "#bbf7d0",
|
|
136
|
+
300: "#86efac",
|
|
137
|
+
400: "#4ade80",
|
|
138
|
+
500: "#22c55e",
|
|
139
|
+
600: "#16a34a",
|
|
140
|
+
700: "#15803d",
|
|
141
|
+
800: "#166534",
|
|
142
|
+
900: "#14532d",
|
|
143
|
+
950: "#052e16"
|
|
144
|
+
},
|
|
145
|
+
emerald: {
|
|
146
|
+
50: "#ecfdf5",
|
|
147
|
+
100: "#d1fae5",
|
|
148
|
+
200: "#a7f3d0",
|
|
149
|
+
300: "#6ee7b7",
|
|
150
|
+
400: "#34d399",
|
|
151
|
+
500: "#10b981",
|
|
152
|
+
600: "#059669",
|
|
153
|
+
700: "#047857",
|
|
154
|
+
800: "#065f46",
|
|
155
|
+
900: "#064e3b",
|
|
156
|
+
950: "#022c22"
|
|
157
|
+
},
|
|
158
|
+
teal: {
|
|
159
|
+
50: "#f0fdfa",
|
|
160
|
+
100: "#ccfbf1",
|
|
161
|
+
200: "#99f6e4",
|
|
162
|
+
300: "#5eead4",
|
|
163
|
+
400: "#2dd4bf",
|
|
164
|
+
500: "#14b8a6",
|
|
165
|
+
600: "#0d9488",
|
|
166
|
+
700: "#0f766e",
|
|
167
|
+
800: "#115e59",
|
|
168
|
+
900: "#134e4a",
|
|
169
|
+
950: "#042f2e"
|
|
170
|
+
},
|
|
171
|
+
cyan: {
|
|
172
|
+
50: "#ecfeff",
|
|
173
|
+
100: "#cffafe",
|
|
174
|
+
200: "#a5f3fc",
|
|
175
|
+
300: "#67e8f9",
|
|
176
|
+
400: "#22d3ee",
|
|
177
|
+
500: "#06b6d4",
|
|
178
|
+
600: "#0891b2",
|
|
179
|
+
700: "#0e7490",
|
|
180
|
+
800: "#155e75",
|
|
181
|
+
900: "#164e63",
|
|
182
|
+
950: "#083344"
|
|
183
|
+
},
|
|
184
|
+
sky: {
|
|
185
|
+
50: "#f0f9ff",
|
|
186
|
+
100: "#e0f2fe",
|
|
187
|
+
200: "#bae6fd",
|
|
188
|
+
300: "#7dd3fc",
|
|
189
|
+
400: "#38bdf8",
|
|
190
|
+
500: "#0ea5e9",
|
|
191
|
+
600: "#0284c7",
|
|
192
|
+
700: "#0369a1",
|
|
193
|
+
800: "#075985",
|
|
194
|
+
900: "#0c4a6e",
|
|
195
|
+
950: "#082f49"
|
|
196
|
+
},
|
|
197
|
+
blue: {
|
|
198
|
+
50: "#eff6ff",
|
|
199
|
+
100: "#dbeafe",
|
|
200
|
+
200: "#bfdbfe",
|
|
201
|
+
300: "#93c5fd",
|
|
202
|
+
400: "#60a5fa",
|
|
203
|
+
500: "#3b82f6",
|
|
204
|
+
600: "#2563eb",
|
|
205
|
+
700: "#1d4ed8",
|
|
206
|
+
800: "#1e40af",
|
|
207
|
+
900: "#1e3a8a",
|
|
208
|
+
950: "#172554"
|
|
209
|
+
},
|
|
210
|
+
indigo: {
|
|
211
|
+
50: "#eef2ff",
|
|
212
|
+
100: "#e0e7ff",
|
|
213
|
+
200: "#c7d2fe",
|
|
214
|
+
300: "#a5b4fc",
|
|
215
|
+
400: "#818cf8",
|
|
216
|
+
500: "#6366f1",
|
|
217
|
+
600: "#4f46e5",
|
|
218
|
+
700: "#4338ca",
|
|
219
|
+
800: "#3730a3",
|
|
220
|
+
900: "#312e81",
|
|
221
|
+
950: "#1e1b4b"
|
|
222
|
+
},
|
|
223
|
+
violet: {
|
|
224
|
+
50: "#f5f3ff",
|
|
225
|
+
100: "#ede9fe",
|
|
226
|
+
200: "#ddd6fe",
|
|
227
|
+
300: "#c4b5fd",
|
|
228
|
+
400: "#a78bfa",
|
|
229
|
+
500: "#8b5cf6",
|
|
230
|
+
600: "#7c3aed",
|
|
231
|
+
700: "#6d28d9",
|
|
232
|
+
800: "#5b21b6",
|
|
233
|
+
900: "#4c1d95",
|
|
234
|
+
950: "#2e1065"
|
|
235
|
+
},
|
|
236
|
+
purple: {
|
|
237
|
+
50: "#faf5ff",
|
|
238
|
+
100: "#f3e8ff",
|
|
239
|
+
200: "#e9d5ff",
|
|
240
|
+
300: "#d8b4fe",
|
|
241
|
+
400: "#c084fc",
|
|
242
|
+
500: "#a855f7",
|
|
243
|
+
600: "#9333ea",
|
|
244
|
+
700: "#7e22ce",
|
|
245
|
+
800: "#6b21a8",
|
|
246
|
+
900: "#581c87",
|
|
247
|
+
950: "#3b0764"
|
|
248
|
+
},
|
|
249
|
+
fuchsia: {
|
|
250
|
+
50: "#fdf4ff",
|
|
251
|
+
100: "#fae8ff",
|
|
252
|
+
200: "#f5d0fe",
|
|
253
|
+
300: "#f0abfc",
|
|
254
|
+
400: "#e879f9",
|
|
255
|
+
500: "#d946ef",
|
|
256
|
+
600: "#c026d3",
|
|
257
|
+
700: "#a21caf",
|
|
258
|
+
800: "#86198f",
|
|
259
|
+
900: "#701a75",
|
|
260
|
+
950: "#4a044e"
|
|
261
|
+
},
|
|
262
|
+
pink: {
|
|
263
|
+
50: "#fdf2f8",
|
|
264
|
+
100: "#fce7f3",
|
|
265
|
+
200: "#fbcfe8",
|
|
266
|
+
300: "#f9a8d4",
|
|
267
|
+
400: "#f472b6",
|
|
268
|
+
500: "#ec4899",
|
|
269
|
+
600: "#db2777",
|
|
270
|
+
700: "#be185d",
|
|
271
|
+
800: "#9d174d",
|
|
272
|
+
900: "#831843",
|
|
273
|
+
950: "#500724"
|
|
274
|
+
},
|
|
275
|
+
rose: {
|
|
276
|
+
50: "#fff1f2",
|
|
277
|
+
100: "#ffe4e6",
|
|
278
|
+
200: "#fecdd3",
|
|
279
|
+
300: "#fda4af",
|
|
280
|
+
400: "#fb7185",
|
|
281
|
+
500: "#f43f5e",
|
|
282
|
+
600: "#e11d48",
|
|
283
|
+
700: "#be123c",
|
|
284
|
+
800: "#9f1239",
|
|
285
|
+
900: "#881337",
|
|
286
|
+
950: "#4c0519"
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const TAILWIND_HEX_COLOR_MAP = new Map();
|
|
291
|
+
|
|
292
|
+
for (const [family, scale] of Object.entries(TAILWIND_HEX_COLORS)) {
|
|
293
|
+
for (const [step, hex] of Object.entries(scale)) {
|
|
294
|
+
TAILWIND_HEX_COLOR_MAP.set(`--color-${family}-${step}`, hex);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function getTailwindDefaultHexForVar(variableName) {
|
|
299
|
+
return TAILWIND_HEX_COLOR_MAP.get(variableName) ?? null;
|
|
300
|
+
}
|