@rokkit/core 1.0.2 → 1.0.4

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/src/utils.js CHANGED
@@ -195,7 +195,7 @@ export function resolveSnippet(snippets, proxy, fallback = ITEM_SNIPPET) {
195
195
  }
196
196
 
197
197
  /**
198
- * convert hex string to `{r} {g} {b}`
198
+ * convert hex string to `{r},{g},{b}`
199
199
  * @param {string} hex
200
200
  * @return {string}
201
201
  */
@@ -205,22 +205,208 @@ export function hex2rgb(hex) {
205
205
  }
206
206
 
207
207
  /**
208
- * Convert a CSS color value to r,g,b for use in CSS variables.
209
- * Hex values (#rrggbb) are converted to "r,g,b" for rgba() support.
210
- * All other CSS color formats (oklch, hsl, named) are returned as-is.
211
- * Non-string values are returned unchanged.
212
- * Note: non-hex values will NOT work with UnoCSS opacity utilities like bg-primary/50.
208
+ * Convert sRGB component (0-255) to linear RGB.
209
+ * @param {number} c - sRGB value 0-255
210
+ * @returns {number}
211
+ */
212
+ function srgbToLinear(c) {
213
+ const s = c / 255
214
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
215
+ }
216
+
217
+ /**
218
+ * Convert linear RGB to sRGB component (0-255).
219
+ * @param {number} c - linear value 0-1
220
+ * @returns {number}
221
+ */
222
+ function linearToSrgb(c) {
223
+ const s = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055
224
+ return Math.round(Math.max(0, Math.min(255, s * 255)))
225
+ }
226
+
227
+ // CSS Color Level 4 two-step matrices (via XYZ D65) for round-trip fidelity.
228
+ // Linear sRGB → XYZ D65
229
+ const SRGB_TO_XYZ = [
230
+ [0.4123907993, 0.3575843394, 0.1804807884],
231
+ [0.2126390059, 0.7151686788, 0.0721923154],
232
+ [0.0193308187, 0.1191947798, 0.9505321522]
233
+ ]
234
+ // XYZ D65 → LMS
235
+ const XYZ_TO_LMS = [
236
+ [0.8189330101, 0.3618667424, -0.1288597137],
237
+ [0.0329845436, 0.9293118715, 0.0361456387],
238
+ [0.0482003018, 0.2643662691, 0.633851707]
239
+ ]
240
+ // LMS^(1/3) → OKLab
241
+ const LMS3_TO_OKLAB = [
242
+ [0.2104542553, 0.793617785, -0.0040720468],
243
+ [1.9779984951, -2.428592205, 0.4505937099],
244
+ [0.0259040371, 0.7827717662, -0.808675766]
245
+ ]
246
+ // OKLab → LMS^(1/3)
247
+ const OKLAB_TO_LMS3 = [
248
+ [1.0, 0.3963377774, 0.2158037573],
249
+ [1.0, -0.1055613458, -0.0638541728],
250
+ [1.0, -0.0894841775, -1.291485548]
251
+ ]
252
+ // LMS → XYZ D65
253
+ const LMS_TO_XYZ = [
254
+ [1.2270138511, -0.5577999807, 0.281256149],
255
+ [-0.0405801784, 1.1122568696, -0.0716766787],
256
+ [-0.0763812845, -0.4214819784, 1.5861632204]
257
+ ]
258
+ // XYZ D65 → linear sRGB
259
+ const XYZ_TO_SRGB = [
260
+ [3.2409699419, -1.5373831776, -0.4986107603],
261
+ [-0.9692436363, 1.8759675015, 0.0415550574],
262
+ [0.0556300797, -0.2039769589, 1.0569715142]
263
+ ]
264
+
265
+ /** @param {number[][]} m @param {number[]} v */
266
+ function matMul(m, v) {
267
+ return m.map((row) => row[0] * v[0] + row[1] * v[1] + row[2] * v[2])
268
+ }
269
+
270
+ /**
271
+ * Convert hex string to OKLCH components: `{L} {C} {H}`
272
+ * Uses CSS Color Level 4 two-step conversion via XYZ D65 for precision.
273
+ * @param {string} hex - 6-digit hex color (with or without #)
274
+ * @returns {string} space-separated OKLCH components for CSS `oklch(L C H / alpha)`
275
+ */
276
+ export function hex2oklch(hex) {
277
+ const [r, g, b] = hex.match(/\w\w/g).map((x) => parseInt(x, 16))
278
+ const lin = [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)]
279
+
280
+ const xyz = matMul(SRGB_TO_XYZ, lin)
281
+ const lms = matMul(XYZ_TO_LMS, xyz)
282
+ const lms3 = [Math.cbrt(lms[0]), Math.cbrt(lms[1]), Math.cbrt(lms[2])]
283
+ const [L, a, bk] = matMul(LMS3_TO_OKLAB, lms3)
284
+
285
+ const C = Math.sqrt(a * a + bk * bk)
286
+ let H = (Math.atan2(bk, a) * 180) / Math.PI
287
+ if (H < 0) H += 360
288
+
289
+ const Lr = Math.round(L * 1000000) / 1000000
290
+ const Cr = Math.round(C * 1000000) / 1000000
291
+ const Hr = Math.round(H * 10000) / 10000
292
+
293
+ return `${Lr} ${Cr} ${Hr}`
294
+ }
295
+
296
+ /**
297
+ * Convert OKLCH components back to hex string.
298
+ * Uses CSS Color Level 4 two-step conversion via XYZ D65 for precision.
299
+ * @param {number} L - Lightness 0-1
300
+ * @param {number} C - Chroma >= 0
301
+ * @param {number} H - Hue 0-360 degrees
302
+ * @returns {string} hex color string (#rrggbb)
303
+ */
304
+ export function oklch2hex(L, C, H) {
305
+ const hRad = (H * Math.PI) / 180
306
+ const lab = [L, C * Math.cos(hRad), C * Math.sin(hRad)]
307
+
308
+ const lms3 = matMul(OKLAB_TO_LMS3, lab)
309
+ const lms = [lms3[0] ** 3, lms3[1] ** 3, lms3[2] ** 3]
310
+ const xyz = matMul(LMS_TO_XYZ, lms)
311
+ const lin = matMul(XYZ_TO_SRGB, xyz)
312
+
313
+ const toHex = (n) => {
314
+ const hex = linearToSrgb(n).toString(16)
315
+ return hex.length === 1 ? `0${hex}` : hex
316
+ }
317
+ return `#${toHex(lin[0])}${toHex(lin[1])}${toHex(lin[2])}`
318
+ }
319
+
320
+ /**
321
+ * Convert hex string to HSL components: `{H} {S}% {L}%`
322
+ * @param {string} hex - 6-digit hex color (with or without #)
323
+ * @returns {string} space-separated HSL components for CSS `hsl(H S% L% / alpha)`
324
+ */
325
+ export function hex2hsl(hex) {
326
+ const [r, g, b] = hex.match(/\w\w/g).map((x) => parseInt(x, 16))
327
+
328
+ const rn = r / 255
329
+ const gn = g / 255
330
+ const bn = b / 255
331
+ const max = Math.max(rn, gn, bn)
332
+ const min = Math.min(rn, gn, bn)
333
+ const l = (max + min) / 2
334
+
335
+ if (max === min) return `0 0% ${Math.round(l * 100)}%`
336
+
337
+ const d = max - min
338
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
339
+
340
+ let h = 0
341
+ if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6
342
+ else if (max === gn) h = ((bn - rn) / d + 2) / 6
343
+ else h = ((rn - gn) / d + 4) / 6
344
+
345
+ const Hr = Math.round(h * 360)
346
+ const Sr = Math.round(s * 100)
347
+ const Lr = Math.round(l * 100)
348
+
349
+ return `${Hr} ${Sr}% ${Lr}%`
350
+ }
351
+
352
+ /** @typedef {'rgb' | 'hsl' | 'oklch'} ColorSpace */
353
+
354
+ /**
355
+ * Convert a hex color to the component format for a given color space.
356
+ * - rgb: "r,g,b" → for `rgba(var(--x), alpha)`
357
+ * - hsl: "H S% L%" → for `hsl(var(--x) / alpha)`
358
+ * - oklch: "L C H" → for `oklch(var(--x) / alpha)`
359
+ *
360
+ * @param {string} hex - 6-digit hex color (#rrggbb)
361
+ * @param {ColorSpace} space
362
+ * @returns {string}
363
+ */
364
+ export function hexToComponents(hex, space) {
365
+ switch (space) {
366
+ case 'hsl':
367
+ return hex2hsl(hex)
368
+ case 'oklch':
369
+ return hex2oklch(hex)
370
+ case 'rgb':
371
+ default:
372
+ return hex2rgb(hex)
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Convert a CSS color value to components for use in CSS variables.
378
+ *
379
+ * - Hex (#rrggbb): converted to space-appropriate components via hexToComponents.
380
+ * - oklch(...) strings: inner components extracted (e.g. "0.58 0.15 35") so the
381
+ * value is compatible with the oklch(var(--x) / alpha) wrapper pattern.
382
+ * - All other formats: returned as-is (no opacity-modifier support).
213
383
  *
214
384
  * @param {unknown} value
385
+ * @param {ColorSpace} [space]
215
386
  * @returns {unknown}
216
387
  */
217
- export function colorToRgb(value) {
218
- if (typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value)) {
219
- return hex2rgb(value)
388
+ export function colorToRgb(value, space) {
389
+ if (typeof value !== 'string') return value
390
+ if (/^#[0-9a-fA-F]{6}$/.test(value)) {
391
+ return space ? hexToComponents(value, space) : hex2rgb(value)
220
392
  }
393
+ // oklch(...) — extract inner components so the var can be used in oklch(var(--x) / alpha)
394
+ const oklchMatch = value.match(/^oklch\(\s*([^)]+?)\s*\)$/)
395
+ if (oklchMatch) return oklchMatch[1]
221
396
  return value
222
397
  }
223
398
 
399
+
400
+ /**
401
+ * Detects whether an icon value is a CSS class (i-*) or literal text (kanji, emoji, etc.)
402
+ * @param {string | null | undefined} icon
403
+ * @returns {boolean} true if icon is a CSS class string
404
+ */
405
+ export function isIconClass(icon) {
406
+ if (!icon || typeof icon !== 'string') return false
407
+ return icon.startsWith('i-')
408
+ }
409
+
224
410
  /**
225
411
  * Checks if a string is a valid image URL
226
412
  *