@rokkit/core 1.0.3 → 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/dist/constants.d.ts +1 -0
- package/dist/theme.d.ts +12 -26
- package/dist/types.d.ts +5 -0
- package/dist/utils.d.ts +50 -7
- package/package.json +2 -2
- package/src/colors/index.ts +1 -0
- package/src/constants.js +1 -0
- package/src/theme.ts +61 -11
- package/src/types.js +5 -0
- package/src/utils.js +195 -9
package/dist/constants.d.ts
CHANGED
package/dist/theme.d.ts
CHANGED
|
@@ -13,19 +13,21 @@ export declare function shadesOf(name: any, modifier?: string): {
|
|
|
13
13
|
*
|
|
14
14
|
* @param {Object} [mapping=DEFAULT_THEME_MAPPING] - An object mapping variant names to color property names.
|
|
15
15
|
* @param {Object} [colors=defaultColors] - The object containing default color definitions.
|
|
16
|
+
* @param {import('./utils').ColorSpace} [colorSpace] - Color space for CSS variable values.
|
|
16
17
|
* @returns {Array<Array>} An array containing two arrays, one for the light theme variant and another for the dark theme.
|
|
17
18
|
*/
|
|
18
|
-
export declare function themeRules(mapping
|
|
19
|
+
export declare function themeRules(mapping: {
|
|
19
20
|
surface: string;
|
|
20
21
|
primary: string;
|
|
21
22
|
secondary: string;
|
|
23
|
+
tertiary: string;
|
|
22
24
|
accent: string;
|
|
23
25
|
success: string;
|
|
24
26
|
warning: string;
|
|
25
27
|
danger: string;
|
|
26
28
|
error: string;
|
|
27
29
|
info: string;
|
|
28
|
-
}, colors
|
|
30
|
+
} | undefined, colors: any, colorSpace: any): {};
|
|
29
31
|
/**
|
|
30
32
|
* Generates UnoCSS shortcut definitions for semantic tones with bg, border, text.
|
|
31
33
|
* @param {string} name - Color name (e.g., 'primary')
|
|
@@ -49,14 +51,15 @@ export declare class Theme {
|
|
|
49
51
|
#private;
|
|
50
52
|
/**
|
|
51
53
|
*
|
|
52
|
-
* @param {import('./types.js').ColorTheme} param0
|
|
54
|
+
* @param {import('./types.js').ColorTheme & { colorSpace?: import('./utils').ColorSpace }} param0
|
|
53
55
|
*/
|
|
54
|
-
constructor({ colors, mapping }?: {
|
|
56
|
+
constructor({ colors, mapping, colorSpace }?: {
|
|
55
57
|
colors?: any;
|
|
56
58
|
mapping?: {
|
|
57
59
|
surface: string;
|
|
58
60
|
primary: string;
|
|
59
61
|
secondary: string;
|
|
62
|
+
tertiary: string;
|
|
60
63
|
accent: string;
|
|
61
64
|
success: string;
|
|
62
65
|
warning: string;
|
|
@@ -64,31 +67,14 @@ export declare class Theme {
|
|
|
64
67
|
error: string;
|
|
65
68
|
info: string;
|
|
66
69
|
} | undefined;
|
|
70
|
+
colorSpace?: string | undefined;
|
|
67
71
|
});
|
|
68
72
|
get colors(): any;
|
|
69
73
|
set colors(colors: any);
|
|
70
|
-
get mapping():
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
accent: string;
|
|
75
|
-
success: string;
|
|
76
|
-
warning: string;
|
|
77
|
-
danger: string;
|
|
78
|
-
error: string;
|
|
79
|
-
info: string;
|
|
80
|
-
};
|
|
81
|
-
set mapping(mapping: {
|
|
82
|
-
surface: string;
|
|
83
|
-
primary: string;
|
|
84
|
-
secondary: string;
|
|
85
|
-
accent: string;
|
|
86
|
-
success: string;
|
|
87
|
-
warning: string;
|
|
88
|
-
danger: string;
|
|
89
|
-
error: string;
|
|
90
|
-
info: string;
|
|
91
|
-
});
|
|
74
|
+
get mapping(): any;
|
|
75
|
+
set mapping(mapping: any);
|
|
76
|
+
get colorSpace(): string;
|
|
77
|
+
set colorSpace(colorSpace: string);
|
|
92
78
|
mapVariant(color: any, variant: any): {};
|
|
93
79
|
getColorRules(mapping?: null): {};
|
|
94
80
|
getPalette(mapping?: null): {};
|
package/dist/types.d.ts
CHANGED
|
@@ -286,6 +286,7 @@ export type ColorMapping = {
|
|
|
286
286
|
*/
|
|
287
287
|
error?: string | undefined;
|
|
288
288
|
};
|
|
289
|
+
export type ColorSpace = "rgb" | "hsl" | "oklch";
|
|
289
290
|
export type ColorTheme = {
|
|
290
291
|
/**
|
|
291
292
|
* - The color mapping.
|
|
@@ -297,4 +298,8 @@ export type ColorTheme = {
|
|
|
297
298
|
colors?: {
|
|
298
299
|
[x: string]: ColorPalette;
|
|
299
300
|
} | undefined;
|
|
301
|
+
/**
|
|
302
|
+
* - Color space for CSS variable values ('rgb', 'hsl', or 'oklch').
|
|
303
|
+
*/
|
|
304
|
+
colorSpace?: ColorSpace | undefined;
|
|
300
305
|
};
|
package/dist/utils.d.ts
CHANGED
|
@@ -79,22 +79,64 @@ export function getPathFromKey(key: string): string[];
|
|
|
79
79
|
export function getSnippet(obj: Object, key: string, defaultSnippet?: null | Function): Function | undefined;
|
|
80
80
|
export function resolveSnippet(snippets: any, proxy: any, fallback?: string): Function | null;
|
|
81
81
|
/**
|
|
82
|
-
* convert hex string to `{r}
|
|
82
|
+
* convert hex string to `{r},{g},{b}`
|
|
83
83
|
* @param {string} hex
|
|
84
84
|
* @return {string}
|
|
85
85
|
*/
|
|
86
86
|
export function hex2rgb(hex: string): string;
|
|
87
87
|
/**
|
|
88
|
-
* Convert
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
|
|
88
|
+
* Convert hex string to OKLCH components: `{L} {C} {H}`
|
|
89
|
+
* Uses CSS Color Level 4 two-step conversion via XYZ D65 for precision.
|
|
90
|
+
* @param {string} hex - 6-digit hex color (with or without #)
|
|
91
|
+
* @returns {string} space-separated OKLCH components for CSS `oklch(L C H / alpha)`
|
|
92
|
+
*/
|
|
93
|
+
export function hex2oklch(hex: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Convert OKLCH components back to hex string.
|
|
96
|
+
* Uses CSS Color Level 4 two-step conversion via XYZ D65 for precision.
|
|
97
|
+
* @param {number} L - Lightness 0-1
|
|
98
|
+
* @param {number} C - Chroma >= 0
|
|
99
|
+
* @param {number} H - Hue 0-360 degrees
|
|
100
|
+
* @returns {string} hex color string (#rrggbb)
|
|
101
|
+
*/
|
|
102
|
+
export function oklch2hex(L: number, C: number, H: number): string;
|
|
103
|
+
/**
|
|
104
|
+
* Convert hex string to HSL components: `{H} {S}% {L}%`
|
|
105
|
+
* @param {string} hex - 6-digit hex color (with or without #)
|
|
106
|
+
* @returns {string} space-separated HSL components for CSS `hsl(H S% L% / alpha)`
|
|
107
|
+
*/
|
|
108
|
+
export function hex2hsl(hex: string): string;
|
|
109
|
+
/** @typedef {'rgb' | 'hsl' | 'oklch'} ColorSpace */
|
|
110
|
+
/**
|
|
111
|
+
* Convert a hex color to the component format for a given color space.
|
|
112
|
+
* - rgb: "r,g,b" → for `rgba(var(--x), alpha)`
|
|
113
|
+
* - hsl: "H S% L%" → for `hsl(var(--x) / alpha)`
|
|
114
|
+
* - oklch: "L C H" → for `oklch(var(--x) / alpha)`
|
|
115
|
+
*
|
|
116
|
+
* @param {string} hex - 6-digit hex color (#rrggbb)
|
|
117
|
+
* @param {ColorSpace} space
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
export function hexToComponents(hex: string, space: ColorSpace): string;
|
|
121
|
+
/**
|
|
122
|
+
* Convert a CSS color value to components for use in CSS variables.
|
|
123
|
+
*
|
|
124
|
+
* - Hex (#rrggbb): converted to space-appropriate components via hexToComponents.
|
|
125
|
+
* - oklch(...) strings: inner components extracted (e.g. "0.58 0.15 35") so the
|
|
126
|
+
* value is compatible with the oklch(var(--x) / alpha) wrapper pattern.
|
|
127
|
+
* - All other formats: returned as-is (no opacity-modifier support).
|
|
93
128
|
*
|
|
94
129
|
* @param {unknown} value
|
|
130
|
+
* @param {ColorSpace} [space]
|
|
95
131
|
* @returns {unknown}
|
|
96
132
|
*/
|
|
97
|
-
export function colorToRgb(value: unknown): unknown;
|
|
133
|
+
export function colorToRgb(value: unknown, space?: ColorSpace): unknown;
|
|
134
|
+
/**
|
|
135
|
+
* Detects whether an icon value is a CSS class (i-*) or literal text (kanji, emoji, etc.)
|
|
136
|
+
* @param {string | null | undefined} icon
|
|
137
|
+
* @returns {boolean} true if icon is a CSS class string
|
|
138
|
+
*/
|
|
139
|
+
export function isIconClass(icon: string | null | undefined): boolean;
|
|
98
140
|
/**
|
|
99
141
|
* A utility function that detects if a string is an image URL or image data (base64)
|
|
100
142
|
*
|
|
@@ -102,3 +144,4 @@ export function colorToRgb(value: unknown): unknown;
|
|
|
102
144
|
* @returns {string|null} - Returns the original string if it's an image URL or image data, otherwise null
|
|
103
145
|
*/
|
|
104
146
|
export function getImage(str: string): string | null;
|
|
147
|
+
export type ColorSpace = "rgb" | "hsl" | "oklch";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Contains core utility functions and classes that can be used in various components.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"ramda": "^0.32.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@rokkit/icons": "1.0.
|
|
49
|
+
"@rokkit/icons": "1.0.4"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/colors/index.ts
CHANGED
package/src/constants.js
CHANGED
package/src/theme.ts
CHANGED
|
@@ -7,9 +7,21 @@ import { colorToRgb } from './utils'
|
|
|
7
7
|
const modifiers = {
|
|
8
8
|
hsl: (value) => `hsl(${value} / <alpha-value>)`,
|
|
9
9
|
rgb: (value) => `rgb(${value} / <alpha-value>)`,
|
|
10
|
+
oklch: (value) => `oklch(${value} / <alpha-value>)`,
|
|
10
11
|
none: (value) => value
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* CSS function wrappers for each color space, used by mapVariant().
|
|
16
|
+
* rgb uses legacy rgba() comma syntax for broad compatibility.
|
|
17
|
+
* hsl/oklch use modern space-separated syntax with / for alpha.
|
|
18
|
+
*/
|
|
19
|
+
const COLOR_SPACE_WRAPPERS = {
|
|
20
|
+
rgb: (varRef) => `rgba(${varRef},<alpha-value>)`,
|
|
21
|
+
hsl: (varRef) => `hsl(${varRef} / <alpha-value>)`,
|
|
22
|
+
oklch: (varRef) => `oklch(${varRef} / <alpha-value>)`
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
/**
|
|
14
26
|
* Generate shades for a color using css varuable
|
|
15
27
|
*
|
|
@@ -37,13 +49,14 @@ export function shadesOf(name, modifier = 'none') {
|
|
|
37
49
|
* @param {string} variant - The name of the variant to generate rules for.
|
|
38
50
|
* @param {Object} colors - The object containing color definitions.
|
|
39
51
|
* @param {Object} mapping - An object that maps variant names to color property names.
|
|
52
|
+
* @param {import('./utils').ColorSpace} [colorSpace] - Color space for component format.
|
|
40
53
|
* @returns {import('./types').ShadeMappings} An array containing the color rules for both light and dark modes.
|
|
41
54
|
*/
|
|
42
|
-
function generateColorRules(variant, colors, mapping) {
|
|
55
|
+
function generateColorRules(variant, colors, mapping, colorSpace) {
|
|
43
56
|
return ['DEFAULT', ...shades].flatMap((shade) => [
|
|
44
57
|
{
|
|
45
58
|
key: shade === 'DEFAULT' ? `--color-${variant}` : `--color-${variant}-${shade}`,
|
|
46
|
-
value: colorToRgb(colors[mapping[variant]][`${shade}`])
|
|
59
|
+
value: colorToRgb(colors[mapping[variant]][`${shade}`], colorSpace)
|
|
47
60
|
}
|
|
48
61
|
])
|
|
49
62
|
}
|
|
@@ -53,14 +66,15 @@ function generateColorRules(variant, colors, mapping) {
|
|
|
53
66
|
*
|
|
54
67
|
* @param {Object} [mapping=DEFAULT_THEME_MAPPING] - An object mapping variant names to color property names.
|
|
55
68
|
* @param {Object} [colors=defaultColors] - The object containing default color definitions.
|
|
69
|
+
* @param {import('./utils').ColorSpace} [colorSpace] - Color space for CSS variable values.
|
|
56
70
|
* @returns {Array<Array>} An array containing two arrays, one for the light theme variant and another for the dark theme.
|
|
57
71
|
*/
|
|
58
|
-
export function themeRules(mapping = DEFAULT_THEME_MAPPING, colors = defaultColors) {
|
|
72
|
+
export function themeRules(mapping = DEFAULT_THEME_MAPPING, colors = defaultColors, colorSpace) {
|
|
59
73
|
mapping = { ...DEFAULT_THEME_MAPPING, ...mapping }
|
|
60
74
|
colors = { ...defaultColors, ...colors }
|
|
61
75
|
const variants = Object.keys(mapping)
|
|
62
76
|
const rules = variants
|
|
63
|
-
.flatMap((variant) => generateColorRules(variant, colors, mapping))
|
|
77
|
+
.flatMap((variant) => generateColorRules(variant, colors, mapping, colorSpace))
|
|
64
78
|
.reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {})
|
|
65
79
|
|
|
66
80
|
return rules
|
|
@@ -144,20 +158,48 @@ export function contrastShortcuts(name) {
|
|
|
144
158
|
]
|
|
145
159
|
}
|
|
146
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Fallback chain for nullable color mappings.
|
|
163
|
+
* If a semantic color is null, it inherits from another semantic color.
|
|
164
|
+
*/
|
|
165
|
+
const COLOR_FALLBACKS = {
|
|
166
|
+
tertiary: 'primary',
|
|
167
|
+
secondary: 'primary',
|
|
168
|
+
accent: 'primary',
|
|
169
|
+
error: 'danger'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolves null values in a color mapping by following the fallback chain.
|
|
174
|
+
* @param {Record<string, string | null>} mapping
|
|
175
|
+
* @returns {Record<string, string>}
|
|
176
|
+
*/
|
|
177
|
+
function resolveColors(mapping) {
|
|
178
|
+
const resolved = { ...mapping }
|
|
179
|
+
for (const [key, fallbackKey] of Object.entries(COLOR_FALLBACKS)) {
|
|
180
|
+
if (resolved[key] === null || resolved[key] === undefined) {
|
|
181
|
+
resolved[key] = resolved[fallbackKey] ?? DEFAULT_THEME_MAPPING[fallbackKey]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return resolved
|
|
185
|
+
}
|
|
186
|
+
|
|
147
187
|
/**
|
|
148
188
|
* Theme class for managing color palettes, mappings, and semantic shortcuts.
|
|
149
189
|
*/
|
|
150
190
|
export class Theme {
|
|
151
191
|
#colors
|
|
152
192
|
#mapping
|
|
193
|
+
#colorSpace
|
|
153
194
|
|
|
154
195
|
/**
|
|
155
196
|
*
|
|
156
|
-
* @param {import('./types.js').ColorTheme} param0
|
|
197
|
+
* @param {import('./types.js').ColorTheme & { colorSpace?: import('./utils').ColorSpace }} param0
|
|
157
198
|
*/
|
|
158
|
-
constructor({ colors = defaultColors, mapping = DEFAULT_THEME_MAPPING } = {}) {
|
|
199
|
+
constructor({ colors = defaultColors, mapping = DEFAULT_THEME_MAPPING, colorSpace = 'rgb' } = {}) {
|
|
159
200
|
this.#colors = { ...defaultColors, ...colors }
|
|
160
|
-
this.#mapping = { ...DEFAULT_THEME_MAPPING, ...mapping }
|
|
201
|
+
this.#mapping = resolveColors({ ...DEFAULT_THEME_MAPPING, ...mapping })
|
|
202
|
+
this.#colorSpace = colorSpace
|
|
161
203
|
}
|
|
162
204
|
|
|
163
205
|
get colors() {
|
|
@@ -171,17 +213,25 @@ export class Theme {
|
|
|
171
213
|
return this.#mapping
|
|
172
214
|
}
|
|
173
215
|
set mapping(mapping) {
|
|
174
|
-
this.#mapping = { ...mapping }
|
|
216
|
+
this.#mapping = resolveColors({ ...mapping })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get colorSpace() {
|
|
220
|
+
return this.#colorSpace
|
|
221
|
+
}
|
|
222
|
+
set colorSpace(colorSpace) {
|
|
223
|
+
this.#colorSpace = colorSpace
|
|
175
224
|
}
|
|
176
225
|
|
|
177
226
|
mapVariant(color, variant) {
|
|
227
|
+
const wrap = COLOR_SPACE_WRAPPERS[this.#colorSpace] || COLOR_SPACE_WRAPPERS.rgb
|
|
178
228
|
return Object.keys(color).reduce(
|
|
179
229
|
(acc, key) => ({
|
|
180
230
|
...acc,
|
|
181
231
|
[key]:
|
|
182
232
|
key === 'DEFAULT'
|
|
183
|
-
? `
|
|
184
|
-
: `
|
|
233
|
+
? wrap(`var(--color-${variant})`)
|
|
234
|
+
: wrap(`var(--color-${variant}-${key})`)
|
|
185
235
|
}),
|
|
186
236
|
{}
|
|
187
237
|
)
|
|
@@ -203,7 +253,7 @@ export class Theme {
|
|
|
203
253
|
const useColors = { ...defaultColors, ...this.#colors }
|
|
204
254
|
const variants = Object.keys(useMapping)
|
|
205
255
|
const rules = variants
|
|
206
|
-
.flatMap((variant) => generateColorRules(variant, useColors, useMapping))
|
|
256
|
+
.flatMap((variant) => generateColorRules(variant, useColors, useMapping, this.#colorSpace))
|
|
207
257
|
.reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {})
|
|
208
258
|
return rules
|
|
209
259
|
}
|
package/src/types.js
CHANGED
|
@@ -152,9 +152,14 @@
|
|
|
152
152
|
* @property {string} [error] - The error color.
|
|
153
153
|
*/
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* @typedef {'rgb' | 'hsl' | 'oklch'} ColorSpace
|
|
157
|
+
*/
|
|
158
|
+
|
|
155
159
|
/**
|
|
156
160
|
* @typedef {Object} ColorTheme
|
|
157
161
|
* @property {ColorMapping} [mapping] - The color mapping.
|
|
158
162
|
* @property {ColorCollection} [colors] - The color collection.
|
|
163
|
+
* @property {ColorSpace} [colorSpace] - Color space for CSS variable values ('rgb', 'hsl', or 'oklch').
|
|
159
164
|
*/
|
|
160
165
|
export default {}
|
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}
|
|
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
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
219
|
-
|
|
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
|
*
|