@idealyst/theme 1.1.7 → 1.1.8

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,817 @@
1
+ /**
2
+ * Style Generator
3
+ *
4
+ * Generates flat, Unistyles-compatible style files from component style definitions.
5
+ * This runs at build time to "unroll" all function calls into static data.
6
+ *
7
+ * Key principles:
8
+ * 1. Theme values stay as `theme.xxx` references (not evaluated)
9
+ * 2. All variant combinations are pre-computed as compound variants
10
+ * 3. No function calls in generated output
11
+ */
12
+
13
+ import type { Theme } from '../theme';
14
+ import type { IdealystConfig, ComponentExtensions } from './types';
15
+
16
+ /**
17
+ * Represents a style value that references the theme.
18
+ * Used during code generation to output `theme.xxx` references.
19
+ */
20
+ interface ThemeReference {
21
+ __themeRef: true;
22
+ path: string; // e.g., 'colors.surface.primary'
23
+ }
24
+
25
+ /**
26
+ * Check if a value is a theme reference.
27
+ */
28
+ function isThemeReference(value: unknown): value is ThemeReference {
29
+ return typeof value === 'object' && value !== null && '__themeRef' in value;
30
+ }
31
+
32
+ /**
33
+ * Create a theme reference for code generation.
34
+ */
35
+ function themeRef(path: string): ThemeReference {
36
+ return { __themeRef: true, path };
37
+ }
38
+
39
+ /**
40
+ * Deep merge objects, with source taking precedence.
41
+ */
42
+ function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
43
+ const result = { ...target };
44
+
45
+ for (const key in source) {
46
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
47
+ const sourceValue = source[key];
48
+ const targetValue = target[key];
49
+
50
+ if (
51
+ sourceValue !== null &&
52
+ typeof sourceValue === 'object' &&
53
+ !Array.isArray(sourceValue) &&
54
+ !isThemeReference(sourceValue) &&
55
+ targetValue !== null &&
56
+ typeof targetValue === 'object' &&
57
+ !Array.isArray(targetValue) &&
58
+ !isThemeReference(targetValue)
59
+ ) {
60
+ (result as any)[key] = deepMerge(targetValue, sourceValue);
61
+ } else if (sourceValue !== undefined) {
62
+ (result as any)[key] = sourceValue;
63
+ }
64
+ }
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Convert a JavaScript value to code string.
72
+ * Theme references become `theme.xxx` expressions.
73
+ */
74
+ function valueToCode(value: unknown, indent: number = 0): string {
75
+ const spaces = ' '.repeat(indent);
76
+ const innerSpaces = ' '.repeat(indent + 1);
77
+
78
+ if (value === null) return 'null';
79
+ if (value === undefined) return 'undefined';
80
+
81
+ if (isThemeReference(value)) {
82
+ return `theme.${value.path}`;
83
+ }
84
+
85
+ if (typeof value === 'string') {
86
+ // Check if it looks like a theme reference string
87
+ if (value.startsWith('theme.')) {
88
+ return value; // Already a theme reference
89
+ }
90
+ return JSON.stringify(value);
91
+ }
92
+
93
+ if (typeof value === 'number' || typeof value === 'boolean') {
94
+ return String(value);
95
+ }
96
+
97
+ if (Array.isArray(value)) {
98
+ if (value.length === 0) return '[]';
99
+ const items = value.map(v => valueToCode(v, indent + 1));
100
+ return `[\n${innerSpaces}${items.join(`,\n${innerSpaces}`)}\n${spaces}]`;
101
+ }
102
+
103
+ if (typeof value === 'object') {
104
+ const entries = Object.entries(value);
105
+ if (entries.length === 0) return '{}';
106
+
107
+ const lines = entries.map(([k, v]) => {
108
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k);
109
+ return `${innerSpaces}${key}: ${valueToCode(v, indent + 1)}`;
110
+ });
111
+
112
+ return `{\n${lines.join(',\n')}\n${spaces}}`;
113
+ }
114
+
115
+ return String(value);
116
+ }
117
+
118
+ // ============================================================================
119
+ // View Component Generator
120
+ // ============================================================================
121
+
122
+ /**
123
+ * Generate background variants for View component.
124
+ */
125
+ function generateBackgroundVariants(theme: Theme): Record<string, any> {
126
+ const variants: Record<string, any> = {
127
+ transparent: { backgroundColor: 'transparent' },
128
+ };
129
+
130
+ // Add all surface colors with theme references
131
+ for (const surface in theme.colors.surface) {
132
+ variants[surface] = {
133
+ backgroundColor: themeRef(`colors.surface.${surface}`),
134
+ };
135
+ }
136
+
137
+ return variants;
138
+ }
139
+
140
+ /**
141
+ * Generate radius variants.
142
+ */
143
+ function generateRadiusVariants(theme: Theme): Record<string, any> {
144
+ const variants: Record<string, any> = {};
145
+
146
+ for (const radius in theme.radii) {
147
+ variants[radius] = {
148
+ borderRadius: theme.radii[radius as keyof typeof theme.radii],
149
+ };
150
+ }
151
+
152
+ return variants;
153
+ }
154
+
155
+ /**
156
+ * Generate spacing variants (gap, padding, margin).
157
+ */
158
+ function generateSpacingVariants(
159
+ property: 'gap' | 'padding' | 'paddingVertical' | 'paddingHorizontal' | 'margin' | 'marginVertical' | 'marginHorizontal'
160
+ ): Record<string, any> {
161
+ const variants: Record<string, any> = {
162
+ none: {},
163
+ };
164
+
165
+ // Map property to actual CSS properties
166
+ const propertyMap: Record<string, string[]> = {
167
+ gap: ['gap'],
168
+ padding: ['padding'],
169
+ paddingVertical: ['paddingTop', 'paddingBottom'],
170
+ paddingHorizontal: ['paddingLeft', 'paddingRight'],
171
+ margin: ['margin'],
172
+ marginVertical: ['marginTop', 'marginBottom'],
173
+ marginHorizontal: ['marginLeft', 'marginRight'],
174
+ };
175
+
176
+ const cssProps = propertyMap[property];
177
+ const spacingValues = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 };
178
+
179
+ for (const [size, value] of Object.entries(spacingValues)) {
180
+ const styles: Record<string, number> = {};
181
+ for (const prop of cssProps) {
182
+ styles[prop] = value;
183
+ }
184
+ variants[size] = styles;
185
+ }
186
+
187
+ return variants;
188
+ }
189
+
190
+ /**
191
+ * Generate border variants for View.
192
+ */
193
+ function generateBorderVariants(): Record<string, any> {
194
+ return {
195
+ none: { borderWidth: 0 },
196
+ thin: {
197
+ borderWidth: 1,
198
+ borderStyle: 'solid',
199
+ borderColor: themeRef('colors.border.primary'),
200
+ },
201
+ thick: {
202
+ borderWidth: 2,
203
+ borderStyle: 'solid',
204
+ borderColor: themeRef('colors.border.primary'),
205
+ },
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Generate View component styles.
211
+ */
212
+ function generateViewStyles(theme: Theme, extensions?: ComponentExtensions): Record<string, any> {
213
+ let viewStyle: Record<string, any> = {
214
+ display: 'flex',
215
+ variants: {
216
+ background: generateBackgroundVariants(theme),
217
+ radius: generateRadiusVariants(theme),
218
+ border: generateBorderVariants(),
219
+ gap: generateSpacingVariants('gap'),
220
+ padding: generateSpacingVariants('padding'),
221
+ paddingVertical: generateSpacingVariants('paddingVertical'),
222
+ paddingHorizontal: generateSpacingVariants('paddingHorizontal'),
223
+ margin: generateSpacingVariants('margin'),
224
+ marginVertical: generateSpacingVariants('marginVertical'),
225
+ marginHorizontal: generateSpacingVariants('marginHorizontal'),
226
+ },
227
+ _web: {
228
+ display: 'flex',
229
+ flexDirection: 'column',
230
+ boxSizing: 'border-box',
231
+ },
232
+ };
233
+
234
+ // Apply extensions if provided
235
+ if (extensions?.View) {
236
+ const ext = typeof extensions.View === 'function'
237
+ ? extensions.View(theme)
238
+ : extensions.View;
239
+
240
+ if (ext.view) {
241
+ viewStyle = deepMerge(viewStyle, ext.view);
242
+ }
243
+ }
244
+
245
+ return { view: viewStyle };
246
+ }
247
+
248
+ // ============================================================================
249
+ // Screen Component Generator
250
+ // ============================================================================
251
+
252
+ /**
253
+ * Generate Screen component styles.
254
+ */
255
+ function generateScreenStyles(theme: Theme, extensions?: ComponentExtensions): Record<string, any> {
256
+ const backgroundVariants = generateBackgroundVariants(theme);
257
+
258
+ let screenStyle: Record<string, any> = {
259
+ flex: 1,
260
+ variants: {
261
+ background: backgroundVariants,
262
+ safeArea: { true: {}, false: {} },
263
+ gap: generateSpacingVariants('gap'),
264
+ padding: generateSpacingVariants('padding'),
265
+ paddingVertical: generateSpacingVariants('paddingVertical'),
266
+ paddingHorizontal: generateSpacingVariants('paddingHorizontal'),
267
+ margin: generateSpacingVariants('margin'),
268
+ marginVertical: generateSpacingVariants('marginVertical'),
269
+ marginHorizontal: generateSpacingVariants('marginHorizontal'),
270
+ },
271
+ _web: {
272
+ overflow: 'auto',
273
+ display: 'flex',
274
+ flexDirection: 'column',
275
+ minHeight: '100%',
276
+ boxSizing: 'border-box',
277
+ },
278
+ };
279
+
280
+ let screenContentStyle: Record<string, any> = {
281
+ variants: {
282
+ background: backgroundVariants,
283
+ safeArea: { true: {}, false: {} },
284
+ gap: generateSpacingVariants('gap'),
285
+ padding: generateSpacingVariants('padding'),
286
+ paddingVertical: generateSpacingVariants('paddingVertical'),
287
+ paddingHorizontal: generateSpacingVariants('paddingHorizontal'),
288
+ margin: generateSpacingVariants('margin'),
289
+ marginVertical: generateSpacingVariants('marginVertical'),
290
+ marginHorizontal: generateSpacingVariants('marginHorizontal'),
291
+ },
292
+ };
293
+
294
+ // Apply extensions
295
+ if (extensions?.Screen) {
296
+ const ext = typeof extensions.Screen === 'function'
297
+ ? extensions.Screen(theme)
298
+ : extensions.Screen;
299
+
300
+ if (ext.screen) {
301
+ screenStyle = deepMerge(screenStyle, ext.screen);
302
+ }
303
+ if (ext.screenContent) {
304
+ screenContentStyle = deepMerge(screenContentStyle, ext.screenContent);
305
+ }
306
+ }
307
+
308
+ return {
309
+ screen: screenStyle,
310
+ screenContent: screenContentStyle,
311
+ };
312
+ }
313
+
314
+ // ============================================================================
315
+ // Button Component Generator
316
+ // ============================================================================
317
+
318
+ /**
319
+ * Generate Button component styles with all intent/type combinations as compound variants.
320
+ */
321
+ function generateButtonStyles(theme: Theme, extensions?: ComponentExtensions): Record<string, any> {
322
+ const intents = Object.keys(theme.intents);
323
+ const types = ['contained', 'outlined', 'text'] as const;
324
+ const sizes = Object.keys(theme.sizes.button);
325
+
326
+ // Generate size variants using theme references
327
+ const sizeVariants: Record<string, any> = {};
328
+ for (const size of sizes) {
329
+ sizeVariants[size] = {
330
+ paddingVertical: themeRef(`sizes.button.${size}.paddingVertical`),
331
+ paddingHorizontal: themeRef(`sizes.button.${size}.paddingHorizontal`),
332
+ minHeight: themeRef(`sizes.button.${size}.minHeight`),
333
+ };
334
+ }
335
+
336
+ // Generate intent variants (empty - styles come from compound variants)
337
+ const intentVariants: Record<string, any> = {};
338
+ for (const intent of intents) {
339
+ intentVariants[intent] = {};
340
+ }
341
+
342
+ // Generate type variants (structure only)
343
+ const typeVariants = {
344
+ contained: { borderWidth: 0 },
345
+ outlined: { borderWidth: 1, borderStyle: 'solid' as const },
346
+ text: { borderWidth: 0 },
347
+ };
348
+
349
+ // Generate compound variants for all intent/type combinations
350
+ const compoundVariants: any[] = [];
351
+ for (const intent of intents) {
352
+ for (const type of types) {
353
+ const styles: Record<string, any> = {};
354
+
355
+ if (type === 'contained') {
356
+ styles.backgroundColor = themeRef(`intents.${intent}.primary`);
357
+ } else if (type === 'outlined') {
358
+ styles.backgroundColor = themeRef('colors.surface.primary');
359
+ styles.borderColor = themeRef(`intents.${intent}.primary`);
360
+ } else {
361
+ styles.backgroundColor = 'transparent';
362
+ }
363
+
364
+ compoundVariants.push({
365
+ intent,
366
+ type,
367
+ styles,
368
+ });
369
+ }
370
+ }
371
+
372
+ let buttonStyle: Record<string, any> = {
373
+ boxSizing: 'border-box',
374
+ alignItems: 'center',
375
+ justifyContent: 'center',
376
+ borderRadius: 8,
377
+ fontWeight: '600',
378
+ textAlign: 'center',
379
+ variants: {
380
+ size: sizeVariants,
381
+ intent: intentVariants,
382
+ type: typeVariants,
383
+ disabled: {
384
+ true: { opacity: 0.6 },
385
+ false: {
386
+ opacity: 1,
387
+ _web: {
388
+ cursor: 'pointer',
389
+ _hover: { opacity: 0.90 },
390
+ _active: { opacity: 0.75 },
391
+ },
392
+ },
393
+ },
394
+ gradient: {
395
+ darken: { _web: { backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(0, 0, 0, 0.15) 100%)' } },
396
+ lighten: { _web: { backgroundImage: 'linear-gradient(135deg, transparent 0%, rgba(255, 255, 255, 0.2) 100%)' } },
397
+ },
398
+ },
399
+ compoundVariants,
400
+ _web: {
401
+ display: 'flex',
402
+ transition: 'all 0.1s ease',
403
+ },
404
+ };
405
+
406
+ // Generate text styles with compound variants for color
407
+ const textCompoundVariants: any[] = [];
408
+ for (const intent of intents) {
409
+ for (const type of types) {
410
+ const color = type === 'contained'
411
+ ? themeRef(`intents.${intent}.contrast`)
412
+ : themeRef(`intents.${intent}.primary`);
413
+
414
+ textCompoundVariants.push({
415
+ intent,
416
+ type,
417
+ styles: { color },
418
+ });
419
+ }
420
+ }
421
+
422
+ const textSizeVariants: Record<string, any> = {};
423
+ for (const size of sizes) {
424
+ textSizeVariants[size] = {
425
+ fontSize: themeRef(`sizes.button.${size}.fontSize`),
426
+ lineHeight: themeRef(`sizes.button.${size}.fontSize`),
427
+ };
428
+ }
429
+
430
+ let textStyle: Record<string, any> = {
431
+ fontWeight: '600',
432
+ textAlign: 'center',
433
+ variants: {
434
+ size: textSizeVariants,
435
+ intent: intentVariants,
436
+ type: { contained: {}, outlined: {}, text: {} },
437
+ disabled: {
438
+ true: { opacity: 0.6 },
439
+ false: { opacity: 1 },
440
+ },
441
+ },
442
+ compoundVariants: textCompoundVariants,
443
+ };
444
+
445
+ // Generate icon styles with compound variants
446
+ const iconSizeVariants: Record<string, any> = {};
447
+ for (const size of sizes) {
448
+ iconSizeVariants[size] = {
449
+ width: themeRef(`sizes.button.${size}.iconSize`),
450
+ height: themeRef(`sizes.button.${size}.iconSize`),
451
+ };
452
+ }
453
+
454
+ let iconStyle: Record<string, any> = {
455
+ display: 'flex',
456
+ alignItems: 'center',
457
+ justifyContent: 'center',
458
+ variants: {
459
+ size: iconSizeVariants,
460
+ intent: intentVariants,
461
+ type: { contained: {}, outlined: {}, text: {} },
462
+ },
463
+ compoundVariants: textCompoundVariants, // Same colors as text
464
+ };
465
+
466
+ let iconContainerStyle: Record<string, any> = {
467
+ display: 'flex',
468
+ flexDirection: 'row',
469
+ alignItems: 'center',
470
+ justifyContent: 'center',
471
+ gap: 4,
472
+ };
473
+
474
+ // Apply extensions
475
+ if (extensions?.Button) {
476
+ const ext = typeof extensions.Button === 'function'
477
+ ? extensions.Button(theme)
478
+ : extensions.Button;
479
+
480
+ if (ext.button) buttonStyle = deepMerge(buttonStyle, ext.button);
481
+ if (ext.text) textStyle = deepMerge(textStyle, ext.text);
482
+ if (ext.icon) iconStyle = deepMerge(iconStyle, ext.icon);
483
+ if (ext.iconContainer) iconContainerStyle = deepMerge(iconContainerStyle, ext.iconContainer);
484
+ }
485
+
486
+ return {
487
+ button: buttonStyle,
488
+ text: textStyle,
489
+ icon: iconStyle,
490
+ iconContainer: iconContainerStyle,
491
+ };
492
+ }
493
+
494
+ // ============================================================================
495
+ // Text Component Generator
496
+ // ============================================================================
497
+
498
+ /**
499
+ * Generate Text component styles.
500
+ */
501
+ function generateTextStyles(theme: Theme, extensions?: ComponentExtensions): Record<string, any> {
502
+ // Generate typography variants
503
+ const typographyVariants: Record<string, any> = {};
504
+ for (const key in theme.sizes.typography) {
505
+ typographyVariants[key] = {
506
+ fontSize: themeRef(`sizes.typography.${key}.fontSize`),
507
+ lineHeight: themeRef(`sizes.typography.${key}.lineHeight`),
508
+ fontWeight: themeRef(`sizes.typography.${key}.fontWeight`),
509
+ };
510
+ }
511
+
512
+ // Generate color variants for text
513
+ const colorVariants: Record<string, any> = {};
514
+ for (const color in theme.colors.text) {
515
+ colorVariants[color] = {
516
+ color: themeRef(`colors.text.${color}`),
517
+ };
518
+ }
519
+
520
+ let textStyle: Record<string, any> = {
521
+ margin: 0,
522
+ padding: 0,
523
+ color: themeRef('colors.text.primary'),
524
+ variants: {
525
+ color: colorVariants,
526
+ typography: typographyVariants,
527
+ weight: {
528
+ light: { fontWeight: '300' },
529
+ normal: { fontWeight: '400' },
530
+ medium: { fontWeight: '500' },
531
+ semibold: { fontWeight: '600' },
532
+ bold: { fontWeight: '700' },
533
+ },
534
+ align: {
535
+ left: { textAlign: 'left' },
536
+ center: { textAlign: 'center' },
537
+ right: { textAlign: 'right' },
538
+ },
539
+ gap: generateSpacingVariants('gap'),
540
+ padding: generateSpacingVariants('padding'),
541
+ paddingVertical: generateSpacingVariants('paddingVertical'),
542
+ paddingHorizontal: generateSpacingVariants('paddingHorizontal'),
543
+ },
544
+ _web: {
545
+ fontFamily: 'inherit',
546
+ },
547
+ };
548
+
549
+ // Apply extensions
550
+ if (extensions?.Text) {
551
+ const ext = typeof extensions.Text === 'function'
552
+ ? extensions.Text(theme)
553
+ : extensions.Text;
554
+
555
+ if (ext.text) textStyle = deepMerge(textStyle, ext.text);
556
+ }
557
+
558
+ return { text: textStyle };
559
+ }
560
+
561
+ // ============================================================================
562
+ // Card Component Generator
563
+ // ============================================================================
564
+
565
+ /**
566
+ * Generate Card component styles.
567
+ */
568
+ function generateCardStyles(theme: Theme, extensions?: ComponentExtensions): Record<string, any> {
569
+ let cardStyle: Record<string, any> = {
570
+ backgroundColor: themeRef('colors.surface.primary'),
571
+ borderRadius: 8,
572
+ overflow: 'hidden',
573
+ variants: {
574
+ shadow: {
575
+ none: {},
576
+ sm: themeRef('shadows.sm'),
577
+ md: themeRef('shadows.md'),
578
+ lg: themeRef('shadows.lg'),
579
+ },
580
+ padding: generateSpacingVariants('padding'),
581
+ },
582
+ _web: {
583
+ boxSizing: 'border-box',
584
+ },
585
+ };
586
+
587
+ // Apply extensions
588
+ if (extensions?.Card) {
589
+ const ext = typeof extensions.Card === 'function'
590
+ ? extensions.Card(theme)
591
+ : extensions.Card;
592
+
593
+ if (ext.card) cardStyle = deepMerge(cardStyle, ext.card);
594
+ }
595
+
596
+ return { card: cardStyle };
597
+ }
598
+
599
+ // ============================================================================
600
+ // Input Component Generator
601
+ // ============================================================================
602
+
603
+ /**
604
+ * Generate Input component styles.
605
+ */
606
+ function generateInputStyles(theme: Theme, extensions?: ComponentExtensions): Record<string, any> {
607
+ const sizes = Object.keys(theme.sizes.input);
608
+
609
+ // Generate size variants
610
+ const sizeVariants: Record<string, any> = {};
611
+ for (const size of sizes) {
612
+ sizeVariants[size] = {
613
+ height: themeRef(`sizes.input.${size}.height`),
614
+ paddingHorizontal: themeRef(`sizes.input.${size}.paddingHorizontal`),
615
+ fontSize: themeRef(`sizes.input.${size}.fontSize`),
616
+ };
617
+ }
618
+
619
+ let wrapperStyle: Record<string, any> = {
620
+ width: '100%',
621
+ };
622
+
623
+ let inputStyle: Record<string, any> = {
624
+ borderWidth: 1,
625
+ borderStyle: 'solid',
626
+ borderColor: themeRef('colors.border.primary'),
627
+ borderRadius: 8,
628
+ backgroundColor: themeRef('colors.surface.primary'),
629
+ color: themeRef('colors.text.primary'),
630
+ variants: {
631
+ size: sizeVariants,
632
+ focused: {
633
+ true: {
634
+ borderColor: themeRef('interaction.focusBorder'),
635
+ },
636
+ false: {},
637
+ },
638
+ error: {
639
+ true: {
640
+ borderColor: themeRef('intents.error.primary'),
641
+ },
642
+ false: {},
643
+ },
644
+ disabled: {
645
+ true: {
646
+ opacity: 0.6,
647
+ },
648
+ false: {},
649
+ },
650
+ },
651
+ _web: {
652
+ outline: 'none',
653
+ boxSizing: 'border-box',
654
+ },
655
+ };
656
+
657
+ let labelStyle: Record<string, any> = {
658
+ color: themeRef('colors.text.primary'),
659
+ marginBottom: 4,
660
+ fontSize: 14,
661
+ fontWeight: '500',
662
+ };
663
+
664
+ let hintStyle: Record<string, any> = {
665
+ color: themeRef('colors.text.secondary'),
666
+ marginTop: 4,
667
+ fontSize: 12,
668
+ variants: {
669
+ error: {
670
+ true: { color: themeRef('intents.error.primary') },
671
+ false: {},
672
+ },
673
+ },
674
+ };
675
+
676
+ // Apply extensions
677
+ if (extensions?.Input) {
678
+ const ext = typeof extensions.Input === 'function'
679
+ ? extensions.Input(theme)
680
+ : extensions.Input;
681
+
682
+ if (ext.wrapper) wrapperStyle = deepMerge(wrapperStyle, ext.wrapper);
683
+ if (ext.input) inputStyle = deepMerge(inputStyle, ext.input);
684
+ if (ext.label) labelStyle = deepMerge(labelStyle, ext.label);
685
+ if (ext.hint) hintStyle = deepMerge(hintStyle, ext.hint);
686
+ }
687
+
688
+ return {
689
+ wrapper: wrapperStyle,
690
+ input: inputStyle,
691
+ label: labelStyle,
692
+ hint: hintStyle,
693
+ };
694
+ }
695
+
696
+ // ============================================================================
697
+ // Code Generation
698
+ // ============================================================================
699
+
700
+ /**
701
+ * Generate code for a component's StyleSheet.create call.
702
+ */
703
+ function generateComponentCode(
704
+ componentName: string,
705
+ styles: Record<string, any>,
706
+ exportName: string
707
+ ): string {
708
+ const stylesCode = valueToCode(styles, 1);
709
+
710
+ return `// GENERATED FILE - DO NOT EDIT DIRECTLY
711
+ // Generated by @idealyst/theme from idealyst.config.ts
712
+
713
+ import { StyleSheet } from 'react-native-unistyles';
714
+
715
+ export const ${exportName} = StyleSheet.create((theme) => (${stylesCode}));
716
+ `;
717
+ }
718
+
719
+ /**
720
+ * Generate Unistyles configuration code.
721
+ */
722
+ function generateUnistylesConfig(config: IdealystConfig): string {
723
+ const themeNames = Object.keys(config.themes);
724
+
725
+ const themeImports = themeNames
726
+ .map(name => `import { ${name}Theme } from './${name}Theme.generated';`)
727
+ .join('\n');
728
+
729
+ const themeDeclarations = themeNames
730
+ .map(name => ` ${name}: typeof ${name}Theme;`)
731
+ .join('\n');
732
+
733
+ const themeAssignments = themeNames
734
+ .map(name => ` ${name}: ${name}Theme,`)
735
+ .join('\n');
736
+
737
+ return `// GENERATED FILE - DO NOT EDIT DIRECTLY
738
+ // Generated by @idealyst/theme from idealyst.config.ts
739
+
740
+ import { StyleSheet } from 'react-native-unistyles';
741
+ ${themeImports}
742
+
743
+ // Unistyles theme type declarations
744
+ declare module 'react-native-unistyles' {
745
+ export interface UnistylesThemes {
746
+ ${themeDeclarations}
747
+ }
748
+ }
749
+
750
+ // Configure Unistyles
751
+ StyleSheet.configure({
752
+ settings: {
753
+ initialTheme: '${themeNames[0]}',
754
+ },
755
+ themes: {
756
+ ${themeAssignments}
757
+ }
758
+ });
759
+
760
+ export const unistylesConfigured = true;
761
+ `;
762
+ }
763
+
764
+ /**
765
+ * Main generator function.
766
+ * Returns an object with all generated file contents.
767
+ */
768
+ export function generateStyles(config: IdealystConfig): Record<string, string> {
769
+ const files: Record<string, string> = {};
770
+
771
+ // Use light theme as reference for generating styles
772
+ // (variants are the same across themes, only values differ)
773
+ const referenceTheme = config.themes.light;
774
+
775
+ // Generate View styles
776
+ const viewStyles = generateViewStyles(referenceTheme, config.extensions);
777
+ files['View.styles.generated.ts'] = generateComponentCode('View', viewStyles, 'viewStyles');
778
+
779
+ // Generate Screen styles
780
+ const screenStyles = generateScreenStyles(referenceTheme, config.extensions);
781
+ files['Screen.styles.generated.ts'] = generateComponentCode('Screen', screenStyles, 'screenStyles');
782
+
783
+ // Generate Button styles
784
+ const buttonStyles = generateButtonStyles(referenceTheme, config.extensions);
785
+ files['Button.styles.generated.ts'] = generateComponentCode('Button', buttonStyles, 'buttonStyles');
786
+
787
+ // Generate Text styles
788
+ const textStyles = generateTextStyles(referenceTheme, config.extensions);
789
+ files['Text.styles.generated.ts'] = generateComponentCode('Text', textStyles, 'textStyles');
790
+
791
+ // Generate Card styles
792
+ const cardStyles = generateCardStyles(referenceTheme, config.extensions);
793
+ files['Card.styles.generated.ts'] = generateComponentCode('Card', cardStyles, 'cardStyles');
794
+
795
+ // Generate Input styles
796
+ const inputStyles = generateInputStyles(referenceTheme, config.extensions);
797
+ files['Input.styles.generated.ts'] = generateComponentCode('Input', inputStyles, 'inputStyles');
798
+
799
+ // Generate Unistyles config
800
+ files['unistyles.generated.ts'] = generateUnistylesConfig(config);
801
+
802
+ // Generate theme files (export the theme objects as-is)
803
+ for (const [name, theme] of Object.entries(config.themes)) {
804
+ files[`${name}Theme.generated.ts`] = `// GENERATED FILE - DO NOT EDIT DIRECTLY
805
+ // Generated by @idealyst/theme from idealyst.config.ts
806
+
807
+ export const ${name}Theme = ${JSON.stringify(theme, null, 2)};
808
+ `;
809
+ }
810
+
811
+ return files;
812
+ }
813
+
814
+ /**
815
+ * Export types for external use.
816
+ */
817
+ export type { IdealystConfig, ComponentExtensions };