oklchtohex 0.3.0 → 0.3.2

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 CHANGED
@@ -34,6 +34,7 @@ const convertedTailwindCss = convertTailwindCssToHex(css);
34
34
 
35
35
  - Known Tailwind default variables like `--color-red-500` are replaced with exact Tailwind HEX palette values.
36
36
  - Unknown variables and raw `oklch(...)` values use functional conversion.
37
+ - Tailwind opacity utilities like `border-red-500/30` and custom theme colors like `border-aurora/40` are handled when emitted as `color-mix(... var(--color-*) ..., transparent)` and converted to 8-digit HEX when the source variable is resolvable.
37
38
  - `gamut` option supports `clip` (default) and `fit`.
38
39
 
39
40
  ## Vite plugin (auto on dev + build)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oklchtohex",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Convert OKLCH colors to HEX as a JavaScript package.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/converter.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { getTailwindDefaultHexForVar } from "./tailwind-default-hex.js";
2
2
  const OKLCH_REGEX = /oklch\(([^)]*)\)/gi;
3
+ const COLOR_MIX_WITH_TRANSPARENT_REGEX =
4
+ /color-mix\(\s*in\s+oklab\s*,\s*var\(\s*(--[\w-]+)\s*\)\s*([0-9.]+)%\s*,\s*transparent\s*\)/gi;
3
5
 
4
6
  function clamp(value, min, max) {
5
7
  return Math.min(max, Math.max(min, value));
@@ -157,6 +159,45 @@ function channelToHex(channel) {
157
159
  return byte.toString(16).padStart(2, "0");
158
160
  }
159
161
 
162
+ function normalizeHexColor(hex) {
163
+ const trimmed = hex.trim().toLowerCase();
164
+ if (!trimmed.startsWith("#")) {
165
+ return null;
166
+ }
167
+ const raw = trimmed.slice(1);
168
+ if (raw.length === 3) {
169
+ return `#${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}`;
170
+ }
171
+ if (raw.length === 4) {
172
+ return `#${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}${raw[3]}${raw[3]}`;
173
+ }
174
+ if (raw.length === 6 || raw.length === 8) {
175
+ return `#${raw}`;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ function splitHexChannels(hex) {
181
+ const normalized = normalizeHexColor(hex);
182
+ if (!normalized) {
183
+ return null;
184
+ }
185
+ const raw = normalized.slice(1);
186
+ const rgbHex = raw.slice(0, 6);
187
+ const alphaHex = raw.length === 8 ? raw.slice(6, 8) : "ff";
188
+ return { rgbHex, alphaHex };
189
+ }
190
+
191
+ function alphaToHex(alpha) {
192
+ return channelToHex(clamp(alpha, 0, 1));
193
+ }
194
+
195
+ function combineAlpha(existingAlphaHex, mixPercent) {
196
+ const baseAlpha = Number.parseInt(existingAlphaHex, 16) / 255;
197
+ const mixedAlpha = baseAlpha * clamp(mixPercent / 100, 0, 1);
198
+ return alphaToHex(mixedAlpha);
199
+ }
200
+
160
201
  function normalizeInput(input) {
161
202
  if (typeof input === "string") {
162
203
  return parseOklch(input);
@@ -219,14 +260,57 @@ function replaceTailwindDefaultColorVariables(text) {
219
260
  );
220
261
  }
221
262
 
263
+ function collectHexVariableDeclarations(text) {
264
+ const declarationRegex = /(--[\w-]+)\s*:\s*(#[0-9a-fA-F]{3,8})\s*;/g;
265
+ const map = new Map();
266
+ let match = declarationRegex.exec(text);
267
+ while (match) {
268
+ const variableName = match[1].toLowerCase();
269
+ const normalized = normalizeHexColor(match[2]);
270
+ if (normalized) {
271
+ map.set(variableName, normalized);
272
+ }
273
+ match = declarationRegex.exec(text);
274
+ }
275
+ return map;
276
+ }
277
+
278
+ function replaceTailwindColorMixWithHex(text, variableHexMap) {
279
+ return text.replace(
280
+ COLOR_MIX_WITH_TRANSPARENT_REGEX,
281
+ (fullMatch, variableNameRaw, percentRaw) => {
282
+ const variableName = String(variableNameRaw).toLowerCase();
283
+ const percent = Number.parseFloat(percentRaw);
284
+ if (Number.isNaN(percent)) {
285
+ return fullMatch;
286
+ }
287
+
288
+ const sourceHex =
289
+ variableHexMap.get(variableName) ??
290
+ getTailwindDefaultHexForVar(variableName);
291
+
292
+ if (!sourceHex) {
293
+ return fullMatch;
294
+ }
295
+
296
+ const channels = splitHexChannels(sourceHex);
297
+ if (!channels) {
298
+ return fullMatch;
299
+ }
300
+
301
+ const alphaHex = combineAlpha(channels.alphaHex, percent);
302
+ return `#${channels.rgbHex}${alphaHex}`;
303
+ }
304
+ );
305
+ }
306
+
222
307
  export function replaceOklchInText(text, options = {}) {
223
308
  if (typeof text !== "string") {
224
309
  throw new Error("replaceOklchInText expects a string");
225
310
  }
226
311
  const { onError = "preserve", ...convertOptions } = options;
227
312
  const withMappedTailwindDefaults = replaceTailwindDefaultColorVariables(text);
228
-
229
- return withMappedTailwindDefaults.replace(OKLCH_REGEX, (match) => {
313
+ const withConvertedOklch = withMappedTailwindDefaults.replace(OKLCH_REGEX, (match) => {
230
314
  try {
231
315
  return oklchToHex(match, convertOptions);
232
316
  } catch (error) {
@@ -236,6 +320,9 @@ export function replaceOklchInText(text, options = {}) {
236
320
  return match;
237
321
  }
238
322
  });
323
+
324
+ const variableHexMap = collectHexVariableDeclarations(withConvertedOklch);
325
+ return replaceTailwindColorMixWithHex(withConvertedOklch, variableHexMap);
239
326
  }
240
327
 
241
328
  export function convertTailwindCssToHex(css, options = {}) {