@idealyst/theme 1.1.7 → 1.1.9

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,112 @@
1
+ import { UnistylesRuntime } from 'react-native-unistyles';
2
+ import { Breakpoint, BreakpointsRecord } from './theme/breakpoint';
3
+
4
+ /**
5
+ * Get the current active breakpoint name.
6
+ *
7
+ * @returns The current breakpoint name, or undefined if not available
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const current = getCurrentBreakpoint();
12
+ * console.log(current); // 'md'
13
+ * ```
14
+ */
15
+ export function getCurrentBreakpoint(): Breakpoint | undefined {
16
+ return UnistylesRuntime.breakpoint as Breakpoint | undefined;
17
+ }
18
+
19
+ /**
20
+ * Get all registered breakpoints and their values.
21
+ *
22
+ * @returns Object mapping breakpoint names to pixel values
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const breakpoints = getBreakpoints();
27
+ * console.log(breakpoints); // { xs: 0, sm: 576, md: 768, lg: 992, xl: 1200 }
28
+ * ```
29
+ */
30
+ export function getBreakpoints(): BreakpointsRecord {
31
+ return UnistylesRuntime.breakpoints as BreakpointsRecord;
32
+ }
33
+
34
+ /**
35
+ * Check if the current viewport is at or above a specific breakpoint.
36
+ *
37
+ * @param breakpoint - The breakpoint to check against
38
+ * @returns True if the current viewport width is >= the breakpoint value
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * if (isBreakpointUp('md')) {
43
+ * // Tablet or larger
44
+ * }
45
+ * ```
46
+ */
47
+ export function isBreakpointUp(breakpoint: Breakpoint): boolean {
48
+ const breakpoints = getBreakpoints();
49
+ const screenWidth = UnistylesRuntime.screen.width;
50
+ return screenWidth >= breakpoints[breakpoint];
51
+ }
52
+
53
+ /**
54
+ * Check if the current viewport is below a specific breakpoint.
55
+ *
56
+ * @param breakpoint - The breakpoint to check against
57
+ * @returns True if the current viewport width is < the breakpoint value
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * if (isBreakpointDown('md')) {
62
+ * // Mobile only (below tablet)
63
+ * }
64
+ * ```
65
+ */
66
+ export function isBreakpointDown(breakpoint: Breakpoint): boolean {
67
+ return !isBreakpointUp(breakpoint);
68
+ }
69
+
70
+ /**
71
+ * Resolve a responsive value to its current breakpoint value.
72
+ * Falls back to the smallest defined breakpoint using CSS cascade behavior.
73
+ *
74
+ * @param value - Either a direct value or an object mapping breakpoints to values
75
+ * @returns The resolved value for the current breakpoint
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * // On a tablet (md breakpoint):
80
+ * const size = resolveResponsive({ xs: 'sm', md: 'lg' });
81
+ * console.log(size); // 'lg'
82
+ *
83
+ * // On a phone (xs breakpoint):
84
+ * const size = resolveResponsive({ xs: 'sm', md: 'lg' });
85
+ * console.log(size); // 'sm'
86
+ * ```
87
+ */
88
+ export function resolveResponsive<T>(value: T | Partial<Record<Breakpoint, T>>): T | undefined {
89
+ // If not an object, return directly
90
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
91
+ return value as T;
92
+ }
93
+
94
+ const responsiveValue = value as Partial<Record<Breakpoint, T>>;
95
+ const breakpoints = getBreakpoints();
96
+ const screenWidth = UnistylesRuntime.screen.width;
97
+
98
+ // Sort breakpoints by value descending
99
+ const sortedBps = Object.entries(breakpoints)
100
+ .sort(([, a], [, b]) => b - a)
101
+ .map(([name]) => name as Breakpoint);
102
+
103
+ // Find the largest breakpoint that matches current screen width
104
+ // and has a defined value (CSS cascade behavior)
105
+ for (const bp of sortedBps) {
106
+ if (screenWidth >= breakpoints[bp] && responsiveValue[bp] !== undefined) {
107
+ return responsiveValue[bp];
108
+ }
109
+ }
110
+
111
+ return undefined;
112
+ }
package/src/builder.ts CHANGED
@@ -42,6 +42,7 @@ export type BuiltTheme<
42
42
  TText extends string,
