@idealyst/theme 1.1.8 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/theme",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "Theming system for Idealyst Framework",
5
5
  "readme": "README.md",
6
6
  "main": "src/index.ts",
@@ -70,6 +70,7 @@
70
70
  "@babel/types": "^7.28.5",
71
71
  "@types/babel__core": "^7.20.0",
72
72
  "@types/react": "^19.1.0",
73
+ "@types/react-native": "^0.73.0",
73
74
  "react-native-unistyles": ">=3.0.0",
74
75
  "typescript": "^5.0.0"
75
76
  },
@@ -214,7 +214,19 @@ function expandIterators(t, callback, themeParam, keys, verbose, expandedVariant
214
214
  }
215
215
 
216
216
  if (t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) {
217
- const processedBody = processNode(node.body, depth + 1);
217
+ let processedBody = processNode(node.body, depth + 1);
218
+
219
+ // Convert block body with single return to arrow notation for Unistyles compatibility
220
+ // This transforms: (props) => { return { ... } }
221
+ // Into: (props) => ({ ... })
222
+ if (t.isBlockStatement(processedBody) && processedBody.body.length === 1) {
223
+ const singleStmt = processedBody.body[0];
224
+ if (t.isReturnStatement(singleStmt) && singleStmt.argument) {
225
+ // Use the return argument directly as the arrow function body
226
+ processedBody = singleStmt.argument;
227
+ }
228
+ }
229
+
218
230
  return t.arrowFunctionExpression(
219
231
  node.params,
220
232
  processedBody
@@ -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;
package/src/darkTheme.ts CHANGED
@@ -54,15 +54,15 @@ export const darkTheme = createTheme()
54
54
  // Shadows (higher opacity for dark backgrounds)
55
55
  .addShadow('none', {})
56
56
  .addShadow('sm', {
57
- elevation: 1,
57
+ elevation: 2,
58
58
  shadowColor: '#000000',
59
59
  shadowOffset: { width: 0, height: 1 },
60
60
  shadowOpacity: 0.18,
61
- shadowRadius: 1,
61
+ shadowRadius: 2,
62
62
  boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.18)',
63
63
  })
64
64
  .addShadow('md', {
65
- elevation: 3,
65
+ elevation: 4,
66
66
  shadowColor: '#000000',
67
67
  shadowOffset: { width: 0, height: 3 },
68
68
  shadowOpacity: 0.2,
@@ -70,7 +70,7 @@ export const darkTheme = createTheme()
70
70
  boxShadow: '0px 3px 9.3px rgba(0, 0, 0, 0.2)',
71
71
  })
72
72
  .addShadow('lg', {
73
- elevation: 6,
73
+ elevation: 8,
74
74
  shadowColor: '#000000',
75
75
  shadowOffset: { width: 0, height: 6 },
76
76
  shadowOpacity: 0.23,
@@ -89,7 +89,8 @@ export const darkTheme = createTheme()
89
89
  .setColors({
90
90
  pallet: generateDarkColorPallette(),
91
91
  surface: {
92
- primary: '#121212',
92
+ screen: '#121212',
93
+ primary: '#1e1e1e',
93
94
  secondary: '#1e1e1e',
94
95
  tertiary: '#2a2a2a',
95
96
  inverse: '#ffffff',
@@ -123,6 +124,14 @@ export const darkTheme = createTheme()
123
124
  disabled: 0.4,
124
125
  },
125
126
  })
127
+ // Breakpoints (same as light theme)
128
+ .setBreakpoints({
129
+ xs: 0, // Extra small devices (portrait phones)
130
+ sm: 576, // Small devices (landscape phones)
131
+ md: 768, // Medium devices (tablets)
132
+ lg: 992, // Large devices (desktops)
133
+ xl: 1200, // Extra large devices (large desktops)
134
+ })
126
135
  .build();
127
136
 
128
137
  // =============================================================================
package/src/index.ts CHANGED
@@ -18,4 +18,9 @@ export * from './styles';
18
18
  export * from './helpers';
19
19
  export * from './styleBuilder';
20
20
  export * from './extensions';
21
- export * from './componentStyles';
21
+ export * from './componentStyles';
22
+
23
+ // Responsive utilities
24
+ export * from './responsive';
25
+ export * from './breakpoints';
26
+ export * from './useResponsiveStyle';
package/src/lightTheme.ts CHANGED
@@ -88,6 +88,7 @@ export const lightTheme = createTheme()
88
88
  .setColors({
89
89
  pallet: generateColorPallette(),
90
90
  surface: {
91
+ screen: '#ffffff',
91
92
  primary: '#ffffff',
92
93
  secondary: '#f5f5f5',
93
94
  tertiary: '#e0e0e0',
@@ -155,11 +156,11 @@ export const lightTheme = createTheme()
155
156
  xl: { radioSize: 26, radioDotSize: 20, fontSize: 20, gap: 12 },
156
157
  },
157
158
  select: {
158
- xs: { paddingHorizontal: 8, minHeight: 28, fontSize: 12, iconSize: 16 },
159
- sm: { paddingHorizontal: 10, minHeight: 36, fontSize: 14, iconSize: 18 },
160
- md: { paddingHorizontal: 12, minHeight: 44, fontSize: 16, iconSize: 20 },
161
- lg: { paddingHorizontal: 16, minHeight: 52, fontSize: 18, iconSize: 24 },
162
- xl: { paddingHorizontal: 20, minHeight: 60, fontSize: 20, iconSize: 28 },
159
+ xs: { paddingHorizontal: 8, minHeight: 28, fontSize: 12, iconSize: 16, borderRadius: 4 },
160
+ sm: { paddingHorizontal: 10, minHeight: 36, fontSize: 14, iconSize: 18, borderRadius: 4 },
161
+ md: { paddingHorizontal: 12, minHeight: 44, fontSize: 16, iconSize: 20, borderRadius: 4 },
162
+ lg: { paddingHorizontal: 16, minHeight: 52, fontSize: 18, iconSize: 24, borderRadius: 4 },
163
+ xl: { paddingHorizontal: 20, minHeight: 60, fontSize: 20, iconSize: 28, borderRadius: 4 },
163
164
  },
164
165
  slider: {
165
166
  xs: { trackHeight: 2, thumbSize: 12, thumbIconSize: 8, markHeight: 6, labelFontSize: 10 },
@@ -290,6 +291,14 @@ export const lightTheme = createTheme()
290
291
  disabled: 0.5,
291
292
  },
292
293
  })
294
+ // Breakpoints
295
+ .setBreakpoints({
296
+ xs: 0, // Extra small devices (portrait phones)
297
+ sm: 576, // Small devices (landscape phones)
298
+ md: 768, // Medium devices (tablets)
299
+ lg: 992, // Large devices (desktops)
300
+ xl: 1200, // Extra large devices (large desktops)
301
+ })
293
302
  .build();
294
303
 
295
304
  // =============================================================================
@@ -0,0 +1,123 @@
1
+ import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
2
+ import { UnistylesRuntime } from 'react-native-unistyles';
3
+ import { Breakpoint, BreakpointsRecord } from './theme/breakpoint';
4
+
5
+ /**
6
+ * Makes a value responsive - can be either a direct value or
7
+ * an object mapping breakpoint names to values.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // Direct value
12
+ * const size: Responsive<Size> = 'md';
13
+ *
14
+ * // Responsive value
15
+ * const size: Responsive<Size> = { xs: 'sm', md: 'lg' };
16
+ * ```
17
+ */
18
+ export type Responsive<T> = T | Partial<Record<Breakpoint, T>>;
19
+
20
+ /**
21
+ * Type guard to check if a value is a responsive object (breakpoint map).
22
+ *
23
+ * @param value - The value to check
24
+ * @returns True if the value is an object with breakpoint keys
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const size: Responsive<Size> = { xs: 'sm', md: 'lg' };
29
+ *
30
+ * if (isResponsiveValue(size)) {
31
+ * // size is Partial<Record<Breakpoint, Size>>
32
+ * console.log(size.xs); // 'sm'
33
+ * } else {
34
+ * // size is Size
35
+ * console.log(size); // 'md'
36
+ * }
37
+ * ```
38
+ */
39
+ export function isResponsiveValue<T>(value: Responsive<T>): value is Partial<Record<Breakpoint, T>> {
40
+ return (
41
+ typeof value === 'object' &&
42
+ value !== null &&
43
+ !Array.isArray(value) &&
44
+ !('$$typeof' in value) // Not a React element
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Style object where each property can be a responsive value.
50
+ */
51
+ export type ResponsiveStyle = {
52
+ [K in keyof ViewStyle]?: Responsive<ViewStyle[K]>;
53
+ } & {
54
+ [K in keyof TextStyle]?: Responsive<TextStyle[K]>;
55
+ } & {
56
+ [K in keyof ImageStyle]?: Responsive<ImageStyle[K]>;
57
+ };
58
+
59
+ /**
60
+ * Resolve a single responsive value to its current breakpoint value.
61
+ * Uses CSS cascade behavior - falls back to the nearest smaller breakpoint.
62
+ */
63
+ function resolveValue<T>(
64
+ value: Responsive<T>,
65
+ screenWidth: number,
66
+ breakpoints: BreakpointsRecord,
67
+ sortedBps: Breakpoint[]
68
+ ): T | undefined {
69
+ if (!isResponsiveValue(value)) {
70
+ return value;
71
+ }
72
+
73
+ // Find the largest breakpoint that matches current screen width
74
+ // and has a defined value (CSS cascade behavior)
75
+ for (const bp of sortedBps) {
76
+ if (screenWidth >= breakpoints[bp] && value[bp] !== undefined) {
77
+ return value[bp];
78
+ }
79
+ }
80
+
81
+ return undefined;
82
+ }
83
+
84
+ /**
85
+ * Resolve a responsive style object to concrete style values based on current breakpoint.
86
+ *
87
+ * This is a non-hook version for use outside of React components.
88
+ *
89
+ * @param style - Style object with responsive values
90
+ * @returns Resolved style object for the current breakpoint
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * const style = resolveResponsiveStyle({
95
+ * flexDirection: { xs: 'column', md: 'row' },
96
+ * padding: { xs: 8, lg: 16 },
97
+ * backgroundColor: '#fff', // Non-responsive values pass through
98
+ * });
99
+ * // On tablet: { flexDirection: 'row', padding: 8, backgroundColor: '#fff' }
100
+ * ```
101
+ */
102
+ export function resolveResponsiveStyle(style: ResponsiveStyle): ViewStyle & TextStyle & ImageStyle {
103
+ const breakpoints = UnistylesRuntime.breakpoints as BreakpointsRecord;
104
+ const screenWidth = UnistylesRuntime.screen.width;
105
+
106
+ // Sort breakpoints by value descending for cascade lookup
107
+ const sortedBps = Object.entries(breakpoints)
108
+ .sort(([, a], [, b]) => b - a)
109
+ .map(([name]) => name as Breakpoint);
110
+
111
+ const resolved: Record<string, unknown> = {};
112
+
113
+ for (const [key, value] of Object.entries(style)) {
114
+ if (value === undefined) continue;
115
+
116
+ const resolvedValue = resolveValue(value, screenWidth, breakpoints, sortedBps);
117
+ if (resolvedValue !== undefined) {
118
+ resolved[key] = resolvedValue;
119
+ }
120
+ }
121
+
122
+ return resolved as ViewStyle & TextStyle & ImageStyle;
123
+ }
@@ -27,6 +27,9 @@ export type ComponentName =
27
27
  | 'Card'
28
28
  | 'Checkbox'
29
29
  | 'Chip'
30
+ | 'DatePickerCalendar'
31
+ | 'DateTimeInput'
32
+ | 'DateTimePicker'
30
33
  | 'Dialog'
31
34
  | 'Divider'
32
35
  | 'Icon'
@@ -49,6 +52,7 @@ export type ComponentName =
49
52
  | 'Table'
50
53
  | 'Text'
51
54
  | 'TextArea'
55
+ | 'TimePicker'
52
56
  | 'Tooltip'
53
57
  | 'Video'
54
58
  | 'View';
@@ -0,0 +1,30 @@
1
+ import { RegisteredTheme } from './extensions';
2
+
3
+ /**
4
+ * All available breakpoint names.
5
+ * Derived from your registered theme's breakpoints.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // With default theme: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
10
+ * const bp: Breakpoint = 'md';
11
+ * ```
12
+ */
13
+ export type Breakpoint = RegisteredTheme['theme'] extends { breakpoints: infer B }
14
+ ? B extends Record<string, number>
15
+ ? keyof B & string
16
+ : never
17
+ : never;
18
+
19
+ /**
20
+ * Get the breakpoints record type from the theme.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // { xs: number; sm: number; md: number; lg: number; xl: number; }
25
+ * const bps: BreakpointsRecord = theme.breakpoints;
26
+ * ```
27
+ */
28
+ export type BreakpointsRecord = RegisteredTheme['theme'] extends { breakpoints: infer B }
29
+ ? B
30
+ : Record<string, number>;
@@ -15,6 +15,7 @@ export interface DefaultTheme {
15
15
  surface: Record<string, ColorValue>;
16
16
  text: Record<string, ColorValue>;
17
17
  border: Record<string, ColorValue>;
18
+ card: Record<string, ColorValue>;
18
19
  };
19
20
  sizes: {
20
21
  button: Record<string, ButtonSizeValue>;
@@ -42,6 +43,11 @@ export interface DefaultTheme {
42
43
  typography: Record<Typography, TypographyValue>;
43
44
  };
44
45
  interaction: InteractionConfig;
46
+ /**
47
+ * Responsive breakpoints for width-based styling.
48
+ * First breakpoint MUST have value 0.
49
+ */
50
+ breakpoints: Record<string, number>;
45
51
  /**
46
52
  * Component style extensions.
47
53
  * Populated by the extension system when extendComponent is called.
@@ -16,6 +16,7 @@ export * from "./intent";
16
16
  export * from "./size";
17
17
  export * from "./color";
18
18
  export * from "./shadow";
19
+ export * from "./breakpoint";
19
20
 
20
21
  // Re-export structures except IntentValue and ShadowValue (those are re-exported with extensions from intent.ts and shadow.ts)
21
22
  export type {
@@ -23,6 +24,7 @@ export type {
23
24
  ColorValue,
24
25
  Shade,
25
26
  SizeValue,
27
+ BreakpointValue,
26
28
  Typography,
27
29
  TypographyValue,
28
30
  ButtonSizeValue,
@@ -44,6 +44,12 @@ export type Shade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
44
44
  */
45
45
  export type SizeValue = number | string;
46
46
 
47
+ /**
48
+ * Breakpoint value - must be a non-negative number representing pixels.
49
+ * The first breakpoint in a set MUST be 0 (simulates CSS cascading behavior).
50
+ */
51
+ export type BreakpointValue = number;
52
+
47
53
  /**
48
54
  * Interaction state configuration for hover, focus, active states
49
55
  */
@@ -137,6 +143,7 @@ export type SelectSizeValue = {
137
143
  minHeight: SizeValue;
138
144
  fontSize: SizeValue;
139
145
  iconSize: SizeValue;
146
+ borderRadius: SizeValue;
140
147
  };
141
148
 
142
149
  export type SliderSizeValue = {
@@ -1,3 +1,3 @@
1
- export type Surface = 'primary' | 'secondary' | 'tertiary' | 'inverse' | 'inverse-secondary' | 'inverse-tertiary';
1
+ export type Surface = 'screen' | 'primary' | 'secondary' | 'tertiary' | 'inverse' | 'inverse-secondary' | 'inverse-tertiary';
2
2
 
3
3
  export type SurfaceValue = string;
package/src/unistyles.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { Theme } from './theme';
2
2
 
3
+ // Extract breakpoints type from theme
4
+ type ThemeBreakpoints = Theme extends { breakpoints: infer B } ? B : Record<string, number>;
5
+
3
6
  // Unistyles v3 themes declaration
4
7
  // Apps should configure their own themes via StyleSheet.configure()
5
8
  declare module 'react-native-unistyles' {
@@ -9,6 +12,9 @@ declare module 'react-native-unistyles' {
9
12
  // Apps can add more themes via module augmentation
10
13
  [key: string]: Theme;
11
14
  }
15
+
16
+ // Breakpoints declaration - derives from theme breakpoints
17
+ export interface UnistylesBreakpoints extends ThemeBreakpoints {}
12
18
  }
13
19
 
14
20
  // Export for type checking
@@ -0,0 +1,282 @@
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import type { ImageStyle, TextStyle, ViewStyle } from 'react-native';
3
+ import { Dimensions, Platform } from 'react-native';
4
+ import { UnistylesRuntime } from 'react-native-unistyles';
5
+ import { Breakpoint, BreakpointsRecord } from './theme/breakpoint';
6
+ import { isResponsiveValue, Responsive } from './responsive';
7
+
8
+ /**
9
+ * Style object where each property can be a responsive value.
10
+ */
11
+ export type ResponsiveStyleInput = {
12
+ [K in keyof ViewStyle]?: Responsive<ViewStyle[K]>;
13
+ } & {
14
+ [K in keyof TextStyle]?: Responsive<TextStyle[K]>;
15
+ } & {
16
+ [K in keyof ImageStyle]?: Responsive<ImageStyle[K]>;
17
+ };
18
+
19
+ /**
20
+ * Get current screen width (non-reactive, for internal use)
21
+ */
22
+ function getScreenWidth(): number {
23
+ if (Platform.OS === 'web' && typeof window !== 'undefined') {
24
+ return window.innerWidth;
25
+ }
26
+ return Dimensions.get('window').width;
27
+ }
28
+
29
+ /**
30
+ * Calculate the current breakpoint from screen width.
31
+ */
32
+ function calculateBreakpoint(
33
+ screenWidth: number,
34
+ breakpoints: BreakpointsRecord
35
+ ): Breakpoint | undefined {
36
+ const sortedBps = Object.entries(breakpoints)
37
+ .sort(([, a], [, b]) => b - a) as [Breakpoint, number][];
38
+
39
+ for (const [name, value] of sortedBps) {
40
+ if (screenWidth >= value) {
41
+ return name;
42
+ }
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ /**
48
+ * Hook that only re-renders when the breakpoint changes, not on every pixel.
49
+ * Returns both the current breakpoint and the screen width.
50
+ */
51
+ function useBreakpointChange(): { breakpoint: Breakpoint | undefined; screenWidth: number } {
52
+ const breakpoints = UnistylesRuntime.breakpoints as BreakpointsRecord;
53
+
54
+ const [state, setState] = useState(() => {
55
+ const width = getScreenWidth();
56
+ return {
57
+ breakpoint: calculateBreakpoint(width, breakpoints),
58
+ screenWidth: width,
59
+ };
60
+ });
61
+
62
+ useEffect(() => {
63
+ const handleResize = () => {
64
+ const newWidth = getScreenWidth();
65
+ const newBreakpoint = calculateBreakpoint(newWidth, breakpoints);
66
+
67
+ // Only update state if breakpoint changed
68
+ setState(prev => {
69
+ if (prev.breakpoint !== newBreakpoint) {
70
+ return { breakpoint: newBreakpoint, screenWidth: newWidth };
71
+ }
72
+ return prev;
73
+ });
74
+ };
75
+
76
+ if (Platform.OS === 'web' && typeof window !== 'undefined') {
77
+ window.addEventListener('resize', handleResize);
78
+ return () => window.removeEventListener('resize', handleResize);
79
+ } else {
80
+ const subscription = Dimensions.addEventListener('change', () => {
81
+ handleResize();
82
+ });
83
+ return () => subscription?.remove();
84
+ }
85
+ }, [breakpoints]);
86
+
87
+ return state;
88
+ }
89
+
90
+ /**
91
+ * Hook to get the current screen width with proper reactivity.
92
+ * WARNING: This re-renders on every resize. Use useBreakpoint() if you only
93
+ * need to react to breakpoint changes.
94
+ */
95
+ export function useScreenWidth(): number {
96
+ const [width, setWidth] = useState(getScreenWidth);
97
+
98
+ useEffect(() => {
99
+ if (Platform.OS === 'web' && typeof window !== 'undefined') {
100
+ const handleResize = () => setWidth(window.innerWidth);
101
+ window.addEventListener('resize', handleResize);
102
+ return () => window.removeEventListener('resize', handleResize);
103
+ } else {
104
+ const subscription = Dimensions.addEventListener('change', ({ window }) => {
105
+ setWidth(window.width);
106
+ });
107
+ return () => subscription?.remove();
108
+ }
109
+ }, []);
110
+
111
+ return width;
112
+ }
113
+
114
+ /**
115
+ * Resolve a single responsive value to its current breakpoint value.
116
+ */
117
+ function resolveValue<T>(
118
+ value: Responsive<T>,
119
+ screenWidth: number,
120
+ breakpoints: BreakpointsRecord,
121
+ sortedBps: Breakpoint[]
122
+ ): T | undefined {
123
+ if (!isResponsiveValue(value)) {
124
+ return value;
125
+ }
126
+
127
+ for (const bp of sortedBps) {
128
+ if (screenWidth >= breakpoints[bp] && value[bp] !== undefined) {
129
+ return value[bp];
130
+ }
131
+ }
132
+
133
+ return undefined;
134
+ }
135
+
136
+ /**
137
+ * Hook to resolve responsive style values based on the current breakpoint.
138
+ *
139
+ * Allows you to pass style objects with responsive values that automatically
140
+ * resolve to the appropriate value for the current screen width.
141
+ *
142
+ * @param style - Style object with responsive values (or factory function)
143
+ * @param deps - Optional dependency array. If not provided, style is only recalculated on breakpoint change.
144
+ * Pass dependencies if your style object depends on props or state.
145
+ * @returns Resolved style object for the current breakpoint
146
+ *
147
+ * @example
148
+ * ```tsx
149
+ * // Static styles - only updates on breakpoint change
150
+ * function MyComponent() {
151
+ * const containerStyle = useResponsiveStyle({
152
+ * flexDirection: { xs: 'column', md: 'row' },
153
+ * padding: { xs: 8, lg: 16 },
154
+ * backgroundColor: '#fff',
155
+ * });
156
+ *
157
+ * return <View style={containerStyle}>...</View>;
158
+ * }
159
+ * ```
160
+ *
161
+ * @example
162
+ * ```tsx
163
+ * // Dynamic styles - updates when dependencies change
164
+ * function Card({ color, size }) {
165
+ * const cardStyle = useResponsiveStyle({
166
+ * padding: { xs: 12, md: 24 },
167
+ * backgroundColor: color,
168
+ * width: size,
169
+ * }, [color, size]);
170
+ *
171
+ * return <View style={cardStyle}>...</View>;
172
+ * }
173
+ * ```
174
+ */
175
+ export function useResponsiveStyle(
176
+ style: ResponsiveStyleInput | (() => ResponsiveStyleInput),
177
+ deps?: React.DependencyList
178
+ ): ViewStyle & TextStyle & ImageStyle {
179
+ // Only re-render when breakpoint changes, not on every pixel
180
+ const { breakpoint, screenWidth } = useBreakpointChange();
181
+ const breakpoints = UnistylesRuntime.breakpoints as BreakpointsRecord;
182
+
183
+ // Sort breakpoints by value descending for cascade lookup
184
+ const sortedBps = useMemo(() =>
185
+ Object.entries(breakpoints)
186
+ .sort(([, a], [, b]) => b - a)
187
+ .map(([name]) => name as Breakpoint),
188
+ [breakpoints]
189
+ );
190
+
191
+ // Build the dependency array: breakpoint + user-provided deps (or empty)
192
+ const effectiveDeps = deps
193
+ ? [breakpoint, sortedBps, ...deps]
194
+ : [breakpoint, sortedBps];
195
+
196
+ const resolved = useMemo(() => {
197
+ const styleObj = typeof style === 'function' ? style() : style;
198
+ const result: Record<string, unknown> = {};
199
+
200
+ for (const [key, value] of Object.entries(styleObj)) {
201
+ if (value === undefined) continue;
202
+
203
+ const resolvedValue = resolveValue(value, screenWidth, breakpoints, sortedBps);
204
+ if (resolvedValue !== undefined) {
205
+ result[key] = resolvedValue;
206
+ }
207
+ }
208
+
209
+ return result as ViewStyle & TextStyle & ImageStyle;
210
+ // eslint-disable-next-line react-hooks/exhaustive-deps
211
+ }, effectiveDeps);
212
+
213
+ return resolved;
214
+ }
215
+
216
+ /**
217
+ * Hook to get the current breakpoint name.
218
+ * Only re-renders when the breakpoint changes, not on every pixel.
219
+ *
220
+ * @returns The current breakpoint name
221
+ *
222
+ * @example
223
+ * ```tsx
224
+ * function MyComponent() {
225
+ * const breakpoint = useBreakpoint();
226
+ *
227
+ * return (
228
+ * <Text>Current breakpoint: {breakpoint}</Text>
229
+ * );
230
+ * }
231
+ * ```
232
+ */
233
+ export function useBreakpoint(): Breakpoint | undefined {
234
+ const { breakpoint } = useBreakpointChange();
235
+ return breakpoint;
236
+ }
237
+
238
+ /**
239
+ * Hook to check if current viewport is at or above a breakpoint.
240
+ * Only re-renders when the breakpoint changes.
241
+ *
242
+ * @param breakpoint - The breakpoint to check against
243
+ * @returns True if viewport width >= breakpoint value
244
+ *
245
+ * @example
246
+ * ```tsx
247
+ * function Sidebar() {
248
+ * const isDesktop = useBreakpointUp('lg');
249
+ *
250
+ * if (!isDesktop) return null;
251
+ *
252
+ * return <View>Desktop sidebar</View>;
253
+ * }
254
+ * ```
255
+ */
256
+ export function useBreakpointUp(targetBreakpoint: Breakpoint): boolean {
257
+ const { screenWidth } = useBreakpointChange();
258
+ const breakpoints = UnistylesRuntime.breakpoints as BreakpointsRecord;
259
+ return screenWidth >= breakpoints[targetBreakpoint];
260
+ }
261
+
262
+ /**
263
+ * Hook to check if current viewport is below a breakpoint.
264
+ * Only re-renders when the breakpoint changes.
265
+ *
266
+ * @param breakpoint - The breakpoint to check against
267
+ * @returns True if viewport width < breakpoint value
268
+ *
269
+ * @example
270
+ * ```tsx
271
+ * function MobileNav() {
272
+ * const isMobile = useBreakpointDown('md');
273
+ *
274
+ * if (!isMobile) return null;
275
+ *
276
+ * return <View>Mobile navigation</View>;
277
+ * }
278
+ * ```
279
+ */
280
+ export function useBreakpointDown(targetBreakpoint: Breakpoint): boolean {
281
+ return !useBreakpointUp(targetBreakpoint);
282
+ }