@idealyst/theme 1.1.6 → 1.1.7

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/src/helpers.ts ADDED
@@ -0,0 +1,206 @@
1
+ import { IntentValue, Theme, Shade, ColorValue } from './theme';
2
+
3
+ /**
4
+ * Deep partial type for nested objects.
5
+ */
6
+ type DeepPartial<T> = {
7
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
8
+ };
9
+
10
+ /**
11
+ * Create an IntentValue with all required properties.
12
+ * Automatically generates light and dark variants if not provided.
13
+ *
14
+ * @param primary - The primary color for this intent
15
+ * @param contrast - The contrast color (typically for text on primary background)
16
+ * @param options - Optional overrides for light, dark, and any extended properties
17
+ * @returns A complete IntentValue object
18
+ *
19
+ * @example Basic usage
20
+ * ```typescript
21
+ * const brandIntent = createIntent('#8B5CF6', '#ffffff');
22
+ * ```
23
+ *
24
+ * @example With extended properties
25
+ * ```typescript
26
+ * const brandIntent = createIntent('#8B5CF6', '#ffffff', {
27
+ * shadowColor: 'rgba(139, 92, 246, 0.3)',
28
+ * hoverBackground: 'rgba(139, 92, 246, 0.1)',
29
+ * });
30
+ * ```
31
+ */
32
+ export function createIntent(
33
+ primary: string,
34
+ contrast: string,
35
+ options?: Partial<Omit<IntentValue, 'primary' | 'contrast'>>
36
+ ): IntentValue {
37
+ return {
38
+ primary,
39
+ contrast,
40
+ light: options?.light ?? lightenColor(primary, 0.3),
41
+ dark: options?.dark ?? darkenColor(primary, 0.3),
42
+ ...options,
43
+ } as IntentValue;
44
+ }
45
+
46
+ /**
47
+ * Deep merge two objects together.
48
+ * The source object's values take priority over target.
49
+ *
50
+ * @param target - Base object
51
+ * @param source - Object to merge in (takes priority)
52
+ * @returns Merged object
53
+ */
54
+ function deepMerge<T extends Record<string, any>>(target: T, source: DeepPartial<T>): T {
55
+ const result: Record<string, any> = { ...target };
56
+
57
+ for (const key in source) {
58
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
59
+ const sourceValue = source[key];
60
+ const targetValue = result[key];
61
+
62
+ if (
63
+ sourceValue !== null &&
64
+ typeof sourceValue === 'object' &&
65
+ !Array.isArray(sourceValue) &&
66
+ targetValue !== null &&
67
+ typeof targetValue === 'object' &&
68
+ !Array.isArray(targetValue)
69
+ ) {
70
+ result[key] = deepMerge(targetValue, sourceValue as DeepPartial<typeof targetValue>);
71
+ } else if (sourceValue !== undefined) {
72
+ result[key] = sourceValue;
73
+ }
74
+ }
75
+ }
76
+
77
+ return result as T;
78
+ }
79
+
80
+ /**
81
+ * Extend a base theme with custom values.
82
+ * Performs a deep merge where extensions override base values.
83
+ *
84
+ * @param base - The base theme to extend (e.g., lightTheme or darkTheme)
85
+ * @param extensions - Partial theme object with values to override
86
+ * @returns Complete theme with extensions applied
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * import { lightTheme, extendTheme, createIntent } from '@idealyst/theme';
91
+ *
92
+ * const myTheme = extendTheme(lightTheme, {
93
+ * intents: {
94
+ * brand: createIntent('#8B5CF6', '#ffffff'),
95
+ * accent: createIntent('#F59E0B', '#ffffff'),
96
+ * },
97
+ * colors: {
98
+ * surface: {
99
+ * elevated: '#ffffff',
100
+ * },
101
+ * },
102
+ * });
103
+ * ```
104
+ */
105
+ export function extendTheme(base: Theme, extensions: DeepPartial<Theme>): Theme {
106
+ return deepMerge(base, extensions);
107
+ }
108
+
109
+ /**
110
+ * Generate a color palette (50-900 shades) from a base color.
111
+ * Uses the 500 shade as the base and interpolates lighter/darker variants.
112
+ *
113
+ * @param baseColor - The base color (hex format, e.g., '#8B5CF6')
114
+ * @returns Record of shade values (50-900)
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * const brandPalette = generatePalette('#8B5CF6');
119
+ * // brandPalette[50] = light variant
120
+ * // brandPalette[500] = base color
121
+ * // brandPalette[900] = dark variant
122
+ * ```
123
+ */
124
+ export function generatePalette(baseColor: string): Record<Shade, ColorValue> {
125
+ const palette = {} as Record<Shade, ColorValue>;
126
+ const baseRgb = hexToRgb(baseColor);
127
+ const white: [number, number, number] = [255, 255, 255];
128
+ const black: [number, number, number] = [0, 0, 0];
129
+ const shades: Shade[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
130
+
131
+ for (const shade of shades) {
132
+ if (shade === 500) {
133
+ palette[shade] = baseColor;
134
+ } else if (shade < 500) {
135
+ // Lighter shades - interpolate towards white
136
+ const factor = (500 - shade) / 450;
137
+ const interpolated = interpolateColor(baseRgb, white, factor);
138
+ palette[shade] = rgbToHex(...interpolated);
139
+ } else {
140
+ // Darker shades - interpolate towards black
141
+ const factor = (shade - 500) / 400;
142
+ const interpolated = interpolateColor(baseRgb, black, factor);
143
+ palette[shade] = rgbToHex(...interpolated);
144
+ }
145
+ }
146
+
147
+ return palette;
148
+ }
149
+
150
+ // ============================================================================
151
+ // Internal Color Utilities
152
+ // ============================================================================
153
+
154
+ /**
155
+ * Convert hex color to RGB tuple.
156
+ */
157
+ function hexToRgb(hex: string): [number, number, number] {
158
+ const cleanHex = hex.replace('#', '');
159
+ const r = parseInt(cleanHex.slice(0, 2), 16);
160
+ const g = parseInt(cleanHex.slice(2, 4), 16);
161
+ const b = parseInt(cleanHex.slice(4, 6), 16);
162
+ return [r, g, b];
163
+ }
164
+
165
+ /**
166
+ * Convert RGB tuple to hex color.
167
+ */
168
+ function rgbToHex(r: number, g: number, b: number): string {
169
+ const toHex = (n: number) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, '0');
170
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
171
+ }
172
+
173
+ /**
174
+ * Interpolate between two RGB colors.
175
+ */
176
+ function interpolateColor(
177
+ color1: [number, number, number],
178
+ color2: [number, number, number],
179
+ factor: number
180
+ ): [number, number, number] {
181
+ return [
182
+ color1[0] + (color2[0] - color1[0]) * factor,
183
+ color1[1] + (color2[1] - color1[1]) * factor,
184
+ color1[2] + (color2[2] - color1[2]) * factor,
185
+ ];
186
+ }
187
+
188
+ /**
189
+ * Lighten a color by a given factor.
190
+ */
191
+ function lightenColor(hex: string, factor: number): string {
192
+ const rgb = hexToRgb(hex);
193
+ const white: [number, number, number] = [255, 255, 255];
194
+ const interpolated = interpolateColor(rgb, white, factor);
195
+ return rgbToHex(...interpolated);
196
+ }
197
+
198
+ /**
199
+ * Darken a color by a given factor.
200
+ */
201
+ function darkenColor(hex: string, factor: number): string {
202
+ const rgb = hexToRgb(hex);
203
+ const black: [number, number, number] = [0, 0, 0];
204
+ const interpolated = interpolateColor(rgb, black, factor);
205
+ return rgbToHex(...interpolated);
206
+ }
package/src/index.ts CHANGED
@@ -4,4 +4,6 @@ export * from './darkTheme';
4
4
  export * from './theme';
5
5
  export * from './variants';
6
6
  export * from './components';
7
- export * from './styles';
7
+ export * from './styles';
8
+ export * from './helpers';
9
+ export * from './builder';