@shohojdhara/atomix 0.5.2 → 0.5.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.
Files changed (61) hide show
  1. package/atomix.config.ts +33 -33
  2. package/dist/atomix.css +3213 -159
  3. package/dist/atomix.css.map +1 -1
  4. package/dist/atomix.min.css +5 -5
  5. package/dist/atomix.min.css.map +1 -1
  6. package/dist/config.d.ts +187 -112
  7. package/dist/config.js +2 -47
  8. package/dist/config.js.map +1 -1
  9. package/dist/index.d.ts +1958 -900
  10. package/dist/index.esm.js +2279 -382
  11. package/dist/index.esm.js.map +1 -1
  12. package/dist/index.js +2332 -413
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.min.js +1 -1
  15. package/dist/index.min.js.map +1 -1
  16. package/dist/theme.d.ts +1390 -276
  17. package/dist/theme.js +2125 -621
  18. package/dist/theme.js.map +1 -1
  19. package/package.json +1 -1
  20. package/scripts/cli/internal/config-loader.js +30 -20
  21. package/src/lib/config/index.ts +38 -362
  22. package/src/lib/config/loader.ts +422 -0
  23. package/src/lib/config/public-api.ts +43 -0
  24. package/src/lib/config/types.ts +389 -0
  25. package/src/lib/config/validator.ts +305 -0
  26. package/src/lib/theme/adapters/index.ts +1 -1
  27. package/src/lib/theme/adapters/themeAdapter.ts +358 -229
  28. package/src/lib/theme/components/ThemeToggle.tsx +276 -0
  29. package/src/lib/theme/config/configLoader.ts +351 -0
  30. package/src/lib/theme/config/loader.ts +221 -0
  31. package/src/lib/theme/core/createTheme.ts +126 -50
  32. package/src/lib/theme/core/createThemeObject.ts +7 -4
  33. package/src/lib/theme/hooks/useThemeSwitcher.ts +164 -0
  34. package/src/lib/theme/index.ts +322 -38
  35. package/src/lib/theme/runtime/ThemeProvider.tsx +44 -10
  36. package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +44 -393
  37. package/src/lib/theme/runtime/useTheme.ts +1 -0
  38. package/src/lib/theme/tokens/tokens.ts +101 -1
  39. package/src/lib/theme/types.ts +91 -0
  40. package/src/lib/theme/utils/performanceMonitor.ts +315 -0
  41. package/src/lib/theme/utils/responsive.ts +280 -0
  42. package/src/lib/theme/utils/themeUtils.ts +531 -117
  43. package/src/styles/01-settings/_settings.background.scss +34 -5
  44. package/src/styles/02-tools/_tools.background.scss +330 -52
  45. package/src/styles/05-objects/_objects.masonry-grid.scss +3 -3
  46. package/src/styles/06-components/_components.accordion.scss +2 -2
  47. package/src/styles/06-components/_components.badge.scss +1 -1
  48. package/src/styles/06-components/_components.button.scss +2 -2
  49. package/src/styles/06-components/_components.callout.scss +2 -2
  50. package/src/styles/06-components/_components.card.scss +1 -1
  51. package/src/styles/06-components/_components.dropdown.scss +1 -1
  52. package/src/styles/06-components/_components.dynamic-background.scss +69 -0
  53. package/src/styles/06-components/_components.edge-panel.scss +2 -2
  54. package/src/styles/06-components/_components.input.scss +3 -3
  55. package/src/styles/06-components/_components.messages.scss +6 -6
  56. package/src/styles/06-components/_components.modal.scss +1 -1
  57. package/src/styles/06-components/_components.navbar.scss +1 -1
  58. package/src/styles/06-components/_components.popover.scss +1 -1
  59. package/src/styles/06-components/_components.toggle.scss +1 -1
  60. package/src/styles/06-components/_components.tooltip.scss +3 -3
  61. package/src/styles/06-components/_index.scss +1 -0
@@ -1,185 +1,599 @@
1
1
  /**
2
2
  * Theme Utilities
3
- *
4
- * Helper utilities for working with themes, including color manipulation,
5
- * spacing helpers, and theme value accessors.
3
+ *
4
+ * Helper functions for common theme operations including:
5
+ * - Theme switching (dark/light mode)
6
+ * - Theme persistence (localStorage)
7
+ * - System preference detection
8
+ * - Smooth transitions
9
+ * - Color manipulation
6
10
  */
