@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.
@@ -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,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
- // Export something to ensure this module is included in compilation
15
- export const unistylesConfigured = true;
16
+ // Breakpoints declaration - derives from theme breakpoints
17
+ export interface UnistylesBreakpoints extends ThemeBreakpoints {}
18
+ }
16
19
 
17
- StyleSheet.configure({
18
- settings: {
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
+ }