@rokkit/ui 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jerry Thomas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/ui",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Data driven UI components for Rokkit applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,10 +43,10 @@
43
43
  "dropdown"
44
44
  ],
45
45
  "dependencies": {
46
- "@rokkit/core": "1.0.2",
47
- "@rokkit/data": "1.0.2",
48
- "@rokkit/states": "1.0.2",
49
- "@rokkit/actions": "1.0.2",
46
+ "@rokkit/core": "1.0.4",
47
+ "@rokkit/data": "1.0.4",
48
+ "@rokkit/states": "1.0.4",
49
+ "@rokkit/actions": "1.0.4",
50
50
  "marked": "^15.0.0",
51
51
  "dompurify": "^3.0.0"
52
52
  },
@@ -56,6 +56,7 @@
56
56
  if (!crossfilterWrapper) return tokens.map((t) => ({ type: 'token', token: t }))
57
57
 
58
58
  const result: Segment[] = []
59
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity
59
60
  const groupMap = new Map<string, { type: 'group'; id: string; items: Token[] }>()
60
61
 
61
62
  for (const token of tokens) {
@@ -1,4 +1,5 @@
1
1
  <script>
2
+ import { isIconClass } from '@rokkit/core'
2
3
  const { proxy, showIcon = true, showSubtext = true } = $props()
3
4
  </script>
4
5
 
@@ -6,7 +7,11 @@
6
7
  {#if proxy.get('avatar')}
7
8
  <img data-item-avatar src={proxy.get('avatar')} alt={proxy.get('tooltip') ?? proxy.label} />
8
9
  {:else if proxy.get('icon')}
9
- <span data-item-icon class={proxy.get('icon')} aria-hidden="true"></span>
10
+ {#if isIconClass(proxy.get('icon'))}
11
+ <span data-item-icon class={proxy.get('icon')} aria-hidden="true"></span>
12
+ {:else}
13
+ <span data-item-icon-literal aria-hidden="true">{proxy.get('icon')}</span>
14
+ {/if}
10
15
  {/if}
11
16
  {/if}
12
17
  {#if proxy.label || (showSubtext && proxy.get('subtext'))}
@@ -20,6 +20,14 @@ export interface HSL {
20
20
  l: number
21
21
  }
22
22
 
23
+ export interface OKLCH {
24
+ L: number
25
+ C: number
26
+ H: number
27
+ }
28
+
29
+ export type ColorSpace = 'rgb' | 'hsl' | 'oklch'
30
+
23
31
  export type ShadeKey =
24
32
  | '50'
25
33
  | '100'
@@ -163,6 +171,106 @@ export function hslToHex(hsl: HSL): string {
163
171
  return rgbToHex(hslToRgb(hsl))
164
172
  }
165
173
 
174
+ // =============================================================================
175
+ // OKLCH Conversion Utilities
176
+ // =============================================================================
177
+
178
+ function srgbToLinear(c: number): number {
179
+ const s = c / 255
180
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
181
+ }
182
+
183
+ function linearToSrgb(c: number): number {
184
+ const s = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055
185
+ return Math.round(Math.max(0, Math.min(255, s * 255)))
186
+ }
187
+
188
+ // CSS Color Level 4 matrices (via XYZ D65) for round-trip precision.
189
+ const SRGB_TO_XYZ: [number, number, number][] = [
190
+ [0.4123907993, 0.3575843394, 0.1804807884],
191
+ [0.2126390059, 0.7151686788, 0.0721923154],
192
+ [0.0193308187, 0.1191947798, 0.9505321522]
193
+ ]
194
+ const XYZ_TO_LMS: [number, number, number][] = [
195
+ [0.8189330101, 0.3618667424, -0.1288597137],
196
+ [0.0329845436, 0.9293118715, 0.0361456387],
197
+ [0.0482003018, 0.2643662691, 0.633851707]
198
+ ]
199
+ const LMS3_TO_OKLAB: [number, number, number][] = [
200
+ [0.2104542553, 0.793617785, -0.0040720468],
201
+ [1.9779984951, -2.428592205, 0.4505937099],
202
+ [0.0259040371, 0.7827717662, -0.808675766]
203
+ ]
204
+ const OKLAB_TO_LMS3: [number, number, number][] = [
205
+ [1.0, 0.3963377774, 0.2158037573],
206
+ [1.0, -0.1055613458, -0.0638541728],
207
+ [1.0, -0.0894841775, -1.291485548]
208
+ ]
209
+ const LMS_TO_XYZ: [number, number, number][] = [
210
+ [1.2270138511, -0.5577999807, 0.281256149],
211
+ [-0.0405801784, 1.1122568696, -0.0716766787],
212
+ [-0.0763812845, -0.4214819784, 1.5861632204]
213
+ ]
214
+ const XYZ_TO_SRGB: [number, number, number][] = [
215
+ [3.2409699419, -1.5373831776, -0.4986107603],
216
+ [-0.9692436363, 1.8759675015, 0.0415550574],
217
+ [0.0556300797, -0.2039769589, 1.0569715142]
218
+ ]
219
+
220
+ function matMul(m: [number, number, number][], v: number[]): number[] {
221
+ return m.map((row) => row[0] * v[0] + row[1] * v[1] + row[2] * v[2])
222
+ }
223
+
224
+ /**
225
+ * Convert RGB to OKLCH (via XYZ D65 for precision)
226
+ */
227
+ export function rgbToOklch(rgb: RGB): OKLCH {
228
+ const lin = [srgbToLinear(rgb.r), srgbToLinear(rgb.g), srgbToLinear(rgb.b)]
229
+ const xyz = matMul(SRGB_TO_XYZ, lin)
230
+ const lms = matMul(XYZ_TO_LMS, xyz)
231
+ const lms3 = [Math.cbrt(lms[0]), Math.cbrt(lms[1]), Math.cbrt(lms[2])]
232
+ const [L, a, b] = matMul(LMS3_TO_OKLAB, lms3)
233
+
234
+ const C = Math.sqrt(a * a + b * b)
235
+ let H = (Math.atan2(b, a) * 180) / Math.PI
236
+ if (H < 0) H += 360
237
+
238
+ return { L, C, H }
239
+ }
240
+
241
+ /**
242
+ * Convert OKLCH to RGB (via XYZ D65 for precision)
243
+ */
244
+ export function oklchToRgb(oklch: OKLCH): RGB {
245
+ const hRad = (oklch.H * Math.PI) / 180
246
+ const lab = [oklch.L, oklch.C * Math.cos(hRad), oklch.C * Math.sin(hRad)]
247
+
248
+ const lms3 = matMul(OKLAB_TO_LMS3, lab)
249
+ const lms = [lms3[0] ** 3, lms3[1] ** 3, lms3[2] ** 3]
250
+ const xyz = matMul(LMS_TO_XYZ, lms)
251
+ const lin = matMul(XYZ_TO_SRGB, xyz)
252
+
253
+ return {
254
+ r: linearToSrgb(lin[0]),
255
+ g: linearToSrgb(lin[1]),
256
+ b: linearToSrgb(lin[2])
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Convert hex to OKLCH
262
+ */
263
+ export function hexToOklch(hex: string): OKLCH {
264
+ return rgbToOklch(hexToRgb(hex))
265
+ }
266
+
267
+ /**
268
+ * Convert OKLCH to hex
269
+ */
270
+ export function oklchToHex(oklch: OKLCH): string {
271
+ return rgbToHex(oklchToRgb(oklch))
272
+ }
273
+
166
274
  // =============================================================================
167
275
  // Shade Generation
168
276
  // =============================================================================
@@ -523,13 +631,38 @@ export function getShades(color: string): Shades {
523
631
  // Runtime Palette Application
524
632
  // =============================================================================
525
633
 
634
+ /**
635
+ * Format a hex color as CSS variable components for the given color space.
636
+ */
637
+ function formatComponents(hex: string, colorSpace: ColorSpace): string {
638
+ switch (colorSpace) {
639
+ case 'hsl': {
640
+ const hsl = hexToHsl(hex)
641
+ return `${Math.round(hsl.h)} ${Math.round(hsl.s)}% ${Math.round(hsl.l)}%`
642
+ }
643
+ case 'oklch': {
644
+ const oklch = hexToOklch(hex)
645
+ const L = Math.round(oklch.L * 10000) / 10000
646
+ const C = Math.round(oklch.C * 10000) / 10000
647
+ const H = Math.round(oklch.H * 100) / 100
648
+ return `${L} ${C} ${H}`
649
+ }
650
+ case 'rgb':
651
+ default: {
652
+ const rgb = hexToRgb(hex)
653
+ return `${rgb.r},${rgb.g},${rgb.b}`
654
+ }
655
+ }
656
+ }
657
+
526
658
  /**
527
659
  * Apply a color palette to the document root
528
660
  * Sets CSS variables for each color role and shade
529
661
  */
530
662
  export function applyPalette(
531
663
  mapping: Partial<Record<ColorRole, string>>,
532
- element: HTMLElement = document.documentElement
664
+ element: HTMLElement = document.documentElement,
665
+ colorSpace: ColorSpace = 'rgb'
533
666
  ): void {
534
667
  for (const [role, color] of Object.entries(mapping)) {
535
668
  if (!color) continue
@@ -537,14 +670,11 @@ export function applyPalette(
537
670
  const shades = getShades(color)
538
671
 
539
672
  for (const [shade, hex] of Object.entries(shades)) {
540
- const rgb = hexToRgb(hex)
541
- // Store as RGB values (comma-separated) for rgba() compatibility
542
- element.style.setProperty(`--color-${role}-${shade}`, `${rgb.r},${rgb.g},${rgb.b}`)
673
+ element.style.setProperty(`--color-${role}-${shade}`, formatComponents(hex, colorSpace))
543
674
  }
544
675
 
545
676
  // Also set the default (500) as the base color variable
546
- const baseRgb = hexToRgb(shades['500'])
547
- element.style.setProperty(`--color-${role}`, `${baseRgb.r},${baseRgb.g},${baseRgb.b}`)
677
+ element.style.setProperty(`--color-${role}`, formatComponents(shades['500'], colorSpace))
548
678
  }
549
679
  }
550
680