7
11
 
12
+ import type { DesignTokens } from '../tokens';
8
13
  import type { SpacingFunction, SpacingOptions } from '../types';
14
+ import { injectCSS, removeCSS } from '../utils/injectCSS';
15
+ import { deepMerge } from '../core/composeTheme';
9
16
 
10
17
  // ============================================================================
11
- // Color Manipulation Utilities
18
+ // Type Definitions
19
+ // ============================================================================
20
+
21
+ export type ThemeMode = 'light' | 'dark' | 'system';
22
+
23
+ export interface ThemeSwitcherOptions {
24
+ /** CSS selector for root element (default: ':root') */
25
+ selector?: string;
26
+ /** Storage key for theme persistence (default: 'atomix-theme') */
27
+ storageKey?: string;
28
+ /** Enable smooth transitions (default: true) */
29
+ enableTransition?: boolean;
30
+ /** Transition duration in ms (default: 300) */
31
+ transitionDuration?: number;
32
+ }
33
+
34
+ export interface ThemePersistenceOptions {
35
+ /** Storage key (default: 'atomix-theme') */
36
+ storageKey?: string;
37
+ /** Storage type (default: 'localStorage') */
38
+ storageType?: 'localStorage' | 'sessionStorage';
39
+ }
40
+
41
+ // ============================================================================
42
+ // Theme Mode Switching
12
43
  // ============================================================================
13
44
 
14
45
  /**
15
- * Convert hex color to RGB object
46
+ * Switch between light and dark themes
47
+ *
48
+ * Automatically toggles a class on the root element and persists the choice.
49
+ *
50
+ * @param mode - Theme mode ('light', 'dark', or 'system')
51
+ * @param options - Configuration options
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * import { switchTheme } from '@shohojdhara/atomix/theme/utils';
56
+ *
57
+ * // Switch to dark mode
58
+ * switchTheme('dark');
59
+ *
60
+ * // Toggle between light/dark
61
+ * const current = getCurrentTheme();
62
+ * switchTheme(current === 'dark' ? 'light' : 'dark');
63
+ * ```
16
64
  */
17
- export function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
18
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
19
- return result
20
- ? {
21
- r: parseInt(result[1]!, 16),
22
- g: parseInt(result[2]!, 16),
23
- b: parseInt(result[3]!, 16),
24
- }
25
- : null;
65
+ export function switchTheme(
66
+ mode: ThemeMode,
67
+ options: ThemeSwitcherOptions = {}
68
+ ): void {
69
+ const {
70
+ selector = ':root',
71
+ storageKey = 'atomix-theme',
72
+ enableTransition = true,
73
+ transitionDuration = 300,
74
+ } = options;
75
+
76
+ // Determine actual mode (resolve 'system')
77
+ const resolvedMode = mode === 'system' ? getSystemTheme() : mode;
78
+
79
+ // Get root element
80
+ const root = selector === ':root' ? document.documentElement : document.querySelector(selector);
81
+ if (!root) return;
82
+
83
+ // Add transition class if enabled
84
+ if (enableTransition) {
85
+ const htmlRoot = root as HTMLElement;
86
+ htmlRoot.style.transition = `all ${transitionDuration}ms ease-in-out`;
87
+
88
+ // Remove transition after it completes
89
+ setTimeout(() => {
90
+ htmlRoot.style.transition = '';
91
+ }, transitionDuration);
92
+ }
93
+
94
+ // Apply theme class
95
+ root.classList.remove('atomix-theme-light', 'atomix-theme-dark');
96
+ root.classList.add(`atomix-theme-${resolvedMode}`);
97
+
98
+ // Set data attribute for CSS selectors
99
+ root.setAttribute('data-theme', resolvedMode);
100
+
101
+ // Persist choice
102
+ persistTheme(resolvedMode, { storageKey });
103
+
104
+ // Dispatch custom event for listeners
105
+ window.dispatchEvent(new CustomEvent('atomix-theme-change', {
106
+ detail: { mode: resolvedMode }
107
+ }));
26
108
  }
27
109
 
