@rokkit/ui 1.0.0-next.98 → 1.0.1

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.
Files changed (81) hide show
  1. package/README.md +159 -1
  2. package/package.json +52 -53
  3. package/src/MarkdownRenderer.svelte +106 -0
  4. package/src/components/AlertList.svelte +59 -0
  5. package/src/components/Avatar.svelte +68 -0
  6. package/src/components/Badge.svelte +57 -0
  7. package/src/components/BreadCrumbs.svelte +94 -0
  8. package/src/components/Button.svelte +91 -0
  9. package/src/components/ButtonGroup.svelte +9 -0
  10. package/src/components/Card.svelte +71 -0
  11. package/src/components/Carousel.svelte +169 -0
  12. package/src/components/Code.svelte +189 -0
  13. package/src/components/Connector.svelte +46 -0
  14. package/src/components/Divider.svelte +24 -0
  15. package/src/components/Dropdown.svelte +202 -0
  16. package/src/components/FloatingAction.svelte +367 -0
  17. package/src/components/FloatingNavigation.svelte +256 -0
  18. package/src/components/Grid.svelte +128 -0
  19. package/src/components/ItemContent.svelte +25 -0
  20. package/src/components/LazyTree.svelte +171 -0
  21. package/src/components/List.svelte +224 -0
  22. package/src/components/Menu.svelte +270 -0
  23. package/src/components/Message.svelte +75 -0
  24. package/src/components/MultiSelect.svelte +367 -0
  25. package/src/components/PaletteManager.svelte +360 -0
  26. package/src/components/Pill.svelte +83 -0
  27. package/src/components/ProgressBar.svelte +31 -0
  28. package/src/components/Range.svelte +346 -0
  29. package/src/components/Rating.svelte +115 -0
  30. package/src/components/Reveal.svelte +46 -0
  31. package/src/components/SearchFilter.svelte +88 -0
  32. package/src/components/Select.svelte +424 -0
  33. package/src/components/Shine.svelte +87 -0
  34. package/src/components/Stack.svelte +38 -0
  35. package/src/components/StatusList.svelte +18 -0
  36. package/src/components/Stepper.svelte +174 -0
  37. package/src/components/Swatch.svelte +102 -0
  38. package/src/components/Switch.svelte +71 -0
  39. package/src/components/Table.svelte +245 -0
  40. package/src/components/Tabs.svelte +202 -0
  41. package/src/components/Tilt.svelte +68 -0
  42. package/src/components/Timeline.svelte +61 -0
  43. package/src/components/Toggle.svelte +93 -0
  44. package/src/components/Toolbar.svelte +306 -0
  45. package/src/components/ToolbarGroup.svelte +17 -0
  46. package/src/components/Tooltip.svelte +121 -0
  47. package/src/components/Tree.svelte +143 -0
  48. package/src/components/UploadFileStatus.svelte +102 -0
  49. package/src/components/UploadProgress.svelte +127 -0
  50. package/src/components/UploadTarget.svelte +126 -0
  51. package/src/components/index.ts +48 -0
  52. package/src/index.ts +59 -0
  53. package/src/markdown-plugin.ts +12 -0
  54. package/src/types/button.ts +86 -0
  55. package/src/types/code.ts +46 -0
  56. package/src/types/floating-action.ts +123 -0
  57. package/src/types/floating-navigation.ts +80 -0
  58. package/src/types/index.ts +58 -0
  59. package/src/types/list.ts +200 -0
  60. package/src/types/menu.ts +95 -0
  61. package/src/types/palette.ts +160 -0
  62. package/src/types/range.ts +51 -0
  63. package/src/types/search-filter.ts +67 -0
  64. package/src/types/select.ts +176 -0
  65. package/src/types/switch.ts +68 -0
  66. package/src/types/table.ts +219 -0
  67. package/src/types/tabs.ts +103 -0
  68. package/src/types/timeline.ts +53 -0
  69. package/src/types/toggle.ts +68 -0
  70. package/src/types/toolbar.ts +167 -0
  71. package/src/types/tooltip.ts +21 -0
  72. package/src/types/tree.ts +250 -0
  73. package/src/types/upload-file-status.ts +45 -0
  74. package/src/types/upload-progress.ts +111 -0
  75. package/src/types/upload-target.ts +71 -0
  76. package/src/utils/palette.ts +618 -0
  77. package/src/utils/shiki.ts +114 -0
  78. package/src/utils/upload.js +145 -0
  79. package/LICENSE +0 -21
  80. package/src/index.js +0 -8
  81. package/src/wrappers.js +0 -2
