@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.
- package/package.json +30 -1
- package/src/babel/index.ts +9 -0
- package/src/babel/plugin.js +883 -0
- package/src/babel/plugin.ts +187 -0
- package/src/babel/runtime.ts +94 -0
- package/src/babel/theme-analyzer.js +357 -0
- package/src/breakpoints.ts +112 -0
- package/src/builder.ts +90 -18
- package/src/componentStyles.ts +93 -0
- package/src/config/cli.ts +95 -0
- package/src/config/generator.ts +817 -0
- package/src/config/index.ts +10 -0
- package/src/config/types.ts +112 -0
- package/src/darkTheme.ts +27 -18
- package/src/extensions.ts +110 -0
- package/src/index.ts +21 -4
- package/src/lightTheme.ts +14 -5
- package/src/responsive.ts +123 -0
- package/src/styleBuilder.ts +112 -0
- package/src/theme/breakpoint.ts +30 -0
- package/src/theme/extensions.ts +13 -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 +11 -15
- package/src/useResponsiveStyle.ts +282 -0
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,25 +1,21 @@
|
|
|
1
|
-
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
-
import { darkTheme } from './darkTheme';
|
|
3
|
-
import { lightTheme } from './lightTheme';
|
|
4
1
|
import { Theme } from './theme';
|
|
5
2
|
|
|
3
|
+
// Extract breakpoints type from theme
|
|
4
|
+
type ThemeBreakpoints = Theme extends { breakpoints: infer B } ? B : Record<string, number>;
|
|
5
|
+
|
|
6
6
|
// Unistyles v3 themes declaration
|
|
7
|
+
// Apps should configure their own themes via StyleSheet.configure()
|
|
7
8
|
declare module 'react-native-unistyles' {
|
|
8
9
|
export interface UnistylesThemes {
|
|
9
10
|
light: Theme;
|
|
10
11
|
dark: Theme;
|
|
12
|
+
// Apps can add more themes via module augmentation
|
|
13
|
+
[key: string]: Theme;
|
|
11
14
|
}
|
|
12
|
-
}
|
|
13
15
|
|
|
14
|
-
//
|
|
15
|
-
export
|
|
16
|
+
// Breakpoints declaration - derives from theme breakpoints
|
|
17
|
+
export interface UnistylesBreakpoints extends ThemeBreakpoints {}
|
|
18
|
+
}
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
initialTheme: 'light',
|
|
20
|
-
},
|
|
21
|
-
themes: {
|
|
22
|
-
light: lightTheme,
|
|
23
|
-
dark: darkTheme,
|
|
24
|
-
}
|
|
25
|
-
})
|
|
20
|
+
// Export for type checking
|
|
21
|
+
export const THEME_TYPES_DECLARED = true;
|
|
@@ -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
|
+
}
|