@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.
@@ -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
+ }
@@ -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 varuable
2
+ * Generate shades for a color using css variable and a ColorSpace adapter.
3
3
  *
4
4
  * @param {string} name
5
- * @param {string} modifier
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, modifier?: string): {
9
- DEFAULT: any;
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?: any): {};
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
- surface: string;
72
- primary: string;
73
- secondary: string;
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} {g} {b}`
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 a CSS color value to r,g,b for use in CSS variables.
89
- * Hex values (#rrggbb) are converted to "r,g,b" for rgba() support.
90
- * All other CSS color formats (oklch, hsl, named) are returned as-is.
91
- * Non-string values are returned unchanged.
92
- * Note: non-hex values will NOT work with UnoCSS opacity utilities like bg-primary/50.
93
- *
94
- * @param {unknown} value
95
- * @returns {unknown}
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 colorToRgb(value: unknown): unknown;
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",
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.3"
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
+ }
@@ -10,6 +10,7 @@ export const defaultPalette = [
10
10
  'surface',
11
11
  'primary',
12
12
  'secondary',
13
+ 'tertiary',
13
14
  'accent',
14
15
  'success',
15
16
  'warning',
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 { colorToRgb } from './utils'
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 varuable
8
+ * Generate shades for a color using css variable and a ColorSpace adapter.
15
9
  *
16
10
  * @param {string} name
17
- * @param {string} modifier
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, modifier = 'none') {
21
- const fn = modifier in modifiers ? modifiers[modifier] : modifiers.none
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]: fn(`var(--color-${name}-${shade})`)
20
+ [shade]: adapter.themeColor(`--color-${name}-${shade}`)
27
21
  }),
28
22
  {
29
- DEFAULT: fn(`var(--color-${name}-500)`)
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: colorToRgb(colors[mapping[variant]][`${shade}`])
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
- ? `rgba(var(--color-${variant}),<alpha-value>)`
184
- : `rgba(var(--color-${variant}-${key}),<alpha-value>)`
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
- ([zone, light]) => ` --color-${name}-${zone}: var(--color-${name}-${light});`
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
- return ` --color-${name}-${zone}: var(--color-${name}-${dark});`
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} {g} {b}`
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 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.
213
- *
214
- * @param {unknown} value
215
- * @returns {unknown}
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 colorToRgb(value) {
218
- if (typeof value === 'string' && /^#[0-9a-fA-F]{6}$/.test(value)) {
219
- return hex2rgb(value)
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 value
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
  /**