43
43
  TBorder extends string,
44
44
  TSize extends string,
45
+ TBreakpoints extends string = never,
45
46
  > = {
46
47
  intents: Record<TIntents, IntentValue>;
47
48
  radii: Record<TRadii, number>;
@@ -78,6 +79,7 @@ export type BuiltTheme<
78
79
  typography: Record<Typography, TypographyValue>;
79
80
  };
80
81
  interaction: InteractionConfig;
82
+ breakpoints: Record<TBreakpoints, number>;
81
83
  };
82
84
 
83
85
  /**
@@ -92,7 +94,8 @@ type ThemeConfig<
92
94
  TText extends string,
93
95
  TBorder extends string,
94
96
  TSize extends string,
95
- > = BuiltTheme<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize>;
97
+ TBreakpoints extends string,
98
+ > = BuiltTheme<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints>;
96
99
 
97
100
  /**
98
101
  * Fluent builder for creating themes with full TypeScript inference.
@@ -127,8 +130,9 @@ export class ThemeBuilder<
127
130
  TText extends string = never,
128
131
  TBorder extends string = never,
129
132
  TSize extends string = never,
133
+ TBreakpoints extends string = never,
130
134
  > {
131
- private config: ThemeConfig<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize>;
135
+ private config: ThemeConfig<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints>;
132
136
 
133
137
  constructor() {
134
138
  this.config = {
@@ -143,6 +147,7 @@ export class ThemeBuilder<
143
147
  },
144
148
  sizes: {} as any,
145
149
  interaction: {} as any,
150
+ breakpoints: {} as any,
146
151
  };
147
152
  }
148
153
 
@@ -152,8 +157,8 @@ export class ThemeBuilder<
152
157
  addIntent<K extends string>(
153
158
  name: K,
154
159
  value: IntentValue
155
- ): ThemeBuilder<TIntents | K, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize> {
156
- const newBuilder = new ThemeBuilder<TIntents | K, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize>();
160
+ ): ThemeBuilder<TIntents | K, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints> {
161
+ const newBuilder = new ThemeBuilder<TIntents | K, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints>();
157
162
  newBuilder.config = {
158
163
  ...this.config,
159
164
  intents: {
@@ -170,8 +175,8 @@ export class ThemeBuilder<
170
175
  addRadius<K extends string>(
171
176
  name: K,
172
177
  value: number
173
- ): ThemeBuilder<TIntents, TRadii | K, TShadows, TPallet, TSurface, TText, TBorder, TSize> {
174
- const newBuilder = new ThemeBuilder<TIntents, TRadii | K, TShadows, TPallet, TSurface, TText, TBorder, TSize>();
178
+ ): ThemeBuilder<TIntents, TRadii | K, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints> {
179
+ const newBuilder = new ThemeBuilder<TIntents, TRadii | K, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints>();
175
180
  newBuilder.config = {
176
181
  ...this.config,
177
182
  radii: {
@@ -188,8 +193,8 @@ export class ThemeBuilder<
188
193
  addShadow<K extends string>(
189
194
  name: K,
190
195
  value: ShadowValue
191
- ): ThemeBuilder<TIntents, TRadii, TShadows | K, TPallet, TSurface, TText, TBorder, TSize> {
192
- const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows | K, TPallet, TSurface, TText, TBorder, TSize>();
196
+ ): ThemeBuilder<TIntents, TRadii, TShadows | K, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints> {
197
+ const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows | K, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints>();
193
198
  newBuilder.config = {
194
199
  ...this.config,
195
200
  shadows: {
@@ -205,8 +210,8 @@ export class ThemeBuilder<
205
210
  */
