@idealyst/theme 1.1.8 → 1.2.0
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 +2 -1
- package/src/babel/plugin.js +13 -1
- package/src/breakpoints.ts +112 -0
- package/src/builder.ts +90 -18
- package/src/darkTheme.ts +14 -5
- package/src/index.ts +6 -1
- package/src/lightTheme.ts +14 -5
- package/src/responsive.ts +123 -0
- package/src/styleBuilder.ts +4 -0
- package/src/theme/breakpoint.ts +30 -0
- package/src/theme/extensions.ts +6 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/structures.ts +7 -0
- package/src/theme/surface.ts +1 -1
- package/src/unistyles.ts +6 -0
- package/src/useResponsiveStyle.ts +282 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/theme",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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
|
},
|
package/src/babel/plugin.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
57
|
+
elevation: 2,
|
|
58
58
|
shadowColor: '#000000',
|
|
59
59
|
shadowOffset: { width: 0, height: 1 },
|
|
60
60
|
shadowOpacity: 0.18,
|
|
61
|
-
shadowRadius:
|
|
61
|
+
shadowRadius: 2,
|
|
62
62
|
boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.18)',
|
|
63
63
|
})
|
|
64
64
|
.addShadow('md', {
|
|
65
|
-
elevation:
|
|
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:
|
|
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
|
-
|
|
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
|
+
}
|
package/src/styleBuilder.ts
CHANGED
|
@@ -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>;
|
package/src/theme/extensions.ts
CHANGED
|
@@ -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.
|
package/src/theme/index.ts
CHANGED
|
@@ -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,
|
package/src/theme/structures.ts
CHANGED
|
@@ -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 = {
|
package/src/theme/surface.ts
CHANGED
|
@@ -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
|
+
}
|