@@ -0,0 +1,71 @@
1
+ /**
2
+ * UploadTarget Component Types
3
+ *
4
+ * Drop zone with file validation. Accepts drag-and-drop or click-to-browse.
5
+ * Validates files against accept (MIME/extension) and maxSize constraints.
6
+ */
7
+
8
+ import type { Snippet } from 'svelte'
9
+
10
+ // =============================================================================
11
+ // Item Types
12
+ // =============================================================================
13
+
14
+ /**
15
+ * File validation error returned via onerror callback
16
+ */
17
+ export interface UploadError {
18
+ /** The file that failed validation */
19
+ file: File
20
+ /** Why the file was rejected */
21
+ reason: 'type' | 'size'
22
+ }
23
+
24
+ // =============================================================================
25
+ // Snippet Types
26
+ // =============================================================================
27
+
28
+ /**
29
+ * Snippet for custom drop zone content.
30
+ * Receives the current dragging state.
31
+ */
32
+ export type UploadTargetContentSnippet = Snippet<[dragging: boolean]>
33
+
34
+ // =============================================================================
35
+ // Component Props
36
+ // =============================================================================
37
+
38
+ /**
39
+ * Props for the UploadTarget component
40
+ */
41
+ export interface UploadTargetProps {
42
+ /** Accepted file types (MIME types or extensions, comma-separated) */
43
+ accept?: string
44
+
45
+ /** Maximum file size in bytes */
46
+ maxSize?: number
47
+
48
+ /** Allow multiple file selection */
49
+ multiple?: boolean
50
+
51
+ /** Disable the drop zone */
52
+ disabled?: boolean
53
+
54
+ /** Label overrides merged with messages.uploadTarget */
55
+ labels?: Record<string, string>
56
+
57
+ /** Called with validated files after drop or browse */
58
+ onfiles?: (files: File[]) => void
59
+
60
+ /** Called for each file that fails validation */
61
+ onerror?: (err: UploadError) => void
62
+
63
+ /** Size variant — full (default) fills available area; small shows only border + icon */
64
+ size?: 'full' | 'small'
65
+
66
+ /** Additional CSS classes */
67
+ class?: string
68
+
69
+ /** Custom content snippet (replaces default drop zone UI) */
70
+ content?: UploadTargetContentSnippet
71
+ }
@@ -0,0 +1,618 @@
1
+ /**
2
+ * Palette Utilities
3
+ *
4
+ * Functions for color manipulation, shade generation, and runtime palette application.
5
+ */
6
+
7
+ // =============================================================================
8
+ // Types
9
+ // =============================================================================
10
+
11
+ export interface RGB {
12
+ r: number
13
+ g: number
14
+ b: number
15
+ }
16
+
17
+ export interface HSL {
18
+ h: number
19
+ s: number
20
+ l: number
21
+ }
22
+
23
+ export type ShadeKey =
24
+ | '50'
25
+ | '100'
26
+ | '200'
27
+ | '300'
28
+ | '400'
29
+ | '500'
30
+ | '600'
31
+ | '700'
32
+ | '800'
33
+ | '900'
34
+ | '950'
35
+
36
+ export type Shades = Record<ShadeKey, string>
37
+
38
+ export type ColorRole =
39
+ | 'primary'
40
+ | 'secondary'
41
+ | 'accent'
42
+ | 'surface'
43
+ | 'success'
44
+ | 'warning'
45
+ | 'danger'
46
+ | 'info'
47
+
48
+ /**
49
+ * Mapping of color roles to Tailwind color names or hex values
50
+ */
51
+ export type ColorMapping = Partial<Record<ColorRole, string>>
52
+
53
+ // =============================================================================
54
+ // Color Conversion Utilities
55
+ // =============================================================================
56
+
57
+ /**
58
+ * Convert hex color to RGB
59
+ */
60
+ export function hexToRgb(hex: string): RGB {
61
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
62
+ if (!result) {
63
+ throw new Error(`Invalid hex color: ${hex}`)
64
+ }
65
+ return {
66
+ r: parseInt(result[1], 16),
67
+ g: parseInt(result[2], 16),
68
+ b: parseInt(result[3], 16)
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Convert RGB to hex color
74
+ */
75
+ export function rgbToHex(rgb: RGB): string {
76
+ const toHex = (n: number) => {
77
+ const hex = Math.round(Math.max(0, Math.min(255, n))).toString(16)
78
+ return hex.length === 1 ? `0${hex}` : hex
79
+ }
80
+ return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`
81
+ }
82
+
83
+ /**
84
+ * Convert RGB to HSL
85
+ */
86
+ interface NormalizedRGB {
87
+ r: number
88
+ g: number
89
+ b: number
90
+ }
91
+
92
+ function computeHue(norm: NormalizedRGB, max: number, d: number): number {
93
+ const { r, g, b } = norm
94
+ if (max === r) return ((g - b) / d + (g < b ? 6 : 0)) / 6
95
+ if (max === g) return ((b - r) / d + 2) / 6
96
+ return ((r - g) / d + 4) / 6
97
+ }
98
+
99
+ export function rgbToHsl(rgb: RGB): HSL {
100
+ const norm: NormalizedRGB = { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255 }
101
+
102
+ const max = Math.max(norm.r, norm.g, norm.b)
103
+ const min = Math.min(norm.r, norm.g, norm.b)
104
+ const l = (max + min) / 2
105
+
106
+ if (max === min) return { h: 0, s: 0, l: l * 100 }
107
+
108
+ const d = max - min
109
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
110
+ const h = computeHue(norm, max, d)
111
+
112
+ return { h: h * 360, s: s * 100, l: l * 100 }
113
+ }
114
+
115
+ function normalizeT(t: number): number {
116
+ if (t < 0) return t + 1
117
+ if (t > 1) return t - 1
118
+ return t
119
+ }
120
+
121
+ function hue2rgb(p: number, q: number, t: number): number {
122
+ const tt = normalizeT(t)
123
+ if (tt < 1 / 6) return p + (q - p) * 6 * tt
124
+ if (tt < 1 / 2) return q
125
+ if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6
126
+ return p
127
+ }
128
+
129
+ /**
130
+ * Convert HSL to RGB
131
+ */
132
+ export function hslToRgb(hsl: HSL): RGB {
133
+ const h = hsl.h / 360
134
+ const s = hsl.s / 100
135
+ const l = hsl.l / 100
136
+
137
+ if (s === 0) {
138
+ const val = Math.round(l * 255)
139
+ return { r: val, g: val, b: val }
140
+ }
141
+
142
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s
143
+ const p = 2 * l - q
144
+
145
+ return {
146
+ r: Math.round(hue2rgb(p, q, h + 1 / 3) * 255),
147
+ g: Math.round(hue2rgb(p, q, h) * 255),
148
+ b: Math.round(hue2rgb(p, q, h - 1 / 3) * 255)
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Convert hex to HSL
154
+ */
155
+ export function hexToHsl(hex: string): HSL {
156
+ return rgbToHsl(hexToRgb(hex))
157
+ }
158
+
159
+ /**
160
+ * Convert HSL to hex
161
+ */
162
+ export function hslToHex(hsl: HSL): string {
163
+ return rgbToHex(hslToRgb(hsl))
164
+ }
165
+
166
+ // =============================================================================
167
+ // Shade Generation
168
+ // =============================================================================
169
+
170
+ /**
171
+ * Generate Tailwind-like shades from a base hex color
172
+ * The input color is used as the 500 shade
173
+ */
174
+ export function generateShades(hex: string): Shades {
175
+ const hsl = hexToHsl(hex)
176
+
177
+ // Shade configuration: [saturation multiplier, target lightness]
178
+ // Lighter shades have higher lightness, lower saturation
179
+ // Darker shades have lower lightness, slightly higher saturation
180
+ const shadeConfig: Record<ShadeKey, [number, number]> = {
181
+ '50': [0.9, 97],
182
+ '100': [0.92, 94],
183
+ '200': [0.94, 86],
184
+ '300': [0.96, 75],
185
+ '400': [0.98, 60],
186
+ '500': [1.0, hsl.l], // Keep original lightness
187
+ '600': [1.02, Math.max(hsl.l * 0.82, 25)],
188
+ '700': [1.04, Math.max(hsl.l * 0.65, 20)],
189
+ '800': [1.06, Math.max(hsl.l * 0.5, 15)],
190
+ '900': [1.08, Math.max(hsl.l * 0.35, 12)],
191
+ '950': [1.12, Math.max(hsl.l * 0.22, 8)]
192
+ }
193
+
194
+ const shades: Partial<Shades> = {}
195
+
196
+ for (const [shade, [satMult, lightness]] of Object.entries(shadeConfig)) {
197
+ const newHsl: HSL = {
198
+ h: hsl.h,
199
+ s: Math.min(100, hsl.s * satMult),
200
+ l: lightness
201
+ }
202
+ shades[shade as ShadeKey] = hslToHex(newHsl)
203
+ }
204
+
205
+ return shades as Shades
206
+ }
207
+
208
+ /**
209
+ * Check if a string is a valid hex color
210
+ */
211
+ export function isHexColor(value: string): boolean {
212
+ return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value)
213
+ }
214
+
215
+ // =============================================================================
216
+ // Tailwind Colors (Built-in Presets)
217
+ // =============================================================================
218
+
219
+ export const tailwindColors: Record<string, Shades> = {
220
+ slate: {
221
+ '50': '#f8fafc',
222
+ '100': '#f1f5f9',
223
+ '200': '#e2e8f0',
224
+ '300': '#cbd5e1',
225
+ '400': '#94a3b8',
226
+ '500': '#64748b',
227
+ '600': '#475569',
228
+ '700': '#334155',
229
+ '800': '#1e293b',
230
+ '900': '#0f172a',
231
+ '950': '#020617'
232
+ },
233
+ gray: {
234
+ '50': '#f9fafb',
235
+ '100': '#f3f4f6',
236
+ '200': '#e5e7eb',
237
+ '300': '#d1d5db',
238
+ '400': '#9ca3af',
239
+ '500': '#6b7280',
240
+ '600': '#4b5563',
241
+ '700': '#374151',
242
+ '800': '#1f2937',
243
+ '900': '#111827',
244
+ '950': '#030712'
245
+ },
246
+ zinc: {
247
+ '50': '#fafafa',
248
+ '100': '#f4f4f5',
249
+ '200': '#e4e4e7',
250
+ '300': '#d4d4d8',
251
+ '400': '#a1a1aa',
252
+ '500': '#71717a',
253
+ '600': '#52525b',
254
+ '700': '#3f3f46',
255
+ '800': '#27272a',
256
+ '900': '#18181b',
257
+ '950': '#09090b'
258
+ },
259
+ neutral: {
260
+ '50': '#fafafa',
261
+ '100': '#f5f5f5',
262
+ '200': '#e5e5e5',
263
+ '300': '#d4d4d4',
264
+ '400': '#a3a3a3',
265
+ '500': '#737373',
266
+ '600': '#525252',
267
+ '700': '#404040',
268
+ '800': '#262626',
269
+ '900': '#171717',
270
+ '950': '#0a0a0a'
271
+ },
272
+ stone: {
273
+ '50': '#fafaf9',
274
+ '100': '#f5f5f4',
275
+ '200': '#e7e5e4',
276
+ '300': '#d6d3d1',
277
+ '400': '#a8a29e',
278
+ '500': '#78716c',
279
+ '600': '#57534e',
280
+ '700': '#44403c',
281
+ '800': '#292524',
282
+ '900': '#1c1917',
283
+ '950': '#0c0a09'
284
+ },
285
+ red: {
286
+ '50': '#fef2f2',
287
+ '100': '#fee2e2',
288
+ '200': '#fecaca',
289
+ '300': '#fca5a5',
290
+ '400': '#f87171',
291
+ '500': '#ef4444',
292
+ '600': '#dc2626',
293
+ '700': '#b91c1c',
294
+ '800': '#991b1b',
295
+ '900': '#7f1d1d',
296
+ '950': '#450a0a'
297
+ },
298
+ orange: {
299
+ '50': '#fff7ed',
300
+ '100': '#ffedd5',
301
+ '200': '#fed7aa',
302
+ '300': '#fdba74',
303
+ '400': '#fb923c',
304
+ '500': '#f97316',
305
+ '600': '#ea580c',
306
+ '700': '#c2410c',
307
+ '800': '#9a3412',
308
+ '900': '#7c2d12',
309
+ '950': '#431407'
310
+ },
311
+ amber: {
312
+ '50': '#fffbeb',
313
+ '100': '#fef3c7',
314
+ '200': '#fde68a',
315
+ '300': '#fcd34d',
316
+ '400': '#fbbf24',
317
+ '500': '#f59e0b',
318
+ '600': '#d97706',
319
+ '700': '#b45309',
320
+ '800': '#92400e',
321
+ '900': '#78350f',
322
+ '950': '#451a03'
323
+ },
324
+ yellow: {
325
+ '50': '#fefce8',
326
+ '100': '#fef9c3',
327
+ '200': '#fef08a',
328
+ '300': '#fde047',
329
+ '400': '#facc15',
330
+ '500': '#eab308',
331
+ '600': '#ca8a04',
332
+ '700': '#a16207',
333
+ '800': '#854d0e',
334
+ '900': '#713f12',
335
+ '950': '#422006'
336
+ },
337
+ lime: {
338
+ '50': '#f7fee7',
339
+ '100': '#ecfccb',
340
+ '200': '#d9f99d',
341
+ '300': '#bef264',
342
+ '400': '#a3e635',
343
+ '500': '#84cc16',
344
+ '600': '#65a30d',
345
+ '700': '#4d7c0f',
346
+ '800': '#3f6212',
347
+ '900': '#365314',
348
+ '950': '#1a2e05'
349
+ },
350
+ green: {
351
+ '50': '#f0fdf4',
352
+ '100': '#dcfce7',
353
+ '200': '#bbf7d0',
354
+ '300': '#86efac',
355
+ '400': '#4ade80',
356
+ '500': '#22c55e',
357
+ '600': '#16a34a',
358
+ '700': '#15803d',
359
+ '800': '#166534',
360
+ '900': '#14532d',
361
+ '950': '#052e16'
362
+ },
363
+ emerald: {
364
+ '50': '#ecfdf5',
365
+ '100': '#d1fae5',
366
+ '200': '#a7f3d0',
367
+ '300': '#6ee7b7',
368
+ '400': '#34d399',
369
+ '500': '#10b981',
370
+ '600': '#059669',
371
+ '700': '#047857',
372
+ '800': '#065f46',
373
+ '900': '#064e3b',
374
+ '950': '#022c22'
375
+ },
376
+ teal: {
377
+ '50': '#f0fdfa',
378
+ '100': '#ccfbf1',
379
+ '200': '#99f6e4',
380
+ '300': '#5eead4',
381
+ '400': '#2dd4bf',
382
+ '500': '#14b8a6',
383
+ '600': '#0d9488',
384
+ '700': '#0f766e',
385
+ '800': '#115e59',
386
+ '900': '#134e4a',
387
+ '950': '#042f2e'
388
+ },
389
+ cyan: {
390
+ '50': '#ecfeff',
391
+ '100': '#cffafe',
392
+ '200': '#a5f3fc',
393
+ '300': '#67e8f9',
394
+ '400': '#22d3ee',
395
+ '500': '#06b6d4',
396
+ '600': '#0891b2',
397
+ '700': '#0e7490',
398
+ '800': '#155e75',
399
+ '900': '#164e63',
400
+ '950': '#083344'
401
+ },
402
+ sky: {
403
+ '50': '#f0f9ff',
404
+ '100': '#e0f2fe',
405
+ '200': '#bae6fd',
406
+ '300': '#7dd3fc',
407
+ '400': '#38bdf8',
408
+ '500': '#0ea5e9',
409
+ '600': '#0284c7',
410
+ '700': '#0369a1',
411
+ '800': '#075985',
412
+ '900': '#0c4a6e',
413
+ '950': '#082f49'
414
+ },
415
+ blue: {
416
+ '50': '#eff6ff',
417
+ '100': '#dbeafe',
418
+ '200': '#bfdbfe',
419
+ '300': '#93c5fd',
420
+ '400': '#60a5fa',
421
+ '500': '#3b82f6',
422
+ '600': '#2563eb',
423
+ '700': '#1d4ed8',
424
+ '800': '#1e40af',
425
+ '900': '#1e3a8a',
426
+ '950': '#172554'
427
+ },
428
+ indigo: {
429
+ '50': '#eef2ff',
430
+ '100': '#e0e7ff',
431
+ '200': '#c7d2fe',
432
+ '300': '#a5b4fc',
433
+ '400': '#818cf8',
434
+ '500': '#6366f1',
435
+ '600': '#4f46e5',
436
+ '700': '#4338ca',
437
+ '800': '#3730a3',
438
+ '900': '#312e81',
439
+ '950': '#1e1b4b'
440
+ },
441
+ violet: {
442
+ '50': '#f5f3ff',
443
+ '100': '#ede9fe',
444
+ '200': '#ddd6fe',
445
+ '300': '#c4b5fd',
446
+ '400': '#a78bfa',
447
+ '500': '#8b5cf6',
448
+ '600': '#7c3aed',
449
+ '700': '#6d28d9',
450
+ '800': '#5b21b6',
451
+ '900': '#4c1d95',
452
+ '950': '#2e1065'
453
+ },
454
+ purple: {
455
+ '50': '#faf5ff',
456
+ '100': '#f3e8ff',
457
+ '200': '#e9d5ff',
458
+ '300': '#d8b4fe',
459
+ '400': '#c084fc',
460
+ '500': '#a855f7',
461
+ '600': '#9333ea',
462
+ '700': '#7e22ce',
463
+ '800': '#6b21a8',
464
+ '900': '#581c87',
465
+ '950': '#3b0764'
466
+ },
467
+ fuchsia: {
468
+ '50': '#fdf4ff',
469
+ '100': '#fae8ff',
470
+ '200': '#f5d0fe',
471
+ '300': '#f0abfc',
472
+ '400': '#e879f9',
473
+ '500': '#d946ef',
474
+ '600': '#c026d3',
475
+ '700': '#a21caf',
476
+ '800': '#86198f',
477
+ '900': '#701a75',
478
+ '950': '#4a044e'
479
+ },
480
+ pink: {
481
+ '50': '#fdf2f8',
482
+ '100': '#fce7f3',
483
+ '200': '#fbcfe8',
484
+ '300': '#f9a8d4',
485
+ '400': '#f472b6',
486
+ '500': '#ec4899',
487
+ '600': '#db2777',
488
+ '700': '#be185d',
489
+ '800': '#9d174d',
490
+ '900': '#831843',
491
+ '950': '#500724'
492
+ },
493
+ rose: {
494
+ '50': '#fff1f2',
495
+ '100': '#ffe4e6',
496
+ '200': '#fecdd3',
497
+ '300': '#fda4af',
498
+ '400': '#fb7185',
499
+ '500': '#f43f5e',
500
+ '600': '#e11d48',
501
+ '700': '#be123c',
502
+ '800': '#9f1239',
503
+ '900': '#881337',
504
+ '950': '#4c0519'
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Get shades for a color (either Tailwind preset or custom hex)
510
+ */
511
+ export function getShades(color: string): Shades {
512
+ if (isHexColor(color)) {
513
+ return generateShades(color)
514
+ }
515
+ const preset = tailwindColors[color.toLowerCase()]
516
+ if (preset) {
517
+ return preset
518
+ }
519
+ throw new Error(`Unknown color: ${color}. Must be a hex color or Tailwind color name.`)
520
+ }
521
+
522
+ // =============================================================================
523
+ // Runtime Palette Application
524
+ // =============================================================================
525
+
526
+ /**
527
+ * Apply a color palette to the document root
528
+ * Sets CSS variables for each color role and shade
529
+ */
530
+ export function applyPalette(
531
+ mapping: Partial<Record<ColorRole, string>>,
532
+ element: HTMLElement = document.documentElement
533
+ ): void {
534
+ for (const [role, color] of Object.entries(mapping)) {
535
+ if (!color) continue
536
+
537
+ const shades = getShades(color)
538
+
539
+ 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}`)
543
+ }
544
+
545
+ // 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}`)
548
+ }
549
+ }
550
+
551
+ const DEFAULT_PALETTE_ROLES: ColorRole[] = [
552
+ 'primary',
553
+ 'secondary',
554
+ 'accent',
555
+ 'surface',
556
+ 'success',
557
+ 'warning',
558
+ 'danger',
559
+ 'info'
560
+ ]
561
+
562
+ const ALL_SHADE_KEYS: ShadeKey[] = [
563
+ '50',
564
+ '100',
565
+ '200',
566
+ '300',
567
+ '400',
568
+ '500',
569
+ '600',
570
+ '700',
571
+ '800',
572
+ '900',
573
+ '950'
574
+ ]
575
+
576
+ /**
577
+ * Remove custom palette CSS variables (reset to theme defaults)
578
+ */
579
+ export function resetPalette(
580
+ roles: ColorRole[] = DEFAULT_PALETTE_ROLES,
581
+ element: HTMLElement = document.documentElement
582
+ ): void {
583
+ for (const role of roles) {
584
+ element.style.removeProperty(`--color-${role}`)
585
+ for (const shade of ALL_SHADE_KEYS) {
586
+ element.style.removeProperty(`--color-${role}-${shade}`)
587
+ }
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Save palette to localStorage
593
+ */
594
+ export function savePalette(key: string, mapping: Partial<Record<ColorRole, string>>): void {
595
+ localStorage.setItem(key, JSON.stringify(mapping))
596
+ }
597
+
598
+ /**
599
+ * Load palette from localStorage
600
+ */
601
+ export function loadPalette(key: string): Partial<Record<ColorRole, string>> | null {
602
+ const stored = localStorage.getItem(key)
603
+ if (stored) {
604
+ try {
605
+ return JSON.parse(stored)
606
+ } catch {
607
+ return null
608
+ }
609
+ }
610
+ return null
611
+ }
612
+
613
+ /**
614
+ * Get list of available Tailwind color names
615
+ */
616
+ export function getTailwindColorNames(): string[] {
617
+ return Object.keys(tailwindColors)
618
+ }