@rokkit/core 1.0.3 → 1.0.5
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/color-space.d.ts +52 -0
- package/dist/constants.d.ts +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/theme.d.ts +19 -31
- package/dist/types.d.ts +5 -0
- package/dist/utils.d.ts +21 -10
- package/package.json +2 -2
- package/src/color-space.js +308 -0
- package/src/colors/index.ts +1 -0
- package/src/constants.js +5 -0
- package/src/index.js +1 -0
- package/src/theme.ts +65 -30
- package/src/types.js +5 -0
- package/src/utils.js +119 -13
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abstract base class for color space adapters.
|
|
3
|
+
*/
|
|
4
|
+
export class ColorSpace {
|
|
5
|
+
/**
|
|
6
|
+
* Factory method to create a ColorSpace adapter by name.
|
|
7
|
+
* @param {string} space - 'rgb', 'hsl', or 'oklch'
|
|
8
|
+
* @returns {ColorSpace}
|
|
9
|
+
*/
|
|
10
|
+
static create(space: string): ColorSpace;
|
|
11
|
+
/** @returns {string} */
|
|
12
|
+
get name(): string;
|
|
13
|
+
/** @returns {string} CSS color-mix interpolation space */
|
|
14
|
+
get mixSpace(): string;
|
|
15
|
+
/** @returns {string} CSS function name */
|
|
16
|
+
get fn(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Convert a hex color to bare channels in this space.
|
|
19
|
+
* @param {string} hex - #rrggbb
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
fromHex(_hex: any): string;
|
|
23
|
+
/**
|
|
24
|
+
* Wrap any input into a complete CSS color value.
|
|
25
|
+
* - hex → convert and wrap
|
|
26
|
+
* - bare channels → wrap in fn()
|
|
27
|
+
* - already wrapped → pass through
|
|
28
|
+
* - non-string / null / undefined → pass through
|
|
29
|
+
* - named colors → pass through
|
|
30
|
+
* @param {*} value
|
|
31
|
+
* @returns {*}
|
|
32
|
+
*/
|
|
33
|
+
wrap(_value: any): any;
|
|
34
|
+
/**
|
|
35
|
+
* Produce a color-mix expression for theme CSS variables with alpha support.
|
|
36
|
+
* @param {string} varName - CSS variable name (e.g. '--color-primary-500')
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
themeColor(varName: string): string;
|
|
40
|
+
}
|
|
41
|
+
export class RgbColorSpace extends ColorSpace {
|
|
42
|
+
fromHex(hex: any): string;
|
|
43
|
+
wrap(value: any): any;
|
|
44
|
+
}
|
|
45
|
+
export class HslColorSpace extends ColorSpace {
|
|
46
|
+
fromHex(hex: any): string;
|
|
47
|
+
wrap(value: any): any;
|
|
48
|
+
}
|
|
49
|
+
export class OklchColorSpace extends ColorSpace {
|
|
50
|
+
fromHex(hex: any): string;
|
|
51
|
+
wrap(value: any): any;
|
|
52
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -54,8 +54,10 @@ export namespace DEFAULT_KEYMAP {
|
|
|
54
54
|
}
|
|
55
55
|
export namespace DEFAULT_THEME_MAPPING {
|
|
56
56
|
let surface: string;
|
|
57
|
+
let ink: string;
|
|
57
58
|
let primary: string;
|
|
58
59
|
let secondary: string;
|
|
60
|
+
let tertiary: string;
|
|
59
61
|
let accent: string;
|
|
60
62
|
let success: string;
|
|
61
63
|
let warning: string;
|
|
@@ -76,5 +78,6 @@ export namespace TONE_MAP {
|
|
|
76
78
|
let z9: number;
|
|
77
79
|
let z10: number;
|
|
78
80
|
}
|
|
81
|
+
export const INVERTED_ROLES: Set<string>;
|
|
79
82
|
export const DEFAULT_STATE_ICONS: any;
|
|
80
83
|
export { defaultColors, syntaxColors, shades, defaultPalette } from "./colors/index";
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./utils.js";
|
|
|
4
4
|
export * from "./nested.js";
|
|
5
5
|
export * from "./string.js";
|
|
6
6
|
export * from "./theme.js";
|
|
7
|
+
export { ColorSpace } from "./color-space.js";
|
|
7
8
|
export { FieldMapper } from "./field-mapper.js";
|
|
8
9
|
export { createEmitter } from "./events.js";
|
|
9
10
|
export { getLineTypes } from "./connector.js";
|
package/dist/theme.d.ts
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generate shades for a color using css
|
|
2
|
+
* Generate shades for a color using css variable and a ColorSpace adapter.
|
|
3
3
|
*
|
|
4
4
|
* @param {string} name
|
|
5
|
-
* @param {string}
|
|
6
|
-
* @returns
|
|
5
|
+
* @param {string|import('./color-space').ColorSpace} space - color space name or adapter instance
|
|
6
|
+
* @returns {Record<string|number, string>}
|
|
7
7
|
*/
|
|
8
|
-
export declare function shadesOf(name: any,
|
|
9
|
-
DEFAULT:
|
|
8
|
+
export declare function shadesOf(name: any, space?: string): {
|
|
9
|
+
DEFAULT: string;
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
12
|
* Constructs and returns the light and dark theme variants based on provided color mapping and color definitions.
|
|
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 {string} [colorSpace] - Color space name 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;
|
|
21
|
+
ink: string;
|
|
20
22
|
primary: string;
|
|
21
23
|
secondary: string;
|
|
24
|
+
tertiary: string;
|
|
22
25
|
accent: string;
|
|
23
26
|
success: string;
|
|
24
27
|
warning: string;
|
|
25
28
|
danger: string;
|
|
26
29
|
error: string;
|
|
27
30
|
info: string;
|
|
28
|
-
}, colors
|
|
31
|
+
} | undefined, colors: any, colorSpace: any): {};
|
|
29
32
|
/**
|
|
30
33
|
* Generates UnoCSS shortcut definitions for semantic tones with bg, border, text.
|
|
31
34
|
* @param {string} name - Color name (e.g., 'primary')
|
|
@@ -49,14 +52,16 @@ export declare class Theme {
|
|
|
49
52
|
#private;
|
|
50
53
|
/**
|
|
51
54
|
*
|
|
52
|
-
* @param {import('./types.js').ColorTheme} param0
|
|
55
|
+
* @param {import('./types.js').ColorTheme & { colorSpace?: string }} param0
|
|
53
56
|
*/
|
|
54
|
-
constructor({ colors, mapping }?: {
|
|
57
|
+
constructor({ colors, mapping, colorSpace }?: {
|
|
55
58
|
colors?: any;
|
|
56
59
|
mapping?: {
|
|
57
60
|
surface: string;
|
|
61
|
+
ink: string;
|
|
58
62
|
primary: string;
|
|
59
63
|
secondary: string;
|
|
64
|
+
tertiary: string;
|
|
60
65
|
accent: string;
|
|
61
66
|
success: string;
|
|
62
67
|
warning: string;
|
|
@@ -64,31 +69,14 @@ export declare class Theme {
|
|
|
64
69
|
error: string;
|
|
65
70
|
info: string;
|
|
66
71
|
} | undefined;
|
|
72
|
+
colorSpace?: string | undefined;
|
|
67
73
|
});
|
|
68
74
|
get colors(): any;
|
|
69
75
|
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
|
-
});
|
|
76
|
+
get mapping(): any;
|
|
77
|
+
set mapping(mapping: any);
|
|
78
|
+
get colorSpace(): string;
|
|
79
|
+
set colorSpace(colorSpace: string);
|
|
92
80
|
mapVariant(color: any, variant: any): {};
|
|
93
81
|
getColorRules(mapping?: null): {};
|
|
94
82
|
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,33 @@ 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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
*
|
|
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
|
+
* Detects whether an icon value is a CSS class (i-*) or literal text (kanji, emoji, etc.)
|
|
105
|
+
* @param {string | null | undefined} icon
|
|
106
|
+
* @returns {boolean} true if icon is a CSS class string
|
|
96
107
|
*/
|
|
97
|
-
export function
|
|
108
|
+
export function isIconClass(icon: string | null | undefined): boolean;
|
|
98
109
|
/**
|
|
99
110
|
* A utility function that detects if a string is an image URL or image data (base64)
|
|
100
111
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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.5"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ColorSpace adapter strategy pattern.
|
|
3
|
+
*
|
|
4
|
+
* Each adapter knows how to convert hex colors to its own representation,
|
|
5
|
+
* wrap bare channels into a complete CSS color function, and produce
|
|
6
|
+
* `color-mix()` expressions for theme variables with alpha support.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── Private math helpers (copied from utils.js — not imported) ──────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert sRGB component (0-255) to linear RGB.
|
|
13
|
+
* @param {number} c - sRGB value 0-255
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
function srgbToLinear(c) {
|
|
17
|
+
const s = c / 255
|
|
18
|
+
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Linear sRGB → XYZ D65
|
|
22
|
+
const SRGB_TO_XYZ = [
|
|
23
|
+
[0.4123907993, 0.3575843394, 0.1804807884],
|
|
24
|
+
[0.2126390059, 0.7151686788, 0.0721923154],
|
|
25
|
+
[0.0193308187, 0.1191947798, 0.9505321522]
|
|
26
|
+
]
|
|
27
|
+
// XYZ D65 → LMS
|
|
28
|
+
const XYZ_TO_LMS = [
|
|
29
|
+
[0.8189330101, 0.3618667424, -0.1288597137],
|
|
30
|
+
[0.0329845436, 0.9293118715, 0.0361456387],
|
|
31
|
+
[0.0482003018, 0.2643662691, 0.633851707]
|
|
32
|
+
]
|
|
33
|
+
// LMS^(1/3) → OKLab
|
|
34
|
+
const LMS3_TO_OKLAB = [
|
|
35
|
+
[0.2104542553, 0.793617785, -0.0040720468],
|
|
36
|
+
[1.9779984951, -2.428592205, 0.4505937099],
|
|
37
|
+
[0.0259040371, 0.7827717662, -0.808675766]
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
/** @param {number[][]} m @param {number[]} v */
|
|
41
|
+
function matMul(m, v) {
|
|
42
|
+
return m.map((row) => row[0] * v[0] + row[1] * v[1] + row[2] * v[2])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Hex parsing helper ──────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parse a hex string (#rrggbb) into [r, g, b] integers.
|
|
49
|
+
* @param {string} hex
|
|
50
|
+
* @returns {number[]}
|
|
51
|
+
*/
|
|
52
|
+
function parseHex(hex) {
|
|
53
|
+
return hex.match(/\w\w/g).map((x) => parseInt(x, 16))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Hex → HSL conversion ───────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert hex to HSL bare channels: "H S% L%"
|
|
60
|
+
* @param {string} hex
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
function hexToHsl(hex) {
|
|
64
|
+
const [r, g, b] = parseHex(hex)
|
|
65
|
+
const rn = r / 255
|
|
66
|
+
const gn = g / 255
|
|
67
|
+
const bn = b / 255
|
|
68
|
+
const max = Math.max(rn, gn, bn)
|
|
69
|
+
const min = Math.min(rn, gn, bn)
|
|
70
|
+
const l = (max + min) / 2
|
|
71
|
+
|
|
72
|
+
if (max === min) return `0 0% ${Math.round(l * 100)}%`
|
|
73
|
+
|
|
74
|
+
const d = max - min
|
|
75
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
76
|
+
|
|
77
|
+
let h = 0
|
|
78
|
+
if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6
|
|
79
|
+
else if (max === gn) h = ((bn - rn) / d + 2) / 6
|
|
80
|
+
else h = ((rn - gn) / d + 4) / 6
|
|
81
|
+
|
|
82
|
+
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Hex → OKLCH conversion ─────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Convert hex to OKLCH bare channels: "L C H"
|
|
89
|
+
* @param {string} hex
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function hexToOklch(hex) {
|
|
93
|
+
const [r, g, b] = parseHex(hex)
|
|
94
|
+
const lin = [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)]
|
|
95
|
+
|
|
96
|
+
const xyz = matMul(SRGB_TO_XYZ, lin)
|
|
97
|
+
const lms = matMul(XYZ_TO_LMS, xyz)
|
|
98
|
+
const lms3 = [Math.cbrt(lms[0]), Math.cbrt(lms[1]), Math.cbrt(lms[2])]
|
|
99
|
+
const [L, a, bk] = matMul(LMS3_TO_OKLAB, lms3)
|
|
100
|
+
|
|
101
|
+
const C = Math.sqrt(a * a + bk * bk)
|
|
102
|
+
let H = (Math.atan2(bk, a) * 180) / Math.PI
|
|
103
|
+
if (H < 0) H += 360
|
|
104
|
+
|
|
105
|
+
const Lr = Math.round(L * 1000000) / 1000000
|
|
106
|
+
const Cr = Math.round(C * 1000000) / 1000000
|
|
107
|
+
const Hr = Math.round(H * 10000) / 10000
|
|
108
|
+
|
|
109
|
+
return `${Lr} ${Cr} ${Hr}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Hex regex ───────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const HEX_RE = /^#[0-9a-fA-F]{6}$/
|
|
115
|
+
|
|
116
|
+
// ── Base class ──────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Abstract base class for color space adapters.
|
|
120
|
+
*/
|
|
121
|
+
export class ColorSpace {
|
|
122
|
+
/** @returns {string} */
|
|
123
|
+
get name() {
|
|
124
|
+
throw new Error('Subclass must implement name')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @returns {string} CSS color-mix interpolation space */
|
|
128
|
+
get mixSpace() {
|
|
129
|
+
throw new Error('Subclass must implement mixSpace')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** @returns {string} CSS function name */
|
|
133
|
+
get fn() {
|
|
134
|
+
throw new Error('Subclass must implement fn')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Convert a hex color to bare channels in this space.
|
|
139
|
+
* @param {string} hex - #rrggbb
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
fromHex(_hex) {
|
|
143
|
+
throw new Error('Subclass must implement fromHex')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Wrap any input into a complete CSS color value.
|
|
148
|
+
* - hex → convert and wrap
|
|
149
|
+
* - bare channels → wrap in fn()
|
|
150
|
+
* - already wrapped → pass through
|
|
151
|
+
* - non-string / null / undefined → pass through
|
|
152
|
+
* - named colors → pass through
|
|
153
|
+
* @param {*} value
|
|
154
|
+
* @returns {*}
|
|
155
|
+
*/
|
|
156
|
+
wrap(_value) {
|
|
157
|
+
throw new Error('Subclass must implement wrap')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Produce a color-mix expression for theme CSS variables with alpha support.
|
|
162
|
+
* @param {string} varName - CSS variable name (e.g. '--color-primary-500')
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
themeColor(varName) {
|
|
166
|
+
return `color-mix(in ${this.mixSpace}, var(${varName}) calc(<alpha-value> * 100%), transparent)`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Factory method to create a ColorSpace adapter by name.
|
|
171
|
+
* @param {string} space - 'rgb', 'hsl', or 'oklch'
|
|
172
|
+
* @returns {ColorSpace}
|
|
173
|
+
*/
|
|
174
|
+
static create(space) {
|
|
175
|
+
switch (space) {
|
|
176
|
+
case 'rgb':
|
|
177
|
+
return new RgbColorSpace()
|
|
178
|
+
case 'hsl':
|
|
179
|
+
return new HslColorSpace()
|
|
180
|
+
case 'oklch':
|
|
181
|
+
return new OklchColorSpace()
|
|
182
|
+
default:
|
|
183
|
+
throw new Error(`Unknown color space: ${space}`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── RGB adapter ─────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export class RgbColorSpace extends ColorSpace {
|
|
191
|
+
get name() {
|
|
192
|
+
return 'rgb'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
get mixSpace() {
|
|
196
|
+
return 'srgb'
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get fn() {
|
|
200
|
+
return 'rgb'
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fromHex(hex) {
|
|
204
|
+
const [r, g, b] = parseHex(hex)
|
|
205
|
+
return `${r}, ${g}, ${b}`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
wrap(value) {
|
|
209
|
+
if (typeof value !== 'string') return value
|
|
210
|
+
|
|
211
|
+
// Already wrapped
|
|
212
|
+
if (value.startsWith('rgb(') || value.startsWith('rgba(')) return value
|
|
213
|
+
|
|
214
|
+
// Hex
|
|
215
|
+
if (HEX_RE.test(value)) return `rgb(${this.fromHex(value)})`
|
|
216
|
+
|
|
217
|
+
// Bare channels: comma-separated or space-separated digits
|
|
218
|
+
// Comma-separated: "255,0,0" or "255, 0, 0"
|
|
219
|
+
if (/^\d+\s*,\s*\d+\s*,\s*\d+$/.test(value)) {
|
|
220
|
+
const parts = value.split(',').map((s) => s.trim())
|
|
221
|
+
return `rgb(${parts.join(', ')})`
|
|
222
|
+
}
|
|
223
|
+
// Space-separated: "255 0 0"
|
|
224
|
+
if (/^\d+\s+\d+\s+\d+$/.test(value)) {
|
|
225
|
+
const parts = value.trim().split(/\s+/)
|
|
226
|
+
return `rgb(${parts.join(', ')})`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Named color or other — pass through
|
|
230
|
+
return value
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── HSL adapter ─────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
export class HslColorSpace extends ColorSpace {
|
|
237
|
+
get name() {
|
|
238
|
+
return 'hsl'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
get mixSpace() {
|
|
242
|
+
return 'srgb'
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
get fn() {
|
|
246
|
+
return 'hsl'
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fromHex(hex) {
|
|
250
|
+
return hexToHsl(hex)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
wrap(value) {
|
|
254
|
+
if (typeof value !== 'string') return value
|
|
255
|
+
|
|
256
|
+
// Already wrapped
|
|
257
|
+
if (value.startsWith('hsl(') || value.startsWith('hsla(')) return value
|
|
258
|
+
|
|
259
|
+
// Hex
|
|
260
|
+
if (HEX_RE.test(value)) return `hsl(${this.fromHex(value)})`
|
|
261
|
+
|
|
262
|
+
// Bare channels: "H S% L%" pattern (number, space, number%, space, number%)
|
|
263
|
+
if (/^\d+\s+\d+%\s+\d+%$/.test(value)) {
|
|
264
|
+
return `hsl(${value})`
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Named color or other — pass through
|
|
268
|
+
return value
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── OKLCH adapter ───────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
export class OklchColorSpace extends ColorSpace {
|
|
275
|
+
get name() {
|
|
276
|
+
return 'oklch'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
get mixSpace() {
|
|
280
|
+
return 'oklch'
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get fn() {
|
|
284
|
+
return 'oklch'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fromHex(hex) {
|
|
288
|
+
return hexToOklch(hex)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
wrap(value) {
|
|
292
|
+
if (typeof value !== 'string') return value
|
|
293
|
+
|
|
294
|
+
// Already wrapped
|
|
295
|
+
if (value.startsWith('oklch(')) return value
|
|
296
|
+
|
|
297
|
+
// Hex
|
|
298
|
+
if (HEX_RE.test(value)) return `oklch(${this.fromHex(value)})`
|
|
299
|
+
|
|
300
|
+
// Bare channels: three space-separated numbers (may contain decimals)
|
|
301
|
+
if (/^[\d.]+\s+[\d.]+\s+[\d.]+$/.test(value)) {
|
|
302
|
+
return `oklch(${value})`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Named color or other — pass through
|
|
306
|
+
return value
|
|
307
|
+
}
|
|
308
|
+
}
|
package/src/colors/index.ts
CHANGED
package/src/constants.js
CHANGED
|
@@ -154,8 +154,10 @@ export const DEFAULT_KEYMAP = {
|
|
|
154
154
|
|
|
155
155
|
export const DEFAULT_THEME_MAPPING = {
|
|
156
156
|
surface: 'slate',
|
|
157
|
+
ink: 'slate',
|
|
157
158
|
primary: 'orange',
|
|
158
159
|
secondary: 'pink',
|
|
160
|
+
tertiary: 'violet',
|
|
159
161
|
accent: 'sky',
|
|
160
162
|
success: 'green',
|
|
161
163
|
warning: 'yellow',
|
|
@@ -177,6 +179,9 @@ export const TONE_MAP = {
|
|
|
177
179
|
z9: 900,
|
|
178
180
|
z10: 950
|
|
179
181
|
}
|
|
182
|
+
|
|
183
|
+
export const INVERTED_ROLES = new Set(['ink'])
|
|
184
|
+
|
|
180
185
|
/**
|
|
181
186
|
* Splits an icon name into its group and key components.
|
|
182
187
|
* @param {string} name - The icon name to split.
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export * from './nested.js'
|
|
|
10
10
|
export * from './string.js'
|
|
11
11
|
// skipcq: JS-E1004 - Needed for exposing all functions
|
|
12
12
|
export * from './theme.js'
|
|
13
|
+
export { ColorSpace } from './color-space.js'
|
|
13
14
|
export { FieldMapper } from './field-mapper.js'
|
|
14
15
|
export { getItemAtIndex, getIndexForItem } from './mapped-items.js'
|
|
15
16
|
export { createEmitter } from './events.js'
|
package/src/theme.ts
CHANGED
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
2
2
|
// @ts-nocheck
|
|
3
|
-
import { DEFAULT_THEME_MAPPING, defaultColors, TONE_MAP } from './constants'
|
|
3
|
+
import { DEFAULT_THEME_MAPPING, defaultColors, TONE_MAP, INVERTED_ROLES } from './constants'
|
|
4
4
|
import { shades } from './colors/index'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
const modifiers = {
|
|
8
|
-
hsl: (value) => `hsl(${value} / <alpha-value>)`,
|
|
9
|
-
rgb: (value) => `rgb(${value} / <alpha-value>)`,
|
|
10
|
-
none: (value) => value
|
|
11
|
-
}
|
|
5
|
+
import { ColorSpace } from './color-space'
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
|
-
* Generate shades for a color using css
|
|
8
|
+
* Generate shades for a color using css variable and a ColorSpace adapter.
|
|
15
9
|
*
|
|
16
10
|
* @param {string} name
|
|
17
|
-
* @param {string}
|
|
18
|
-
* @returns
|
|
11
|
+
* @param {string|import('./color-space').ColorSpace} space - color space name or adapter instance
|
|
12
|
+
* @returns {Record<string|number, string>}
|
|
19
13
|
*/
|
|
20
|
-
export function shadesOf(name,
|
|
21
|
-
const
|
|
14
|
+
export function shadesOf(name, space = 'rgb') {
|
|
15
|
+
const adapter = typeof space === 'string' ? ColorSpace.create(space) : space
|
|
22
16
|
|
|
23
17
|
return shades.reduce(
|
|
24
18
|
(result, shade) => ({
|
|
25
19
|
...result,
|
|
26
|
-
[shade]:
|
|
20
|
+
[shade]: adapter.themeColor(`--color-${name}-${shade}`)
|
|
27
21
|
}),
|
|
28
22
|
{
|
|
29
|
-
DEFAULT:
|
|
23
|
+
DEFAULT: adapter.themeColor(`--color-${name}-500`)
|
|
30
24
|
}
|
|
31
25
|
)
|
|
32
26
|
}
|
|
@@ -37,13 +31,14 @@ export function shadesOf(name, modifier = 'none') {
|
|
|
37
31
|
* @param {string} variant - The name of the variant to generate rules for.
|
|
38
32
|
* @param {Object} colors - The object containing color definitions.
|
|
39
33
|
* @param {Object} mapping - An object that maps variant names to color property names.
|
|
34
|
+
* @param {import('./color-space').ColorSpace} adapter - ColorSpace adapter instance.
|
|
40
35
|
* @returns {import('./types').ShadeMappings} An array containing the color rules for both light and dark modes.
|
|
41
36
|
*/
|
|
42
|
-
function generateColorRules(variant, colors, mapping) {
|
|
37
|
+
function generateColorRules(variant, colors, mapping, adapter) {
|
|
43
38
|
return ['DEFAULT', ...shades].flatMap((shade) => [
|
|
44
39
|
{
|
|
45
40
|
key: shade === 'DEFAULT' ? `--color-${variant}` : `--color-${variant}-${shade}`,
|
|
46
|
-
value:
|
|
41
|
+
value: adapter.wrap(colors[mapping[variant]][`${shade}`])
|
|
47
42
|
}
|
|
48
43
|
])
|
|
49
44
|
}
|
|
@@ -53,14 +48,16 @@ function generateColorRules(variant, colors, mapping) {
|
|
|
53
48
|
*
|
|
54
49
|
* @param {Object} [mapping=DEFAULT_THEME_MAPPING] - An object mapping variant names to color property names.
|
|
55
50
|
* @param {Object} [colors=defaultColors] - The object containing default color definitions.
|
|
51
|
+
* @param {string} [colorSpace] - Color space name for CSS variable values.
|
|
56
52
|
* @returns {Array<Array>} An array containing two arrays, one for the light theme variant and another for the dark theme.
|
|
57
53
|
*/
|
|
58
|
-
export function themeRules(mapping = DEFAULT_THEME_MAPPING, colors = defaultColors) {
|
|
54
|
+
export function themeRules(mapping = DEFAULT_THEME_MAPPING, colors = defaultColors, colorSpace) {
|
|
59
55
|
mapping = { ...DEFAULT_THEME_MAPPING, ...mapping }
|
|
60
56
|
colors = { ...defaultColors, ...colors }
|
|
57
|
+
const adapter = ColorSpace.create(colorSpace || 'rgb')
|
|
61
58
|
const variants = Object.keys(mapping)
|
|
62
59
|
const rules = variants
|
|
63
|
-
.flatMap((variant) => generateColorRules(variant, colors, mapping))
|
|
60
|
+
.flatMap((variant) => generateColorRules(variant, colors, mapping, adapter))
|
|
64
61
|
.reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {})
|
|
65
62
|
|
|
66
63
|
return rules
|
|
@@ -144,20 +141,49 @@ export function contrastShortcuts(name) {
|
|
|
144
141
|
]
|
|
145
142
|
}
|
|
146
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Fallback chain for nullable color mappings.
|
|
146
|
+
* If a semantic color is null, it inherits from another semantic color.
|
|
147
|
+
*/
|
|
148
|
+
const COLOR_FALLBACKS = {
|
|
149
|
+
ink: 'surface',
|
|
150
|
+
tertiary: 'primary',
|
|
151
|
+
secondary: 'primary',
|
|
152
|
+
accent: 'primary',
|
|
153
|
+
error: 'danger'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolves null values in a color mapping by following the fallback chain.
|
|
158
|
+
* @param {Record<string, string | null>} mapping
|
|
159
|
+
* @returns {Record<string, string>}
|
|
160
|
+
*/
|
|
161
|
+
function resolveColors(mapping) {
|
|
162
|
+
const resolved = { ...mapping }
|
|
163
|
+
for (const [key, fallbackKey] of Object.entries(COLOR_FALLBACKS)) {
|
|
164
|
+
if (resolved[key] === null || resolved[key] === undefined) {
|
|
165
|
+
resolved[key] = resolved[fallbackKey] ?? DEFAULT_THEME_MAPPING[fallbackKey]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return resolved
|
|
169
|
+
}
|
|
170
|
+
|
|
147
171
|
/**
|
|
148
172
|
* Theme class for managing color palettes, mappings, and semantic shortcuts.
|
|
149
173
|
*/
|
|
150
174
|
export class Theme {
|
|
151
175
|
#colors
|
|
152
176
|
#mapping
|
|
177
|
+
#adapter
|
|
153
178
|
|
|
154
179
|
/**
|
|
155
180
|
*
|
|
156
|
-
* @param {import('./types.js').ColorTheme} param0
|
|
181
|
+
* @param {import('./types.js').ColorTheme & { colorSpace?: string }} param0
|
|
157
182
|
*/
|
|
158
|
-
constructor({ colors = defaultColors, mapping = DEFAULT_THEME_MAPPING } = {}) {
|
|
183
|
+
constructor({ colors = defaultColors, mapping = DEFAULT_THEME_MAPPING, colorSpace = 'rgb' } = {}) {
|
|
159
184
|
this.#colors = { ...defaultColors, ...colors }
|
|
160
|
-
this.#mapping = { ...DEFAULT_THEME_MAPPING, ...mapping }
|
|
185
|
+
this.#mapping = resolveColors({ ...DEFAULT_THEME_MAPPING, ...mapping })
|
|
186
|
+
this.#adapter = ColorSpace.create(colorSpace)
|
|
161
187
|
}
|
|
162
188
|
|
|
163
189
|
get colors() {
|
|
@@ -171,7 +197,14 @@ export class Theme {
|
|
|
171
197
|
return this.#mapping
|
|
172
198
|
}
|
|
173
199
|
set mapping(mapping) {
|
|
174
|
-
this.#mapping = { ...mapping }
|
|
200
|
+
this.#mapping = resolveColors({ ...mapping })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
get colorSpace() {
|
|
204
|
+
return this.#adapter.name
|
|
205
|
+
}
|
|
206
|
+
set colorSpace(colorSpace) {
|
|
207
|
+
this.#adapter = ColorSpace.create(colorSpace)
|
|
175
208
|
}
|
|
176
209
|
|
|
177
210
|
mapVariant(color, variant) {
|
|
@@ -180,8 +213,8 @@ export class Theme {
|
|
|
180
213
|
...acc,
|
|
181
214
|
[key]:
|
|
182
215
|
key === 'DEFAULT'
|
|
183
|
-
?
|
|
184
|
-
:
|
|
216
|
+
? this.#adapter.themeColor(`--color-${variant}`)
|
|
217
|
+
: this.#adapter.themeColor(`--color-${variant}-${key}`)
|
|
185
218
|
}),
|
|
186
219
|
{}
|
|
187
220
|
)
|
|
@@ -203,7 +236,7 @@ export class Theme {
|
|
|
203
236
|
const useColors = { ...defaultColors, ...this.#colors }
|
|
204
237
|
const variants = Object.keys(useMapping)
|
|
205
238
|
const rules = variants
|
|
206
|
-
.flatMap((variant) => generateColorRules(variant, useColors, useMapping))
|
|
239
|
+
.flatMap((variant) => generateColorRules(variant, useColors, useMapping, this.#adapter))
|
|
207
240
|
.reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {})
|
|
208
241
|
return rules
|
|
209
242
|
}
|
|
@@ -226,15 +259,17 @@ export class Theme {
|
|
|
226
259
|
const names = Object.keys(this.#mapping)
|
|
227
260
|
|
|
228
261
|
const lightLines = names.flatMap((name) =>
|
|
229
|
-
Object.entries(TONE_MAP).map(
|
|
230
|
-
(
|
|
231
|
-
|
|
262
|
+
Object.entries(TONE_MAP).map(([zone, light]) => {
|
|
263
|
+
const value = INVERTED_ROLES.has(name) ? 1000 - light : light
|
|
264
|
+
return ` --color-${name}-${zone}: var(--color-${name}-${value});`
|
|
265
|
+
})
|
|
232
266
|
)
|
|
233
267
|
|
|
234
268
|
const darkLines = names.flatMap((name) =>
|
|
235
269
|
Object.entries(TONE_MAP).map(([zone, light]) => {
|
|
236
270
|
const dark = 1000 - light
|
|
237
|
-
|
|
271
|
+
const value = INVERTED_ROLES.has(name) ? light : dark
|
|
272
|
+
return ` --color-${name}-${zone}: var(--color-${name}-${value});`
|
|
238
273
|
})
|
|
239
274
|
)
|
|
240
275
|
|
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,20 +205,126 @@ export function hex2rgb(hex) {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
|
-
* Convert
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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)`
|
|
216
275
|
*/
|
|
217
|
-
export function
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
220
316
|
}
|
|
221
|
-
return
|
|
317
|
+
return `#${toHex(lin[0])}${toHex(lin[1])}${toHex(lin[2])}`
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Detects whether an icon value is a CSS class (i-*) or literal text (kanji, emoji, etc.)
|
|
322
|
+
* @param {string | null | undefined} icon
|
|
323
|
+
* @returns {boolean} true if icon is a CSS class string
|
|
324
|
+
*/
|
|
325
|
+
export function isIconClass(icon) {
|
|
326
|
+
if (!icon || typeof icon !== 'string') return false
|
|
327
|
+
return icon.startsWith('i-')
|
|
222
328
|
}
|
|
223
329
|
|
|
224
330
|
/**
|