@idealyst/theme 1.1.6 → 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.
- package/package.json +29 -1
- package/src/babel/index.ts +9 -0
- package/src/babel/plugin.js +871 -0
- package/src/babel/plugin.ts +187 -0
- package/src/babel/runtime.ts +94 -0
- package/src/babel/theme-analyzer.js +357 -0
- package/src/builder.ts +317 -0
- package/src/componentStyles.ts +93 -0
- package/src/config/cli.ts +95 -0
- package/src/config/generator.ts +817 -0
- package/src/config/index.ts +10 -0
- package/src/config/types.ts +112 -0
- package/src/darkTheme.ts +186 -943
- package/src/extensions.ts +110 -0
- package/src/helpers.ts +206 -0
- package/src/index.ts +18 -4
- package/src/lightTheme.ts +286 -859
- package/src/styleBuilder.ts +108 -0
- package/src/theme/color.ts +36 -15
- package/src/theme/extensions.ts +99 -0
- package/src/theme/index.ts +41 -27
- package/src/theme/intent.ts +12 -7
- package/src/theme/shadow.ts +12 -16
- package/src/theme/size.ts +6 -202
- package/src/theme/structures.ts +237 -0
- package/src/unistyles.ts +8 -24
- package/src/variants/color.ts +3 -5
|
@@ -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 };
|