28
110
  /**
29
- * Convert RGB to hex color
111
+ * Toggle between light and dark themes
112
+ *
113
+ * @param options - Configuration options
114
+ * @returns The new theme mode
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * const newMode = toggleTheme();
119
+ * console.log('Switched to:', newMode);
120
+ * ```
30
121
  */
31
- export function rgbToHex(r: number, g: number, b: number): string {
32
- const toHex = (val: number) => {
33
- const hex = Math.round(Math.max(0, Math.min(255, val)))
34
- .toString(16)
35
- .padStart(2, '0');
36
- return hex;
122
+ export function toggleTheme(options: ThemeSwitcherOptions = {}): ThemeMode {
123
+ const current = getCurrentTheme(options.storageKey);
124
+ const next = current === 'dark' ? 'light' : 'dark';
125
+ switchTheme(next, options);
126
+ return next;
127
+ }
128
+
129
+ /**
130
+ * Get current theme mode
131
+ *
132
+ * @param storageKey - Storage key (default: 'atomix-theme')
133
+ * @returns Current theme mode or 'light' if not set
134
+ */
135
+ export function getCurrentTheme(storageKey: string = 'atomix-theme'): ThemeMode {
136
+ if (typeof window === 'undefined') return 'light';
137
+
138
+ const stored = localStorage.getItem(storageKey);
139
+ return (stored as ThemeMode) || 'light';
140
+ }
141
+
142
+ /**
143
+ * Get system theme preference
144
+ *
145
+ * @returns 'dark' if system prefers dark mode, 'light' otherwise
146
+ */
147
+ export function getSystemTheme(): ThemeMode {
148
+ if (typeof window === 'undefined') return 'light';
149
+
150
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
151
+ return prefersDark ? 'dark' : 'light';
152
+ }
153
+
154
+ /**
155
+ * Initialize theme based on saved preference or system preference
156
+ *
157
+ * Call this once at app startup.
158
+ *
159
+ * @param options - Configuration options
160
+ * @returns The initialized theme mode
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * // In your app entry point
165
+ * import { initializeTheme } from '@shohojdhara/atomix/theme/utils';
166
+ *
167
+ * const theme = initializeTheme();
168
+ * console.log('Theme initialized:', theme);
169
+ * ```
170
+ */
171
+ export function initializeTheme(options: ThemeSwitcherOptions = {}): ThemeMode {
172
+ const saved = getCurrentTheme(options.storageKey);
173
+
174
+ // If no saved preference, use system preference
175
+ if (!saved || saved === 'system') {
176
+ const system = getSystemTheme();
177
+ switchTheme(system, options);
178
+ return system;
179
+ }
180
+
181
+ // Use saved preference
182
+ switchTheme(saved, options);
183
+ return saved;
184
+ }
185
+
186
+ /**
187
+ * Listen for system theme changes
188
+ *
189
+ * @param callback - Function to call when system theme changes
190
+ * @returns Cleanup function to stop listening
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * const cleanup = listenToSystemTheme((mode) => {
195
+ * console.log('System theme changed to:', mode);
196
+ * switchTheme(mode);
197
+ * });
198
+ *
199
+ * // Later, when component unmounts
200
+ * cleanup();
201
+ * ```
202
+ */
203
+ export function listenToSystemTheme(callback: (mode: ThemeMode) => void): () => void {
204
+ if (typeof window === 'undefined') return () => {};
205
+
206
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
207
+
208
+ const handler = (e: MediaQueryListEvent) => {
209
+ callback(e.matches ? 'dark' : 'light');
37
210
  };
38
- return `#${toHex(r ?? 0)}${toHex(g ?? 0)}${toHex(b ?? 0)}`;
211
+
212
+ // Modern browsers
213
+ if (mediaQuery.addEventListener) {
214
+ mediaQuery.addEventListener('change', handler);
215
+ return () => mediaQuery.removeEventListener('change', handler);
216
+ }
217
+
218
+ // Fallback for older browsers
219
+ mediaQuery.addListener(handler);
220
+ return () => mediaQuery.removeListener(handler);
39
221
  }
40
222
 
223
+ // ============================================================================
224
+ // Theme Persistence
225
+ // ============================================================================
226
+
41
227
  /**
42
- * Calculate relative luminance of a color
43
- * Used for determining contrast ratios
228
+ * Save theme preference to storage
229
+ *
230
+ * @param mode - Theme mode to save
231
+ * @param options - Persistence options
44
232
  */
45
- export function getLuminance(color: string): number {
46
- const rgb = hexToRgb(color);
47
- if (!rgb) return 0;
233
+ export function persistTheme(
234
+ mode: ThemeMode,
235
+ options: ThemePersistenceOptions = {}
236
+ ): void {
237
+ if (typeof window === 'undefined') return;
238
+
239
+ const {
240
+ storageKey = 'atomix-theme',
241
+ storageType = 'localStorage',
242
+ } = options;
243
+
244
+ const storage = storageType === 'localStorage' ? localStorage : sessionStorage;
245
+ storage.setItem(storageKey, mode);
246
+ }
247
+
248
+ /**
249
+ * Clear saved theme preference
250
+ *
251
+ * @param options - Persistence options
252
+ */
253
+ export function clearThemePreference(options: ThemePersistenceOptions = {}): void {
254
+ if (typeof window === 'undefined') return;
255
+
256
+ const {
257
+ storageKey = 'atomix-theme',
258
+ storageType = 'localStorage',
259
+ } = options;
260
+
261
+ const storage = storageType === 'localStorage' ? localStorage : sessionStorage;
262
+ storage.removeItem(storageKey);
263
+ }
264
+
265
+ // ============================================================================
266
+ // Theme Tokens Manipulation
267
+ // ============================================================================
268
+
269
+ /**
270
+ * Merge multiple token sets
271
+ *
272
+ * Deep merges token objects, with later tokens overriding earlier ones.
273
+ *
274
+ * @param tokens - Token objects to merge
275
+ * @returns Merged tokens
276
+ *
277
+ * @example
278
+ * ```typescript
279
+ * const merged = mergeTokens(
280
+ * baseTokens,
281
+ * { colors: { primary: { main: '#custom' } } }
282
+ * );
283
+ * ```
284
+ */
285
+ export function mergeTokens(...tokens: Array<Partial<DesignTokens>>): Partial<DesignTokens> {
286
+ return tokens.reduce((acc, current) => {
287
+ return deepMerge(acc, current);
288
+ }, {} as Partial<DesignTokens>);
289
+ }
290
+
291
+ /**
292
+ * Override specific tokens
293
+ *
294
+ * Creates a new token object with specific overrides.
295
+ *
296
+ * @param base - Base tokens
297
+ * @param overrides - Tokens to override
298
+ * @returns New tokens with overrides applied
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * const customized = overrideTokens(defaultTokens, {
303
+ * colors: {
304
+ * primary: { main: '#ff0000' }
305
+ * }
306
+ * });
307
+ * ```
308
+ */
309
+ export function overrideTokens(
310
+ base: Partial<DesignTokens>,
311
+ overrides: Partial<DesignTokens>
312
+ ): Partial<DesignTokens> {
313
+ return deepMerge({ ...base }, overrides as any);
314
+ }
315
+
316
+ /**
317
+ * Pick specific token categories
318
+ *
319
+ * Extracts only the specified categories from tokens.
320
+ *
321
+ * @param tokens - Source tokens
322
+ * @param categories - Categories to pick
323
+ * @returns Tokens with only selected categories
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * const colorTokens = pickTokens(allTokens, ['colors']);
328
+ * ```
329
+ */
330
+ export function pickTokens(
331
+ tokens: Partial<DesignTokens>,
332
+ categories: Array<keyof DesignTokens>
333
+ ): Partial<DesignTokens> {
334
+ const result: Partial<DesignTokens> = {};
335
+
336
+ categories.forEach(category => {
337
+ if (tokens[category]) {
338
+ result[category] = tokens[category];
339
+ }
340
+ });
341
+
342
+ return result;
343
+ }
48
344
 
49
- const { r, g, b } = rgb;
50
- const [rs, gs, bs] = [r ?? 0, g ?? 0, b ?? 0].map(c => {
51
- const val = c / 255;
52
- return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4);
345
+ /**
346
+ * Omit specific token categories
347
+ *
348
+ * Removes specified categories from tokens.
349
+ *
350
+ * @param tokens - Source tokens
351
+ * @param categories - Categories to omit
352
+ * @returns Tokens without omitted categories
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * const withoutColors = omitTokens(allTokens, ['colors']);
357
+ * ```
358
+ */
359
+ export function omitTokens(
360
+ tokens: Partial<DesignTokens>,
361
+ categories: Array<keyof DesignTokens>
362
+ ): Partial<DesignTokens> {
363
+ const result = { ...tokens };
364
+
365
+ categories.forEach(category => {
366
+ delete result[category];
53
367
  });
368
+
369
+ return result;
370
+ }
54
371
 
55
- return 0.2126 * (rs ?? 0) + 0.7152 * (gs ?? 0) + 0.0722 * (bs ?? 0);
372
+ // ============================================================================
373
+ // Color Utilities
374
+ // ============================================================================
375
+
376
+ /**
377
+ * Convert hex color to RGB
378
+ *
379
+ * @param hex - Hex color (with or without #)
380
+ * @returns RGB object { r, g, b }
381
+ */
382
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
383
+ // Remove # if present
384
+ hex = hex.replace(/^#/, '');
385
+
386
+ // Handle shorthand hex
387
+ if (hex.length === 3) {
388
+ hex = hex.split('').map(c => c + c).join('');
389
+ }
390
+
391
+ // Validate
392
+ if (hex.length !== 6) return null;
393
+
394
+ const num = parseInt(hex, 16);
395
+ return {
396
+ r: (num >> 16) & 255,
397
+ g: (num >> 8) & 255,
398
+ b: num & 255,
399
+ };
400
+ }
401
+
402
+ /**
403
+ * Convert RGB to hex
404
+ *
405
+ * @param r - Red (0-255)
406
+ * @param g - Green (0-255)
407
+ * @param b - Blue (0-255)
408
+ * @returns Hex color with #
409
+ */
410
+ export function rgbToHex(r: number, g: number, b: number): string {
411
+ return '#' + [r, g, b].map(x => {
412
+ const hex = x.toString(16);
413
+ return hex.length === 1 ? '0' + hex : hex;
414
+ }).join('');
415
+ }
416
+
417
+ /**
418
+ * Calculate luminance of a color
419
+ *
420
+ * Used for determining contrast ratios.
421
+ *
422
+ * @param hex - Hex color
423
+ * @returns Luminance value (0-1)
424
+ */
425
+ export function getLuminance(hex: string): number {
426
+ const rgb = hexToRgb(hex);
427
+ if (!rgb) return 0;
428
+
429
+ const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(v => {
430
+ v /= 255;
431
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
432
+ });
433
+
434
+ return 0.2126 * (r ?? 0) + 0.7152 * (g ?? 0) + 0.0722 * (b ?? 0);
56
435
  }
57
436
 
58
437
  /**
59
438
  * Calculate contrast ratio between two colors
439
+ *
440
+ * @param hex1 - First hex color
441
+ * @param hex2 - Second hex color
442
+ * @returns Contrast ratio (1-21)
60
443
  */
61
- export function getContrastRatio(foreground: string, background: string): number {
62
- const lumA = getLuminance(foreground);
63
- const lumB = getLuminance(background);
64
- return (Math.max(lumA, lumB) + 0.05) / (Math.min(lumA, lumB) + 0.05);
444
+ export function getContrastRatio(hex1: string, hex2: string): number {
445
+ const lum1 = getLuminance(hex1);
446
+ const lum2 = getLuminance(hex2);
447
+
448
+ const brightest = Math.max(lum1, lum2);
449
+ const darkest = Math.min(lum1, lum2);
450
+
451
+ return (brightest + 0.05) / (darkest + 0.05);
65
452
  }
66
453
 
67
454
  /**
68
- * Get appropriate contrast text color (black or white) for a background color
455
+ * Check if text color passes WCAG AA standard
456
+ *
457
+ * @param textColor - Text color hex
458
+ * @param backgroundColor - Background color hex
459
+ * @param size - Font size ('small' or 'large')
460
+ * @returns true if passes WCAG AA
69
461
  */
70
- export function getContrastText(background: string, threshold: number = 3): string {
71
- const contrastWithWhite = getContrastRatio('#FFFFFF', background);
72
- const contrastWithBlack = getContrastRatio('#000000', background);
462
+ export function isAccessible(
463
+ textColor: string,
464
+ backgroundColor: string,
465
+ size: 'small' | 'large' = 'small'
466
+ ): boolean {
467
+ const ratio = getContrastRatio(textColor, backgroundColor);
468
+
469
+ // WCAG AA requires 4.5:1 for normal text, 3:1 for large text
470
+ const minimumRatio = size === 'large' ? 3 : 4.5;
471
+
472
+ return ratio >= minimumRatio;
473
+ }
73
474
 
475
+ /**
476
+ * Get appropriate text color (black or white) for a background
477
+ *
478
+ * @param backgroundColor - Background hex color
479
+ * @param threshold - Contrast threshold (default: 3)
480
+ * @returns '#000000' or '#FFFFFF'
481
+ */
482
+ export function getContrastText(backgroundColor: string, threshold: number = 3): string {
483
+ const contrastWithWhite = getContrastRatio(backgroundColor, '#FFFFFF');
74
484
  if (contrastWithWhite >= threshold) {
75
485
  return '#FFFFFF';
76
486
  }
77
- if (contrastWithBlack >= threshold) {
78
- return '#000000';
79
- }
80
-
81
- // Default to white if neither meets threshold
82
- return contrastWithWhite > contrastWithBlack ? '#FFFFFF' : '#000000';
487
+ return '#000000';
83
488
  }
84
489
 
85
490
  /**
86
- * Lighten a color by a given amount
87
- *
88
- * @param color - Hex color string
89
- * @param amount - Amount to lighten (0-1), default 0.2
491
+ * Lighten a color
492
+ *
493
+ * @param hex - Base hex color
494
+ * @param amount - Amount to lighten (0-1)
90
495
  * @returns Lightened hex color
91
496
  */
92
- export function lighten(color: string, amount: number = 0.2): string {
93
- const rgb = hexToRgb(color);
94
- if (!rgb) return color;
95
-
96
- const { r, g, b } = rgb;
97
- const lightenValue = (val: number) => Math.min(255, Math.round(val + (255 - val) * amount));
98
-
99
- return rgbToHex(lightenValue(r), lightenValue(g), lightenValue(b));
497
+ export function lighten(hex: string, amount: number = 0): string {
498
+ const rgb = hexToRgb(hex);
499
+ if (!rgb) return hex;
500
+
501
+ // Use amount directly as factor (0-1)
502
+ const factor = Math.max(0, Math.min(1, amount));
503
+
504
+ const r = Math.round(rgb.r + (255 - rgb.r) * factor);
505
+ const g = Math.round(rgb.g + (255 - rgb.g) * factor);
506
+ const b = Math.round(rgb.b + (255 - rgb.b) * factor);
507
+
508
+ return rgbToHex(Math.min(255, r), Math.min(255, g), Math.min(255, b));
100
509
  }
101
510
 
102
511
  /**
103
- * Darken a color by a given amount
104
- *
105
- * @param color - Hex color string
106
- * @param amount - Amount to darken (0-1), default 0.2
512
+ * Darken a color
513
+ *
514
+ * @param hex - Base hex color
515
+ * @param amount - Amount to darken (0-1)
107
516
  * @returns Darkened hex color
108
517
  */
109
- export function darken(color: string, amount: number = 0.2): string {
110
- const rgb = hexToRgb(color);
111
- if (!rgb) return color;
112
-
113
- const { r, g, b } = rgb;
114
- const darkenValue = (val: number) => Math.max(0, Math.round(val * (1 - amount)));
115
-
116
- return rgbToHex(darkenValue(r), darkenValue(g), darkenValue(b));
518
+ export function darken(hex: string, amount: number = 0): string {
519
+ const rgb = hexToRgb(hex);
520
+ if (!rgb) return hex;
521
+
522
+ // Use amount directly as factor (0-1)
523
+ const factor = Math.max(0, Math.min(1, amount));
524
+
525
+ const r = Math.round(rgb.r * (1 - factor));
526
+ const g = Math.round(rgb.g * (1 - factor));
527
+ const b = Math.round(rgb.b * (1 - factor));
528
+
529
+ return rgbToHex(Math.max(0, r), Math.max(0, g), Math.max(0, b));
117
530
  }
118
531
 
119
532
  /**
120
- * Add alpha (opacity) to a color
121
- *
122
- * @param color - Hex color string
533
+ * Add alpha to a color
534
+ *
535
+ * @param hex - Hex color
123
536
  * @param opacity - Opacity value (0-1)
124
537
  * @returns RGBA color string
125
538
  */
126
- export function alpha(color: string, opacity: number): string {
127
- const rgb = hexToRgb(color);
128
- if (!rgb) return color;
129
-
130
- const { r, g, b } = rgb;
131
- const clampedOpacity = Math.max(0, Math.min(1, opacity));
132
-
133
- return `rgba(${r}, ${g}, ${b}, ${clampedOpacity})`;
539
+ export function alpha(hex: string, opacity: number): string {
540
+ const rgb = hexToRgb(hex);
541
+ if (!rgb) return hex;
542
+
543
+ const validOpacity = Math.max(0, Math.min(1, opacity));
544
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${validOpacity})`;
134
545
  }
135
546
 
136
547
  /**
137
548
  * Emphasize a color (lighten if dark, darken if light)
138
- *
139
- * @param color - Hex color string
140
- * @param coefficient - Amount to emphasize (0-1), default 0.15
549
+ *
550
+ * @param hex - Hex color
551
+ * @param amount - Amount to emphasize (0-1)
141
552
  * @returns Emphasized hex color
142
553
  */
143
- export function emphasize(color: string, coefficient: number = 0.15): string {
144
- const luminance = getLuminance(color);
145
- return luminance > 0.5 ? darken(color, coefficient) : lighten(color, coefficient);
554
+ export function emphasize(hex: string, amount: number = 0.15): string {
555
+ const luminance = getLuminance(hex);
556
+ return luminance > 0.5 ? darken(hex, amount) : lighten(hex, amount);
146
557
  }
147
558
 
148
- // ============================================================================
149
- // Spacing Utilities
150
- // ============================================================================
151
-
152
559
  /**
153
- * Create a spacing function from various input types
154
- *
155
- * @param spacingInput - Spacing configuration (number, array, or function), default 4
560
+ * Create a spacing utility
561
+ *
562
+ * @param spacingInput - Spacing configuration
156
563
  * @returns Spacing function
157
564
  */
158
565
  export function createSpacing(spacingInput: SpacingOptions = 4): SpacingFunction {
159
- // If it's already a function, return it
160
- if (typeof spacingInput === 'function') {
161
- return spacingInput;
162
- }
566
+ return (...values: number[]): string => {
567
+ if (values.length === 0) {
568
+ return typeof spacingInput === 'number' ? `0px` : '0px';
569
+ }
163
570
 
164
- // If it's a number, create a function that multiplies by that number
165
- if (typeof spacingInput === 'number') {
166
- return (...values: number[]) => {
167
- if (values.length === 0) return '0px';
168
- return values.map(value => `${value * spacingInput}px`).join(' ');
169
- };
170
- }
571
+ if (typeof spacingInput === 'function') {
572
+ return spacingInput(...values);
573
+ }
171
574
 
172
- // If it's an array, use it as a scale
173
- if (Array.isArray(spacingInput)) {
174
- return (...values: number[]) => {
175
- if (values.length === 0) return '0px';
176
- return values.map(value => `${spacingInput[value] || value}px`).join(' ');
177
- };
178
- }
179
-
180
- // Default to 4px base
181
- return (...values: number[]) => {
182
- if (values.length === 0) return '0px';
183
- return values.map(value => `${value * 4}px`).join(' ');
575
+ return values
576
+ .map(value => {
577
+ if (typeof spacingInput === 'number') {
578
+ return `${value * spacingInput}px`;
579
+ }
580
+ if (Array.isArray(spacingInput)) {
581
+ const scaled = spacingInput[value];
582
+ return typeof scaled === 'number' ? `${scaled}px` : `${value}px`;
583
+ }
584
+ return `${value}px`;
585
+ })
586
+ .join(' ');
184
587
  };
185
588
  }
589
+
590
+ // ============================================================================
591
+ // Helper Functions
592
+ // ============================================================================
593
+
594
+ /**
595
+ * Check if value is an object
596
+ */
597
+ function isObject(item: any): item is object {
598
+ return item && typeof item === 'object' && !Array.isArray(item);
599
+ }