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.
- package/lib/commonjs/index.js +49 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/spinner/index.js +279 -0
- package/lib/commonjs/spinner/index.js.map +1 -0
- package/lib/commonjs/utiles/validators.js +70 -1
- package/lib/commonjs/utiles/validators.js.map +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/spinner/index.js +275 -0
- package/lib/module/spinner/index.js.map +1 -0
- package/lib/module/utiles/validators.js +70 -0
- package/lib/module/utiles/validators.js.map +1 -1
- package/lib/typescript/index.d.ts +4 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/spinner/index.d.ts +75 -0
- package/lib/typescript/spinner/index.d.ts.map +1 -0
- package/lib/typescript/utiles/validators.d.ts +28 -0
- package/lib/typescript/utiles/validators.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -1
- package/src/spinner/index.tsx +369 -0
- package/src/utiles/validators.ts +88 -0
|
@@ -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;
|
package/src/utiles/validators.ts
CHANGED
|
@@ -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 };
|