fluent-styles 1.56.0 → 1.58.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.
@@ -0,0 +1,369 @@
1
+ import React, { forwardRef } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ ActivityIndicatorProps,
5
+ } from 'react-native';
6
+ import { theme } from '../utiles/theme';
7
+ import { styled } from '../utiles/styled';
8
+ import { YStack, XStack } from '../stack';
9
+ import { StyledText } from '../text';
10
+ import { StyledButton } from '../button';
11
+
12
+ /**
13
+ * Props for Spinner component
14
+ */
15
+ interface SpinnerProps extends Omit<ActivityIndicatorProps, 'ref' | 'size'> {
16
+ // Sizing
17
+ size?: 'small' | 'medium' | 'large' | number;
18
+
19
+ // Color variants
20
+ variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger';
21
+
22
+ // Custom color
23
+ color?: string;
24
+
25
+ // Overlay mode
26
+ overlay?: boolean;
27
+ overlayColor?: string;
28
+
29
+ // Label support
30
+ label?: string;
31
+ labelColor?: string;
32
+ labelSize?: number;
33
+
34
+ // Accessibility
35
+ accessibilityLabel?: string;
36
+ }
37
+
38
+ /**
39
+ * Size configuration for Spinner
40
+ */
41
+ const sizeConfig: Record<'small' | 'medium' | 'large', number> = {
42
+ small: 30,
43
+ medium: 50,
44
+ large: 80,
45
+ };
46
+
47
+ /**
48
+ * Variant configuration for Spinner
49
+ */
50
+ const variantConfig: Record<
51
+ 'default' | 'primary' | 'success' | 'warning' | 'danger',
52
+ { color: string; label: string }
53
+ > = {
54
+ default: {
55
+ color: theme.colors.gray[400],
56
+ label: 'default',
57
+ },
58
+ primary: {
59
+ color: theme.colors.blue[500],
60
+ label: 'primary',
61
+ },
62
+ success: {
63
+ color: theme.colors.green[500],
64
+ label: 'success',
65
+ },
66
+ warning: {
67
+ color: theme.colors.amber[500],
68
+ label: 'warning',
69
+ },
70
+ danger: {
71
+ color: theme.colors.red[500],
72
+ label: 'danger',
73
+ },
74
+ };
75
+
76
+ /**
77
+ * Base styled ActivityIndicator
78
+ */
79
+ const StyledActivityIndicator = styled<any>(ActivityIndicator, {
80
+ base: {
81
+ color: theme.colors.gray[400],
82
+ } as any,
83
+ });
84
+
85
+ /**
86
+ * Spinner: Professional loading indicator component
87
+ *
88
+ * Features:
89
+ * - Multiple size options (small, medium, large, custom)
90
+ * - 5 color variants (default, primary, success, warning, danger)
91
+ * - Custom color support
92
+ * - Overlay mode for full-screen loading
93
+ * - Optional label text
94
+ * - Theme integration
95
+ * - Accessibility support
96
+ */
97
+ const StyledSpinner = forwardRef<any, SpinnerProps>(
98
+ (
99
+ {
100
+ size = 'medium',
101
+ variant = 'primary',
102
+ color,
103
+ overlay = false,
104
+ overlayColor = 'rgba(0, 0, 0, 0.3)',
105
+ label,
106
+ labelColor,
107
+ labelSize = 14,
108
+ accessibilityLabel = 'Loading',
109
+ ...rest
110
+ },
111
+ ref
112
+ ) => {
113
+ // Determine size
114
+ const finalSize = typeof size === 'number' ? size : sizeConfig[size];
115
+
116
+ // Determine color
117
+ const variantConfig_value = variantConfig[variant];
118
+ const finalColor = color || variantConfig_value.color;
119
+
120
+ const spinnerElement = (
121
+ <StyledActivityIndicator
122
+ ref={ref}
123
+ size={finalSize}
124
+ color={finalColor}
125
+ accessibilityLabel={accessibilityLabel}
126
+ accessible={true}
127
+ {...rest}
128
+ />
129
+ );
130
+
131
+ // If no overlay, return basic spinner
132
+ if (!overlay) {
133
+ if (!label) {
134
+ return spinnerElement;
135
+ }
136
+
137
+ // Spinner with label
138
+ return (
139
+ <YStack alignItems="center" gap={12}>
140
+ {spinnerElement}
141
+ {label && (
142
+ <StyledText
143
+ fontSize={labelSize}
144
+ color={labelColor || theme.colors.gray[600]}
145
+ numberOfLines={1}
146
+ >
147
+ {label}
148
+ </StyledText>
149
+ )}
150
+ </YStack>
151
+ );
152
+ }
153
+
154
+ // Overlay mode
155
+ return (
156
+ <YStack
157
+ position="absolute"
158
+ top={0}
159
+ left={0}
160
+ right={0}
161
+ bottom={0}
162
+ backgroundColor={overlayColor}
163
+ justifyContent="center"
164
+ alignItems="center"
165
+ zIndex={9999}
166
+ >
167
+ <YStack alignItems="center" gap={12}>
168
+ {spinnerElement}
169
+ {label && (
170
+ <StyledText
171
+ fontSize={labelSize}
172
+ color={labelColor || theme.colors.white[500]}
173
+ numberOfLines={1}
174
+ >
175
+ {label}
176
+ </StyledText>
177
+ )}
178
+ </YStack>
179
+ </YStack>
180
+ );
181
+ }
182
+ );
183
+
184
+ StyledSpinner.displayName = 'StyledSpinner';
185
+
186
+ /**
187
+ * Props for SpinnerContainer - Full-screen loading overlay with backdrop
188
+ */
189
+ interface SpinnerContainerProps extends Omit<SpinnerProps, 'ref' | 'overlay'> {
190
+ isVisible?: boolean;
191
+ backdropColor?: string;
192
+ message?: string;
193
+ onBackdropPress?: () => void;
194
+ }
195
+
196
+ /**
197
+ * SpinnerContainer: Full-screen loading container with backdrop
198
+ *
199
+ * Use for: Page loading, data fetching, async operations
200
+ */
201
+ const SpinnerContainer = forwardRef<any, SpinnerContainerProps>(
202
+ (
203
+ {
204
+ isVisible = true,
205
+ size = 'large',
206
+ variant = 'primary',
207
+ color,
208
+ backdropColor = 'rgba(0, 0, 0, 0.5)',
209
+ message,
210
+ labelColor,
211
+ labelSize = 14,
212
+ onBackdropPress,
213
+ ...rest
214
+ },
215
+ ref
216
+ ) => {
217
+ if (!isVisible) return null;
218
+
219
+ // Determine size
220
+ const finalSize = typeof size === 'number' ? size : sizeConfig[size];
221
+
222
+ // Determine color
223
+ const variantConfig_value = variantConfig[variant];
224
+ const finalColor = color || variantConfig_value.color;
225
+
226
+ const spinnerContent = (
227
+ <YStack alignItems="center" gap={20}>
228
+ <StyledActivityIndicator
229
+ ref={ref}
230
+ size={finalSize}
231
+ color={finalColor}
232
+ accessible={true}
233
+ accessibilityLabel="Loading"
234
+ {...rest}
235
+ />
236
+ {message && (
237
+ <StyledText
238
+ fontSize={labelSize}
239
+ color={labelColor || theme.colors.white[500]}
240
+ numberOfLines={2}
241
+ textAlign="center"
242
+ >
243
+ {message}
244
+ </StyledText>
245
+ )}
246
+ </YStack>
247
+ );
248
+
249
+ if (!onBackdropPress) {
250
+ return (
251
+ <YStack
252
+ position="absolute"
253
+ top={0}
254
+ left={0}
255
+ right={0}
256
+ bottom={0}
257
+ backgroundColor={backdropColor}
258
+ justifyContent="center"
259
+ alignItems="center"
260
+ zIndex={9999}
261
+ >
262
+ {spinnerContent}
263
+ </YStack>
264
+ );
265
+ }
266
+
267
+ return (
268
+ <StyledButton
269
+ link={true}
270
+ onPress={onBackdropPress}
271
+ position="absolute"
272
+ top={0}
273
+ left={0}
274
+ right={0}
275
+ bottom={0}
276
+ backgroundColor={backdropColor}
277
+ justifyContent="center"
278
+ alignItems="center"
279
+ zIndex={9999}
280
+ borderRadius={0}
281
+ borderWidth={0}
282
+ >
283
+ {spinnerContent}
284
+ </StyledButton>
285
+ );
286
+ }
287
+ );
288
+
289
+ SpinnerContainer.displayName = 'SpinnerContainer';
290
+
291
+ /**
292
+ * Props for InlineSpinner - Spinner with text in a row
293
+ */
294
+ interface InlineSpinnerProps extends Omit<SpinnerProps, 'overlay'> {
295
+ text?: string;
296
+ direction?: 'row' | 'column';
297
+ gap?: number;
298
+ }
299
+
300
+ /**
301
+ * InlineSpinner: Spinner with text in flexible direction
302
+ *
303
+ * Use for: Button loading states, inline operations, compact loading indicators
304
+ */
305
+ const InlineSpinner = forwardRef<any, InlineSpinnerProps>(
306
+ (
307
+ {
308
+ size = 'small',
309
+ variant = 'primary',
310
+ color,
311
+ text,
312
+ labelColor,
313
+ labelSize = 12,
314
+ direction = 'row',
315
+ gap = 8,
316
+ accessibilityLabel = 'Loading',
317
+ ...rest
318
+ },
319
+ ref
320
+ ) => {
321
+ // Determine size
322
+ const finalSize = typeof size === 'number' ? size : sizeConfig[size];
323
+
324
+ // Determine color
325
+ const variantConfig_value = variantConfig[variant];
326
+ const finalColor = color || variantConfig_value.color;
327
+
328
+ const StackComponent = direction === 'row' ? XStack : YStack;
329
+
330
+ return (
331
+ <StackComponent gap={gap} alignItems="center">
332
+ <StyledActivityIndicator
333
+ ref={ref}
334
+ size={finalSize}
335
+ color={finalColor}
336
+ accessible={true}
337
+ accessibilityLabel={accessibilityLabel}
338
+ {...rest}
339
+ />
340
+ {text && (
341
+ <StyledText
342
+ fontSize={labelSize}
343
+ color={labelColor || theme.colors.gray[600]}
344
+ numberOfLines={1}
345
+ >
346
+ {text}
347
+ </StyledText>
348
+ )}
349
+ </StackComponent>
350
+ );
351
+ }
352
+ );
353
+
354
+ InlineSpinner.displayName = 'InlineSpinner';
355
+
356
+ /**
357
+ * Exports
358
+ */
359
+ export {
360
+ StyledSpinner ,
361
+ SpinnerContainer,
362
+ InlineSpinner,
363
+ type SpinnerProps,
364
+ type SpinnerContainerProps,
365
+ type InlineSpinnerProps,
366
+ sizeConfig,
367
+ variantConfig,
368
+ };
369
+ export default StyledSpinner;
@@ -19,3 +19,91 @@ export const isValidNumber = (value: unknown): value is number => {
19
19
  export const isValidString = (value: unknown): value is string => {
20
20
  return typeof value === "string" && value.trim().length > 0;
21
21
  };
22
+
23
+ // Define the structure for a single rule
24
+ interface Rule {
25
+ array?: boolean;
26
+ pattern?: RegExp;
27
+ message: string;
28
+ validate?: (value: any, fields: Record<string, any>) => string | undefined;
29
+ }
30
+
31
+ // Define the structure for the rules object used in validation
32
+ interface Rules {
33
+ [key: string]: Rule[];
34
+ }
35
+
36
+ // Define the structure for the values object used in validation
37
+ interface Values {
38
+ [key: string]: any;
39
+ }
40
+
41
+ // Define the structure for the errors object in the validation response
42
+ export interface Errors {
43
+ [key: string]: { message: string };
44
+ }
45
+
46
+ /**
47
+ * Validates a value against provided rules.
48
+ * @param value - The value to validate.
49
+ * @param rules - The array of validation rules.
50
+ * @returns The error message if the value is invalid, otherwise undefined.
51
+ */
52
+ const validateField = (value: any, rules: Rule[], fields: Record<string, any> = {}): string | undefined => {
53
+ for (const rule of rules) {
54
+ // Explicit null or undefined check
55
+ if (value === null || value === undefined) {
56
+ return rule.message; // Null/undefined fails required validation
57
+ }
58
+
59
+ // Array validation
60
+ if (rule.array && Array.isArray(value) && value.length === 0) {
61
+ return rule.message;
62
+ }
63
+
64
+ // Pattern validation
65
+ if (rule.pattern && !rule.pattern.test(value)) {
66
+ return rule.message;
67
+ }
68
+
69
+ // Custom validation logic
70
+ if (rule.validate) {
71
+ const customMessage = rule.validate(value, fields);
72
+ if (customMessage) {
73
+ return customMessage;
74
+ }
75
+ }
76
+ }
77
+
78
+ return undefined; // No errors found
79
+ };
80
+
81
+
82
+ /**
83
+ * Validates a set of values against a set of field rules.
84
+ * @param values - The object of field values.
85
+ * @param rules - The object of field rules.
86
+ * @returns An object containing a boolean indicating if there are errors, and an object of errors.
87
+ */
88
+ const validate = (values: Values, rules: Rules): { hasError: boolean; errors: Errors } => {
89
+ const errors: Errors = {};
90
+ let hasError = false;
91
+
92
+ for (const field in rules) {
93
+ const fieldRules = rules[field];
94
+ const value = values[field] ?? null; // Safely access values[field]
95
+ const error = validateField(value, fieldRules, values); // Pass `values` for cross-field validation
96
+
97
+ if (error) {
98
+ hasError = true;
99
+ errors[field] = { message: error };
100
+ }
101
+ }
102
+
103
+ return {
104
+ hasError,
105
+ errors
106
+ };
107
+ };
108
+
109
+ export { validate };