206
211
  setInteraction(
207
212
  interaction: InteractionConfig
208
- ): ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize> {
209
- const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize>();
213
+ ): ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints> {
214
+ const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints>();
210
215
  newBuilder.config = {
211
216
  ...this.config,
212
217
  interaction,
@@ -227,8 +232,8 @@ export class ThemeBuilder<
227
232
  surface: Record<S, ColorValue>;
228
233
  text: Record<T, ColorValue>;
229
234
  border: Record<B, ColorValue>;
230
- }): ThemeBuilder<TIntents, TRadii, TShadows, P, S, T, B, TSize> {
231
- const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows, P, S, T, B, TSize>();
235
+ }): ThemeBuilder<TIntents, TRadii, TShadows, P, S, T, B, TSize, TBreakpoints> {
236
+ const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows, P, S, T, B, TSize, TBreakpoints>();
232
237
  newBuilder.config = {
233
238
  ...this.config,
234
239
  colors,
@@ -263,8 +268,8 @@ export class ThemeBuilder<
263
268
  tooltip: Record<S, TooltipSizeValue>;
264
269
  view: Record<S, ViewSizeValue>;
265
270
  typography: Record<Typography, TypographyValue>;
266
- }): ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, S> {
267
- const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, S>();
271
+ }): ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, S, TBreakpoints> {
272
+ const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, S, TBreakpoints>();
268
273
  newBuilder.config = {
269
274
  ...this.config,
270
275
  sizes,
@@ -272,10 +277,75 @@ export class ThemeBuilder<
272
277
  return newBuilder;
273
278
  }
274
279
 
280
+ /**
281
+ * Add a single breakpoint to the theme.
282
+ *
283
+ * IMPORTANT: At least one breakpoint must have value 0 (typically 'xs').
284
+ * This simulates CSS cascading behavior in Unistyles.
285
+ *
286
+ * @param name - The breakpoint name (e.g., 'xs', 'sm', 'md')
287
+ * @param value - The minimum width in pixels for this breakpoint
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * createTheme()
292
+ * .addBreakpoint('xs', 0)
293
+ * .addBreakpoint('sm', 576)
294
+ * .addBreakpoint('md', 768)
295
+ * .build();
296
+ * ```
297
+ */
298
+ addBreakpoint<K extends string>(
299
+ name: K,
300
+ value: number
301
+ ): ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints | K> {
302
+ const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints | K>();
303
+ newBuilder.config = {
304
+ ...this.config,
305
+ breakpoints: {
306
+ ...this.config.breakpoints,
307
+ [name]: value,
308
+ } as any,
309
+ };
310
+ return newBuilder;
311
+ }
312
+
313
+ /**
314
+ * Set all breakpoints at once.
315
+ *
316
+ * IMPORTANT: At least one breakpoint must have value 0.
317
+ * Breakpoints define responsive behavior based on viewport width.
318
+ *
319
+ * @param breakpoints - Object mapping breakpoint names to pixel values
320
+ *
321
+ * @example
322
+ * ```typescript
323
+ * createTheme()
324
+ * .setBreakpoints({
325
+ * xs: 0, // Required: starts at 0
326
+ * sm: 576,
327
+ * md: 768,
328
+ * lg: 992,
329
+ * xl: 1200,
330
+ * })
331
+ * .build();
332
+ * ```
333
+ */
334
+ setBreakpoints<B extends Record<string, number>>(
335
+ breakpoints: B
336
+ ): ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, keyof B & string> {
337
+ const newBuilder = new ThemeBuilder<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, keyof B & string>();
338
+ newBuilder.config = {
339
+ ...this.config,
340
+ breakpoints,
341
+ } as any;
342
+ return newBuilder;
343
+ }
344
+
275
345
  /**
276
346
  * Build the final theme object.
277
347
  */
278
- build(): BuiltTheme<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize> {
348
+ build(): BuiltTheme<TIntents, TRadii, TShadows, TPallet, TSurface, TText, TBorder, TSize, TBreakpoints> {
279
349
  return this.config;
280
350
  }
281
351
  }
@@ -290,7 +360,7 @@ export function createTheme(): ThemeBuilder {
290
360
  /**
291
361
  * Create a builder from an existing theme to add more values.
292
362
  */
293
- export function fromTheme<T extends BuiltTheme<any, any, any, any, any, any, any, any>>(
363
+ export function fromTheme<T extends BuiltTheme<any, any, any, any, any, any, any, any, any>>(
294
364
  base: T
295
365
  ): ThemeBuilder<
296
366
  keyof T['intents'] & string,
@@ -300,7 +370,8 @@ export function fromTheme<T extends BuiltTheme<any, any, any, any, any, any, any
300
370
  keyof T['colors']['surface'] & string,
301
371
  keyof T['colors']['text'] & string,
302
372
  keyof T['colors']['border'] & string,
303
- keyof T['sizes']['button'] & string
373
+ keyof T['sizes']['button'] & string,
374
+ keyof T['breakpoints'] & string
304
375
  > {
305
376
  const builder = new ThemeBuilder<
306
377
  keyof T['intents'] & string,
@@ -310,7 +381,8 @@ export function fromTheme<T extends BuiltTheme<any, any, any, any, any, any, any
310
381
  keyof T['colors']['surface'] & string,
311
382
  keyof T['colors']['text'] & string,
312
383
  keyof T['colors']['border'] & string,
313
- keyof T['sizes']['button'] & string
384
+ keyof T['sizes']['button'] & string,
385
+ keyof T['breakpoints'] & string
314
386
  >();
315
387
  (builder as any).config = { ...base };
316
388
  return builder;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Component Style Types Registry
3
+ *
4
+ * This module provides type definitions for component styles used with
5
+ * defineStyle, extendStyle, and overrideStyle.
6
+ *
7
+ * Components register their style types via module augmentation:
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // In Text.styles.tsx
12
+ * declare module '@idealyst/theme' {
13
+ * interface ComponentStyleRegistry {
14
+ * Text: TextStyleDef;
15
+ * }
16
+ * }
17
+ * ```
18
+ */
19
+
20
+ import type { TextStyle, ViewStyle } from 'react-native';
21
+
22
+ /**
23
+ * Registry interface that components augment to register their style types.
24
+ * This enables type-safe extendStyle and overrideStyle calls.
25
+ */
26
+ export interface ComponentStyleRegistry {
27
+ // Components augment this interface to add their style types
28
+ // Example: Text: { text: (params: TextStyleParams) => TextStyleObject }
29
+ }
30
+
31
+ /**
32
+ * Get the style definition type for a component.
33
+ * Returns the registered type or a loose Record type for unregistered components.
34
+ */
35
+ export type ComponentStyleDef<K extends string> = K extends keyof ComponentStyleRegistry
36
+ ? ComponentStyleRegistry[K]
37
+ : Record<string, any>;
38
+
39
+ /**
40
+ * Deep partial type that works with functions.
41
+ * For style functions, preserves the function signature but makes the return type partial.
42
+ */
43
+ export type DeepPartialStyle<T> = T extends (...args: infer A) => infer R
44
+ ? (...args: A) => DeepPartialStyle<R>
45
+ : T extends object
46
+ ? { [K in keyof T]?: DeepPartialStyle<T[K]> }
47
+ : T;
48
+
49
+ /**
50
+ * Style definition for extendStyle - requires functions with same params as base.
51
+ * All style properties must be functions to access dynamic params.
52
+ */
53
+ export type ExtendStyleDef<K extends string> = DeepPartialStyle<ComponentStyleDef<K>>;
54
+
55
+ /**
56
+ * Style definition for overrideStyle - requires full implementation with functions.
57
+ */
58
+ export type OverrideStyleDef<K extends string> = ComponentStyleDef<K>;
59
+
60
+ /**
61
+ * Helper to extract the params type from a dynamic style function.
62
+ * Use this to type your extension functions.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * type TextParams = StyleParams<TextStyleDef['text']>;
67
+ * // TextParams = { color?: TextColorVariant }
68
+ * ```
69
+ */
70
+ export type StyleParams<T> = T extends (params: infer P) => any ? P : never;
71
+
72
+ // =============================================================================
73
+ // Common Style Types
74
+ // =============================================================================
75
+
76
+ /**
77
+ * Base style object with optional variants and compound variants.
78
+ */
79
+ export interface StyleWithVariants<TVariants extends Record<string, any> = Record<string, any>> {
80
+ variants?: {
81
+ [K in keyof TVariants]?: {
82
+ [V in TVariants[K] extends string | boolean ? TVariants[K] : string]?: ViewStyle | TextStyle;
83
+ };
84
+ };
85
+ compoundVariants?: Array<{
86
+ [K in keyof TVariants]?: TVariants[K];
87
+ } & { styles: ViewStyle | TextStyle }>;
88
+ }
89
+
90
+ /**
91
+ * Dynamic style function type.
92
+ */
93
+ export type DynamicStyleFn<TParams, TStyle> = (params: TParams) => TStyle;
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Idealyst Style Generator CLI
4
+ *
5
+ * Reads idealyst.config.ts and generates flat Unistyles-compatible style files.
6
+ *
7
+ * Usage:
8
+ * npx ts-node packages/theme/src/config/cli.ts [config-path] [output-dir]
9
+ *
10
+ * Defaults:
11
+ * config-path: ./idealyst.config.ts
12
+ * output-dir: ./generated
13
+ */
14
+
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+
18
+ // Dynamic import for ESM compatibility
19
+ async function main() {
20
+ const args = process.argv.slice(2);
21
+ const configPath = args[0] || './idealyst.config.ts';
22
+ const outputDir = args[1] || './generated';
23
+
24
+ console.log('🎨 Idealyst Style Generator');
25
+ console.log(` Config: ${configPath}`);
26
+ console.log(` Output: ${outputDir}`);
27
+ console.log('');
28
+
29
+ // Resolve paths
30
+ const resolvedConfigPath = path.resolve(process.cwd(), configPath);
31
+ const resolvedOutputDir = path.resolve(process.cwd(), outputDir);
32
+
33
+ // Check config exists
34
+ if (!fs.existsSync(resolvedConfigPath)) {
35
+ console.error(`❌ Config file not found: ${resolvedConfigPath}`);
36
+ console.error('');
37
+ console.error('Create an idealyst.config.ts file with your theme configuration.');
38
+ console.error('See the documentation for examples.');
39
+ process.exit(1);
40
+ }
41
+
42
+ // Import config dynamically
43
+ // Note: This requires ts-node or a pre-compiled config
44
+ let config;
45
+ try {
46
+ // Try to import the config
47
+ const configModule = await import(resolvedConfigPath);
48
+ config = configModule.default || configModule;
49
+ } catch (err) {
50
+ console.error(`❌ Failed to load config: ${err}`);
51
+ console.error('');
52
+ console.error('Make sure your config file:');
53
+ console.error(' 1. Is a valid TypeScript/JavaScript file');
54
+ console.error(' 2. Uses export default or named exports');
55
+ console.error(' 3. Has all required theme properties');
56
+ process.exit(1);
57
+ }
58
+
59
+ // Validate config
60
+ if (!config.themes || !config.themes.light || !config.themes.dark) {
61
+ console.error('❌ Invalid config: themes.light and themes.dark are required');
62
+ process.exit(1);
63
+ }
64
+
65
+ // Import generator
66
+ const { generateStyles } = await import('./generator');
67
+
68
+ // Generate styles
69
+ console.log('⚙️ Generating styles...');
70
+ const files = generateStyles(config);
71
+
72
+ // Ensure output directory exists
73
+ if (!fs.existsSync(resolvedOutputDir)) {
74
+ fs.mkdirSync(resolvedOutputDir, { recursive: true });
75
+ }
76
+
77
+ // Write files
78
+ for (const [filename, content] of Object.entries(files)) {
79
+ const filePath = path.join(resolvedOutputDir, filename);
80
+ fs.writeFileSync(filePath, content, 'utf-8');
81
+ console.log(` ✓ ${filename}`);
82
+ }
83
+
84
+ console.log('');
85
+ console.log(`✅ Generated ${Object.keys(files).length} files in ${outputDir}`);
86
+ console.log('');
87
+ console.log('Next steps:');
88
+ console.log(' 1. Import the generated unistyles.generated.ts in your app entry');
89
+ console.log(' 2. Update components to import from generated style files');
90
+ }
91
+
92
+ main().catch((err) => {
93
+ console.error('Fatal error:', err);
94
+ process.exit(1);
95
+ });