@widergy/mobile-ui 1.49.7 → 1.50.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [1.50.1](https://github.com/widergy/mobile-ui/compare/v1.50.0...v1.50.1) (2025-09-18)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * [CX-1130] UTCuit Field Refactor ([#456](https://github.com/widergy/mobile-ui/issues/456)) ([19cce9d](https://github.com/widergy/mobile-ui/commit/19cce9dc619a04044b662cb3e359dd755c5c4715))
7
+
8
+ # [1.50.0](https://github.com/widergy/mobile-ui/compare/v1.49.7...v1.50.0) (2025-08-12)
9
+
10
+
11
+ ### Features
12
+
13
+ * [CX-987] UTSkeletonLoader ([#451](https://github.com/widergy/mobile-ui/issues/451)) ([20c5853](https://github.com/widergy/mobile-ui/commit/20c58534fa83913eae1e9deb0ab0316369bc0bb3))
14
+
1
15
  ## [1.49.7](https://github.com/widergy/mobile-ui/compare/v1.49.6...v1.49.7) (2025-07-30)
2
16
 
3
17
 
@@ -18,3 +18,34 @@ export const PLACE_SELF_TYPES = {
18
18
  END: 'end',
19
19
  START: 'start'
20
20
  };
21
+
22
+ export const ROOT_LOADER_STRATEGY = 'root';
23
+ export const INTERNAL_LOADER_STRATEGY = 'internal';
24
+ export const DEFAULT_LOADER_STRATEGY = ROOT_LOADER_STRATEGY;
25
+
26
+ export const DEFAULT_LOADER_PROPS = {
27
+ alignment: 'center',
28
+ items: [
29
+ [
30
+ [
31
+ { height: 14, width: '100%' },
32
+ { height: 8, width: '100%' }
33
+ ],
34
+ [{ height: 28, width: 28 }]
35
+ ],
36
+ [
37
+ [
38
+ { height: 19, width: '100%' },
39
+ { height: 12, width: '100%' }
40
+ ],
41
+ [
42
+ { height: 19, width: '100%' },
43
+ { height: 12, width: '100%' }
44
+ ]
45
+ ]
46
+ ],
47
+ strategy: ROOT_LOADER_STRATEGY,
48
+ styles: {
49
+ skeletonContainer: { paddingTop: 16, paddingBottom: 12, paddingHorizontal: 16 }
50
+ }
51
+ };
@@ -1,4 +1,4 @@
1
- import React, { memo } from 'react';
1
+ import React, { Fragment, memo } from 'react';
2
2
  import { TouchableOpacity, View } from 'react-native';
3
3
  import {
4
4
  arrayOf,
@@ -16,8 +16,18 @@ import {
16
16
  import { isEmpty } from 'lodash';
17
17
 
18
18
  import Surface from '../Surface';
19
+ import UTSkeletonLoader from '../UTSkeletonLoader';
19
20
 
20
- import { ACTION_TYPES, HEADER_ACTIONS_VARIANTS, PLACE_SELF_TYPES, SIZES } from './constants';
21
+ import {
22
+ ACTION_TYPES,
23
+ DEFAULT_LOADER_PROPS,
24
+ DEFAULT_LOADER_STRATEGY,
25
+ HEADER_ACTIONS_VARIANTS,
26
+ INTERNAL_LOADER_STRATEGY,
27
+ PLACE_SELF_TYPES,
28
+ ROOT_LOADER_STRATEGY,
29
+ SIZES
30
+ } from './constants';
21
31
  import Header from './components/Header';
22
32
  import AdditionalMessage from './components/AdditionalMessage';
23
33
  import BottomActions from './components/BottomActions';
@@ -37,6 +47,8 @@ const UTActionCard = ({
37
47
  descriptionProps = {},
38
48
  headerActions,
39
49
  headerActionsProps = { variant: HEADER_ACTIONS_VARIANTS.DEFAULT },
50
+ loading = false,
51
+ loadingProps = DEFAULT_LOADER_PROPS,
40
52
  mainAction,
41
53
  size = SIZES.SMALL,
42
54
  status,
@@ -48,58 +60,72 @@ const UTActionCard = ({
48
60
  titleProps = {},
49
61
  withBodyPadding = true
50
62
  }) => {
63
+ const { strategy: loaderStrategy = DEFAULT_LOADER_STRATEGY } = loadingProps;
64
+ const rootLoader = loaderStrategy === ROOT_LOADER_STRATEGY;
65
+ const internalLoader = loaderStrategy === INTERNAL_LOADER_STRATEGY;
66
+ const LoaderWrapper = rootLoader ? UTSkeletonLoader : Fragment;
67
+ const InternalLoaderWrapper = internalLoader ? UTSkeletonLoader : Fragment;
68
+
51
69
  return (
52
70
  <Surface elevation={surfaceElevation} style={[styles.cardContainer, classNames.container]}>
53
- <TouchableOpacity
54
- activeOpacity={0.6}
55
- disabled={!mainAction}
56
- onPress={mainAction && (() => mainAction?.())}
57
- style={[styles.innerContainer, classNames.innerContainer, mainAction && styles.withMainAction]}
58
- >
59
- <View>
60
- {BackgroundImage && (
61
- <BackgroundImage
62
- height={backgroundHeight}
63
- style={[styles.backgroundImage, classNames.backgroundImage]}
64
- width={backgroundWidth}
65
- />
66
- )}
67
- <View style={[styles.headerAndChildrenContainer, classNames.headerAndChildrenContainer]}>
68
- <Header
69
- {...{
70
- adornment,
71
- classNames,
72
- description,
73
- descriptionProps,
74
- headerActions,
75
- headerActionsProps,
76
- mainAction,
77
- size,
78
- status,
79
- statusAlignment,
80
- statusLabel,
81
- statusProps,
82
- title,
83
- titleProps
84
- }}
85
- />
86
- {children && (
87
- <View
88
- style={[
89
- withBodyPadding ? styles[`bodyPadding_${size}`] : styles[`withoutBodyPadding_${size}`],
90
- classNames?.childrenContainer ?? {}
91
- ]}
92
- >
93
- {children}
94
- </View>
71
+ <LoaderWrapper loading={loading} {...loadingProps}>
72
+ <TouchableOpacity
73
+ activeOpacity={0.6}
74
+ disabled={!mainAction || loading}
75
+ onPress={mainAction && (() => mainAction?.())}
76
+ style={[styles.innerContainer, classNames.innerContainer, mainAction && styles.withMainAction]}
77
+ >
78
+ <View>
79
+ {BackgroundImage && (
80
+ <BackgroundImage
81
+ height={backgroundHeight}
82
+ style={[styles.backgroundImage, classNames.backgroundImage]}
83
+ width={backgroundWidth}
84
+ />
95
85
  )}
86
+ <InternalLoaderWrapper loading={loading} {...loadingProps}>
87
+ <View style={[styles.headerAndChildrenContainer, classNames.headerAndChildrenContainer]}>
88
+ <Header
89
+ {...{
90
+ adornment,
91
+ classNames,
92
+ description,
93
+ descriptionProps,
94
+ headerActions,
95
+ headerActionsProps,
96
+ mainAction,
97
+ size,
98
+ status,
99
+ statusAlignment,
100
+ statusLabel,
101
+ statusProps,
102
+ title,
103
+ titleProps
104
+ }}
105
+ />
106
+ {children && (
107
+ <View
108
+ style={[
109
+ withBodyPadding ? styles[`bodyPadding_${size}`] : styles[`withoutBodyPadding_${size}`],
110
+ classNames?.childrenContainer ?? {}
111
+ ]}
112
+ >
113
+ {children}
114
+ </View>
115
+ )}
116
+ </View>
117
+ </InternalLoaderWrapper>
96
118
  </View>
97
- </View>
98
- </TouchableOpacity>
99
- {!isEmpty(additionalMessage) && <AdditionalMessage {...additionalMessage} size={size} />}
100
- {!isEmpty(bottomActions) && (
101
- <BottomActions actions={bottomActions} bottomActionsVariant={bottomActionsVariant} />
102
- )}
119
+ </TouchableOpacity>
120
+ {internalLoader && loading
121
+ ? null
122
+ : !isEmpty(additionalMessage) && <AdditionalMessage {...additionalMessage} size={size} />}
123
+ {internalLoader && loading
124
+ ? null
125
+ : !isEmpty(bottomActions) && (
126
+ <BottomActions actions={bottomActions} bottomActionsVariant={bottomActionsVariant} />
127
+ )}
128
+ </LoaderWrapper>
103
129
  </Surface>
104
130
  );
105
131
  };
@@ -150,6 +176,8 @@ UTActionCard.propTypes = {
150
176
  buttonGroupProps: shape({ colorTheme: string, shape: string }),
151
177
  variant: oneOf([HEADER_ACTIONS_VARIANTS.DEFAULT, HEADER_ACTIONS_VARIANTS.BUTTON_GROUP])
152
178
  }),
179
+ loading: bool,
180
+ loadingProps: object,
153
181
  mainAction: func,
154
182
  size: oneOf([SIZES.MEDIUM, SIZES.SMALL]),
155
183
  status: string,
@@ -0,0 +1,8 @@
1
+ export const ID_CODE_REGEX = /(\d+)/gi;
2
+
3
+ export const PREFIX_LENGTH = 2;
4
+ export const DOCUMENT_LENGTH = 8;
5
+
6
+ export const PREFIX_REGEX = /^[23]\d$/;
7
+
8
+ export const TITLE = 'CUIT';
@@ -0,0 +1,191 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { bool, func, oneOf, oneOfType, string } from 'prop-types';
3
+ import { View } from 'react-native';
4
+
5
+ import UTLabel from '../UTLabel';
6
+ import UTTextInput from '../UTTextInput';
7
+
8
+ import styles from './styles';
9
+ import { ID_CODE_REGEX, PREFIX_LENGTH, DOCUMENT_LENGTH, TITLE } from './constants';
10
+
11
+ const UTCuit = ({
12
+ blurOnSubmit,
13
+ defaultValue,
14
+ editable = true,
15
+ editableDocument = true,
16
+ formError,
17
+ onBlur,
18
+ onChange,
19
+ onFocus,
20
+ onSubmitEditing,
21
+ returnKeyType = 'done',
22
+ title,
23
+ value
24
+ }) => {
25
+ const prefixRef = useRef();
26
+ const documentNumberRef = useRef();
27
+ const suffixRef = useRef();
28
+
29
+ const [documentNumber, setDocumentNumber] = useState('');
30
+ const [prefix, setPrefix] = useState('');
31
+ const [suffix, setSuffix] = useState('');
32
+
33
+ const valuesRef = useRef({ prefix: '', documentNumber: '', suffix: '' });
34
+ const isInitialized = useRef(false);
35
+
36
+ useEffect(() => {
37
+ if (!isInitialized.current) {
38
+ const initialValue = value || defaultValue || '';
39
+ const code = initialValue.match(ID_CODE_REGEX);
40
+
41
+ let initialPrefix = '';
42
+ let initialDocNumber = '';
43
+ let initialSuffix = '';
44
+
45
+ if (code) {
46
+ if (code.length === 3) [initialPrefix, initialDocNumber, initialSuffix] = code;
47
+ else [initialDocNumber] = code;
48
+ }
49
+
50
+ const formattedDocumentNumber = !editableDocument
51
+ ? `${'0'.repeat(DOCUMENT_LENGTH - initialDocNumber.length)}${initialDocNumber}`
52
+ : initialDocNumber;
53
+
54
+ setDocumentNumber(formattedDocumentNumber);
55
+ setPrefix(initialPrefix);
56
+ setSuffix(initialSuffix);
57
+
58
+ valuesRef.current = {
59
+ prefix: initialPrefix,
60
+ documentNumber: formattedDocumentNumber,
61
+ suffix: initialSuffix
62
+ };
63
+
64
+ isInitialized.current = true;
65
+ }
66
+ }, [value, defaultValue, editableDocument]);
67
+
68
+ const changeDocumentNumber = newDocumentNumber => {
69
+ if (editableDocument) {
70
+ setDocumentNumber(newDocumentNumber);
71
+ valuesRef.current.documentNumber = newDocumentNumber;
72
+ if (onChange) {
73
+ onChange(`${valuesRef.current.prefix}-${newDocumentNumber}-${valuesRef.current.suffix}`);
74
+ }
75
+ }
76
+ };
77
+
78
+ const changePrefix = newPrefix => {
79
+ if (newPrefix.length === 1 && !['2', '3'].includes(newPrefix)) {
80
+ return;
81
+ }
82
+ if (newPrefix.length === 2 && !/^[23][0-9]$/.test(newPrefix)) {
83
+ return;
84
+ }
85
+
86
+ setPrefix(newPrefix);
87
+ valuesRef.current.prefix = newPrefix;
88
+ if (onChange) {
89
+ onChange(`${newPrefix}-${valuesRef.current.documentNumber}-${valuesRef.current.suffix}`);
90
+ }
91
+ };
92
+
93
+ const changeSuffix = newSuffix => {
94
+ setSuffix(newSuffix);
95
+ valuesRef.current.suffix = newSuffix;
96
+ if (onChange) {
97
+ onChange(`${valuesRef.current.prefix}-${valuesRef.current.documentNumber}-${newSuffix}`);
98
+ }
99
+ };
100
+
101
+ const handleBlur = () => {
102
+ if (onBlur) {
103
+ onBlur();
104
+ }
105
+ };
106
+
107
+ return (
108
+ <View style={styles.container}>
109
+ <UTLabel variant="medium" style={styles.title}>
110
+ {title || TITLE}
111
+ </UTLabel>
112
+ <View style={styles.fieldContainer}>
113
+ <View style={styles.input}>
114
+ <UTTextInput
115
+ alwaysShowPlaceholder
116
+ blurOnSubmit={blurOnSubmit}
117
+ disabled={!editable}
118
+ error={!!formError && ' '}
119
+ inputRef={prefixRef}
120
+ maxLength={PREFIX_LENGTH}
121
+ onBlur={handleBlur}
122
+ onChange={changePrefix}
123
+ onFocus={onFocus}
124
+ placeholder="XX"
125
+ returnKeyType={returnKeyType}
126
+ type="numeric"
127
+ value={prefix}
128
+ version="V1"
129
+ />
130
+ </View>
131
+ <View style={styles.documentContainer}>
132
+ <UTTextInput
133
+ alwaysShowPlaceholder
134
+ blurOnSubmit={blurOnSubmit}
135
+ disabled={!editable || !editableDocument}
136
+ error={!!formError && ' '}
137
+ inputRef={documentNumberRef}
138
+ maxLength={DOCUMENT_LENGTH}
139
+ onBlur={handleBlur}
140
+ onChange={changeDocumentNumber}
141
+ onFocus={onFocus}
142
+ placeholder="XXXXXXXX"
143
+ returnKeyType={returnKeyType}
144
+ type="numeric"
145
+ value={documentNumber}
146
+ version="V1"
147
+ />
148
+ </View>
149
+ <View style={styles.input}>
150
+ <UTTextInput
151
+ alwaysShowPlaceholder
152
+ blurOnSubmit={blurOnSubmit}
153
+ disabled={!editable}
154
+ error={!!formError && ' '}
155
+ inputRef={suffixRef}
156
+ maxLength={1}
157
+ onBlur={handleBlur}
158
+ onChange={changeSuffix}
159
+ onFocus={onFocus}
160
+ onSubmitEditing={onSubmitEditing}
161
+ placeholder="X"
162
+ returnKeyType={returnKeyType}
163
+ type="numeric"
164
+ value={suffix}
165
+ version="V1"
166
+ />
167
+ </View>
168
+ </View>
169
+ <UTLabel colorTheme="error" variant="xsmall" style={styles.error}>
170
+ {formError || ''}
171
+ </UTLabel>
172
+ </View>
173
+ );
174
+ };
175
+
176
+ UTCuit.propTypes = {
177
+ blurOnSubmit: bool,
178
+ defaultValue: string,
179
+ editable: bool,
180
+ editableDocument: bool,
181
+ formError: oneOfType([string, bool]),
182
+ onBlur: func,
183
+ onChange: func.isRequired,
184
+ onFocus: func,
185
+ onSubmitEditing: func,
186
+ returnKeyType: oneOf(['done', 'go', 'next', 'search']),
187
+ title: string,
188
+ value: string
189
+ };
190
+
191
+ export default UTCuit;
@@ -0,0 +1,33 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ export default StyleSheet.create({
4
+ container: {
5
+ flexDirection: 'column'
6
+ },
7
+ title: {
8
+ marginBottom: 8
9
+ },
10
+ fieldContainer: {
11
+ alignItems: 'flex-end',
12
+ flexDirection: 'row',
13
+ marginTop: 6
14
+ },
15
+ documentContainer: {
16
+ flex: 2,
17
+ paddingHorizontal: 4
18
+ },
19
+ input: {
20
+ flex: 1
21
+ },
22
+ inputContainer: {
23
+ flex: 1
24
+ },
25
+ message: {
26
+ paddingTop: 15,
27
+ marginBottom: -25
28
+ },
29
+ error: {
30
+ marginBottom: 12,
31
+ marginTop: 12
32
+ }
33
+ });
@@ -0,0 +1,285 @@
1
+ # UTSkeletonLoader
2
+
3
+ ## Description
4
+
5
+ `UTSkeletonLoader` is a flexible skeleton loading component that displays placeholder content while data is being loaded. It supports complex layouts with multiple rows, columns, and customizable skeleton items to match your content structure.
6
+
7
+ ## Props
8
+
9
+ | Name | Type | Default | Description |
10
+ | --------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
11
+ | alignment | string | | Alignment of skeleton items. One of: `left`, `right`, `center`, `spaceBetween`, `spaceAround`, `spaceEvenly`. |
12
+ | children | node | | Content to display when `loading` is false. |
13
+ | count | number | 1 | Number of times to repeat the skeleton structure. |
14
+ | height | number\|string | | Global height for all skeleton items. Can be overridden by individual item height. |
15
+ | items | array | [] | Array defining the skeleton structure. See Items Structure section for details. |
16
+ | loading | bool | true | Controls whether to show skeleton or children content. |
17
+ | spacing | number | 8 | Spacing between skeleton items in pixels. |
18
+ | styles | object | | Custom styles to apply to the component. Can contain styles for different parts of the skeleton. |
19
+ | width | number\|string | | Global width for all skeleton items. Can be overridden by individual item width. |
20
+
21
+ ## Items Structure
22
+
23
+ The `items` prop defines the skeleton layout structure as a three-dimensional array:
24
+
25
+ ```
26
+ items: Array<Array<Array<SkeletonItem>>>
27
+ ```
28
+
29
+ - **First level**: Rows - each element represents a horizontal row
30
+ - **Second level**: Columns - each element represents a vertical column within a row
31
+ - **Third level**: Items - each element represents an individual skeleton item
32
+
33
+ ### SkeletonItem Object
34
+
35
+ Each skeleton item is an object with the following properties:
36
+
37
+ | Name | Type | Description |
38
+ | --------- | -------- | ------------------------------------------------------------------------------- |
39
+ | height | number\|string | Height of the skeleton item. Overrides global height. |
40
+ | width | number\|string | Width of the skeleton item. Overrides global width. |
41
+ | alignment | string | Alignment for this specific item. Overrides global alignment. |
42
+ | style | object | Custom styles for this specific skeleton item. |
43
+
44
+ ## Alignment Options
45
+
46
+ The alignment can be one of the following values:
47
+
48
+ - `left`: Aligns items to the left (flex-start)
49
+ - `right`: Aligns items to the right (flex-end)
50
+ - `center`: Centers items
51
+ - `spaceBetween`: Distributes items with space between them
52
+ - `spaceAround`: Distributes items with space around them
53
+ - `spaceEvenly`: Distributes items with even space
54
+
55
+ ## Usage
56
+
57
+ ### Basic Example
58
+
59
+ ```jsx
60
+ import React, { useState, useEffect } from 'react';
61
+ import { UTSkeletonLoader, UTLabel } from '@widergy/mobile-ui';
62
+
63
+ const BasicExample = () => {
64
+ const [loading, setLoading] = useState(true);
65
+ const [data, setData] = useState(null);
66
+
67
+ useEffect(() => {
68
+ // Simulate API call
69
+ setTimeout(() => {
70
+ setData('Loaded content');
71
+ setLoading(false);
72
+ }, 2000);
73
+ }, []);
74
+
75
+ return (
76
+ <UTSkeletonLoader loading={loading}>
77
+ <UTLabel>{data}</UTLabel>
78
+ </UTSkeletonLoader>
79
+ );
80
+ };
81
+
82
+ export default BasicExample;
83
+ ```
84
+
85
+ ### Simple Skeleton with Custom Dimensions
86
+
87
+ ```jsx
88
+ import React from 'react';
89
+ import { UTSkeletonLoader } from '@widergy/mobile-ui';
90
+
91
+ const SimpleExample = () => {
92
+ return (
93
+ <UTSkeletonLoader
94
+ loading={true}
95
+ width="50%"
96
+ height={24}
97
+ alignment="center"
98
+ count={3}
99
+ spacing={12}
100
+ >
101
+ <UTLabel>Content will appear here</UTLabel>
102
+ </UTSkeletonLoader>
103
+ );
104
+ };
105
+
106
+ export default SimpleExample;
107
+ ```
108
+
109
+ ### Complex Layout Example - Account Selector
110
+
111
+ ```jsx
112
+ import React from 'react';
113
+ import { UTSkeletonLoader } from '@widergy/mobile-ui';
114
+
115
+ const AccountSelectorSkeleton = () => {
116
+ const accountSelectorItems = [
117
+ [
118
+ [{ height: 16, width: '100%' }], // Full width text line
119
+ [{ height: 18, width: 18 }] // Small square (avatar/icon)
120
+ ],
121
+ [
122
+ [{ height: 12, width: '100%' }] // Another full width text line
123
+ ]
124
+ ];
125
+
126
+ return (
127
+ <UTSkeletonLoader
128
+ loading={true}
129
+ items={accountSelectorItems}
130
+ spacing={0}
131
+ >
132
+ <UTLabel>Account Selector Content</UTLabel>
133
+ </UTSkeletonLoader>
134
+ );
135
+ };
136
+
137
+ export default AccountSelectorSkeleton;
138
+ ```
139
+
140
+ ### Grid Layout Example - Shortcuts
141
+
142
+ ```jsx
143
+ import React from 'react';
144
+ import { UTSkeletonLoader } from '@widergy/mobile-ui';
145
+
146
+ const ShortcutsSkeleton = () => {
147
+ const shortcutsItems = [
148
+ [
149
+ [
150
+ { height: 38, width: 38 }, // Icon
151
+ { height: 8, width: 60 } // Text below icon
152
+ ],
153
+ [
154
+ { height: 38, width: 38 }, // Icon
155
+ { height: 8, width: 60 } // Text below icon
156
+ ],
157
+ [
158
+ { height: 38, width: 38 }, // Icon
159
+ { height: 8, width: 60 } // Text below icon
160
+ ],
161
+ [
162
+ { height: 38, width: 38 }, // Icon
163
+ { height: 8, width: 60 } // Text below icon
164
+ ]
165
+ ]
166
+ ];
167
+
168
+ return (
169
+ <UTSkeletonLoader
170
+ loading={true}
171
+ alignment="center"
172
+ items={shortcutsItems}
173
+ >
174
+ <UTLabel>Shortcuts Content</UTLabel>
175
+ </UTSkeletonLoader>
176
+ );
177
+ };
178
+
179
+ export default ShortcutsSkeleton;
180
+ ```
181
+
182
+ ### Card Layout Example
183
+
184
+ ```jsx
185
+ import React from 'react';
186
+ import { UTSkeletonLoader } from '@widergy/mobile-ui';
187
+
188
+ const CardSkeleton = () => {
189
+ const cardItems = [
190
+ [
191
+ [{ height: 14, width: '100%' }], // Title
192
+ [{ height: 32, width: 32 }] // Icon/Image
193
+ ],
194
+ [
195
+ [
196
+ { height: 19, width: '100%' }, // Main text
197
+ { height: 12, width: '100%' } // Secondary text
198
+ ],
199
+ [{ height: 20, width: '100%' }] // Action area
200
+ ]
201
+ ];
202
+
203
+ return (
204
+ <UTSkeletonLoader
205
+ loading={true}
206
+ alignment="center"
207
+ items={cardItems}
208
+ >
209
+ <UTLabel>Card Content</UTLabel>
210
+ </UTSkeletonLoader>
211
+ );
212
+ };
213
+
214
+ export default CardSkeleton;
215
+ ```
216
+
217
+ ### Multiple Skeletons
218
+
219
+ ```jsx
220
+ import React from 'react';
221
+ import { UTSkeletonLoader } from '@widergy/mobile-ui';
222
+
223
+ const MultipleSkeleton = () => {
224
+ const listItemStructure = [
225
+ [
226
+ [{ height: 16, width: '80%' }], // Title
227
+ [{ height: 40, width: 40 }] // Avatar
228
+ ],
229
+ [
230
+ [{ height: 12, width: '60%' }] // Subtitle
231
+ ]
232
+ ];
233
+
234
+ return (
235
+ <UTSkeletonLoader
236
+ loading={true}
237
+ items={listItemStructure}
238
+ count={5} // Repeat the structure 5 times
239
+ spacing={16}
240
+ >
241
+ <UTLabel>List Content</UTLabel>
242
+ </UTSkeletonLoader>
243
+ );
244
+ };
245
+
246
+ export default MultipleSkeleton;
247
+ ```
248
+
249
+ ## Custom Styles
250
+
251
+ The `styles` prop allows you to customize the appearance of different parts of the skeleton:
252
+
253
+ ```jsx
254
+ const customStyles = {
255
+ container: {
256
+ backgroundColor: '#f5f5f5',
257
+ padding: 10
258
+ },
259
+ column: {
260
+ marginHorizontal: 5
261
+ },
262
+ item: {
263
+ borderRadius: 8
264
+ }
265
+ };
266
+
267
+ <UTSkeletonLoader
268
+ loading={true}
269
+ styles={customStyles}
270
+ >
271
+ <UTLabel>Content</UTLabel>
272
+ </UTSkeletonLoader>
273
+ ```
274
+
275
+ ## Best Practices
276
+
277
+ 1. **Match your content structure**: Design your skeleton items array to closely match the layout of your actual content.
278
+
279
+ 2. **Use appropriate dimensions**: Make skeleton item dimensions similar to your real content for a smoother transition.
280
+
281
+ 3. **Consider loading states**: Always provide meaningful content in the children prop for when loading is false.
282
+
283
+ 4. **Optimize for performance**: For repeated patterns, use the `count` prop instead of duplicating items in the array.
284
+
285
+ 5. **Test different screen sizes**: Ensure your skeleton layout works well across different device sizes by using percentage widths when appropriate.
@@ -0,0 +1,18 @@
1
+ export const DEFAULT_BACKGROUND_COLOR = '#E4E6EA';
2
+ export const SKELETON_OPACITY = {
3
+ FULL: '',
4
+ LIGHT: '59'
5
+ };
6
+
7
+ export const START_ANIMATION_CONFIG = {
8
+ toValue: 1,
9
+ duration: 1000,
10
+ useNativeDriver: true
11
+ };
12
+ export const END_ANIMATION_CONFIG = {
13
+ toValue: 0,
14
+ duration: 1000,
15
+ useNativeDriver: true
16
+ };
17
+
18
+ export const INPUT_RANGE = [0, 1];
@@ -0,0 +1,58 @@
1
+ import React, { useRef, useEffect } from 'react';
2
+ import { Animated } from 'react-native';
3
+ import { number, object, oneOfType, string } from 'prop-types';
4
+
5
+ import { mergeMultipleStyles } from '../../../../utils/styleUtils';
6
+
7
+ import { END_ANIMATION_CONFIG, INPUT_RANGE, START_ANIMATION_CONFIG } from './constants';
8
+ import { getOutputRange } from './utils';
9
+ import ownStyles from './styles';
10
+
11
+ const SkeletonItem = ({ alignment, height, style = {}, theme, width }) => {
12
+ const animatedValue = useRef(new Animated.Value(0)).current;
13
+
14
+ useEffect(() => {
15
+ const animation = Animated.loop(
16
+ Animated.sequence([
17
+ Animated.timing(animatedValue, START_ANIMATION_CONFIG),
18
+ Animated.timing(animatedValue, END_ANIMATION_CONFIG)
19
+ ])
20
+ );
21
+
22
+ animation.start();
23
+
24
+ return () => {
25
+ animation.stop();
26
+ };
27
+ }, [animatedValue]);
28
+
29
+ const backgroundColor = animatedValue.interpolate({
30
+ inputRange: INPUT_RANGE,
31
+ outputRange: getOutputRange(theme)
32
+ });
33
+
34
+ const styles = mergeMultipleStyles(ownStyles, style);
35
+
36
+ return (
37
+ <Animated.View
38
+ style={[
39
+ styles.skeleton(alignment),
40
+ {
41
+ width,
42
+ height,
43
+ backgroundColor
44
+ }
45
+ ]}
46
+ />
47
+ );
48
+ };
49
+
50
+ SkeletonItem.propTypes = {
51
+ alignment: string,
52
+ height: oneOfType([number, string]),
53
+ style: object,
54
+ theme: object,
55
+ width: oneOfType([number, string])
56
+ };
57
+
58
+ export default SkeletonItem;
@@ -0,0 +1,10 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ import { getAlignSelf } from '../../utils';
4
+
5
+ export default StyleSheet.create({
6
+ skeleton: alignment => ({
7
+ borderRadius: 4,
8
+ alignSelf: getAlignSelf(alignment)
9
+ })
10
+ });
@@ -0,0 +1,6 @@
1
+ import { DEFAULT_BACKGROUND_COLOR, SKELETON_OPACITY } from './constants';
2
+
3
+ export const getOutputRange = theme => [
4
+ `${theme?.Palette?.light?.['04'] || DEFAULT_BACKGROUND_COLOR}${SKELETON_OPACITY.FULL}`,
5
+ `${theme?.Palette?.light?.['04'] || DEFAULT_BACKGROUND_COLOR}${SKELETON_OPACITY.LIGHT}`
6
+ ];
@@ -0,0 +1,8 @@
1
+ export const DEFAULT_ITEM_PROPS = {
2
+ height: 16,
3
+ width: '100%'
4
+ };
5
+
6
+ export const DEFAULT_CONTENT = [[{ height: DEFAULT_ITEM_PROPS.height, width: DEFAULT_ITEM_PROPS.width }]];
7
+ export const DEFAULT_SPACING = 8;
8
+ export const DEFAULT_COUNT = 1;
@@ -0,0 +1,84 @@
1
+ /* eslint-disable react/no-array-index-key */
2
+ import React from 'react';
3
+ import { View } from 'react-native';
4
+ import { string, node, number, bool, object, oneOfType, shape, oneOf, arrayOf } from 'prop-types';
5
+
6
+ import { useTheme } from '../../theming';
7
+ import { mergeMultipleStyles } from '../../utils/styleUtils';
8
+ import { TEST_IDS } from '../../constants/testIds';
9
+
10
+ import SkeletonItem from './components/SkeletonItem';
11
+ import { getItemsToRender } from './utils';
12
+ import { DEFAULT_COUNT, DEFAULT_ITEM_PROPS, DEFAULT_SPACING } from './constants';
13
+ import ownStyles from './styles';
14
+
15
+ const { skeletonLoader } = TEST_IDS;
16
+
17
+ const UTSkeletonLoader = ({
18
+ alignment,
19
+ children,
20
+ count = DEFAULT_COUNT,
21
+ dataTestId = skeletonLoader,
22
+ height,
23
+ items = [],
24
+ loading = true,
25
+ spacing = DEFAULT_SPACING,
26
+ styles: customStyles = {},
27
+ width
28
+ }) => {
29
+ const theme = useTheme();
30
+ const styles = mergeMultipleStyles(ownStyles, customStyles, theme?.UTSkeletonLoader);
31
+ const itemsToRender = getItemsToRender(items, count);
32
+
33
+ return loading ? (
34
+ <View style={styles.skeletonContainer} testID={dataTestId}>
35
+ {itemsToRender.map((row, rowIndex) => (
36
+ <View key={rowIndex} style={styles.container(alignment)}>
37
+ {row.map((column, colIndex) => (
38
+ <View key={colIndex} style={styles.column(colIndex, row, spacing)}>
39
+ {column.map((item, itemIndex) => (
40
+ <View key={itemIndex} style={styles.item(item, spacing)}>
41
+ <SkeletonItem
42
+ alignment={item?.alignment || alignment}
43
+ height={height || item.height || DEFAULT_ITEM_PROPS.height}
44
+ style={item.style}
45
+ theme={theme}
46
+ width={width || item.width || DEFAULT_ITEM_PROPS.width}
47
+ />
48
+ </View>
49
+ ))}
50
+ </View>
51
+ ))}
52
+ </View>
53
+ ))}
54
+ </View>
55
+ ) : (
56
+ children
57
+ );
58
+ };
59
+
60
+ UTSkeletonLoader.propTypes = {
61
+ alignment: oneOf(['left', 'right', 'center', 'spaceBetween', 'spaceAround', 'spaceEvenly']),
62
+ children: node,
63
+ count: number,
64
+ dataTestId: string,
65
+ height: oneOfType([string, number]),
66
+ items: arrayOf(
67
+ arrayOf(
68
+ arrayOf(
69
+ shape({
70
+ height: oneOfType([string, number]),
71
+ width: oneOfType([string, number]),
72
+ alignment: oneOf(['left', 'right', 'center', 'spaceBetween', 'spaceAround', 'spaceEvenly']),
73
+ style: object
74
+ })
75
+ )
76
+ )
77
+ ),
78
+ loading: bool,
79
+ spacing: number,
80
+ styles: object,
81
+ width: oneOfType([string, number])
82
+ };
83
+
84
+ export default UTSkeletonLoader;
@@ -0,0 +1,31 @@
1
+ import { StyleSheet } from 'react-native';
2
+
3
+ import { DEFAULT_ITEM_PROPS } from './constants';
4
+ import { getAlignSelf } from './utils';
5
+
6
+ export default StyleSheet.create({
7
+ container: alignment => ({
8
+ alignItems: getAlignSelf(alignment),
9
+ flexDirection: 'row',
10
+ justifyContent: getAlignSelf(alignment)
11
+ }),
12
+ column: (colIndex, row, spacing) => {
13
+ const hasPercentageWidth = row[colIndex].some(item =>
14
+ (item?.width || DEFAULT_ITEM_PROPS.width)?.includes?.('%')
15
+ );
16
+
17
+ return {
18
+ flex: hasPercentageWidth ? 1 : 0,
19
+ flexShrink: hasPercentageWidth ? 1 : 0,
20
+ marginRight: colIndex !== row.length - 1 ? spacing : 0
21
+ };
22
+ },
23
+ item: (item, spacing) => ({
24
+ marginBottom: spacing,
25
+ padding: item.margin || 4,
26
+ paddingBottom: item.marginBottom,
27
+ paddingLeft: item.marginLeft,
28
+ paddingRight: item.marginRight,
29
+ paddingTop: item.marginTop
30
+ })
31
+ });
@@ -0,0 +1,27 @@
1
+ import isEmpty from 'lodash/isEmpty';
2
+
3
+ import { DEFAULT_CONTENT } from './constants';
4
+
5
+ export const getAlignSelf = alignment => {
6
+ switch (alignment) {
7
+ case 'left':
8
+ return 'flex-start';
9
+ case 'right':
10
+ return 'flex-end';
11
+ case 'center':
12
+ return 'center';
13
+ case 'spaceBetween':
14
+ return 'space-between';
15
+ case 'spaceAround':
16
+ return 'space-around';
17
+ case 'spaceEvenly':
18
+ return 'space-evenly';
19
+ default:
20
+ return 'flex-start';
21
+ }
22
+ };
23
+
24
+ export const getItemsToRender = (items, count) => {
25
+ if (!isEmpty(items)) return Array.from({ length: count }, () => items).flat();
26
+ return Array.from({ length: count }, () => DEFAULT_CONTENT);
27
+ };
@@ -1,6 +1,7 @@
1
1
  export const TEST_IDS = {
2
2
  modal: 'modal',
3
- roundView: 'roundView'
3
+ roundView: 'roundView',
4
+ skeletonLoader: 'skeletonLoader'
4
5
  };
5
6
 
6
7
  export const TEST_ID_CONSTANTS = {
package/lib/index.js CHANGED
@@ -44,6 +44,7 @@ export { default as UTButton } from './components/UTButton';
44
44
  export { default as UTButtonGroup } from './components/UTButtonGroup';
45
45
  export { default as UTCBUInput } from './components/UTCBUInput';
46
46
  export { default as UTCheckBox } from './components/UTCheckBox';
47
+ export { default as UTCuit } from './components/UTCuit';
47
48
  export { default as UTCheckList } from './components/UTCheckList';
48
49
  export { default as UTDataCategory } from './components/UTDataCategory';
49
50
  export { default as UTDataElement } from './components/UTDataElement';
@@ -64,6 +65,7 @@ export { default as UTRoundView } from './components/UTRoundView';
64
65
  export { default as UTSearchField } from './components/UTSearchField';
65
66
  export { default as UTSelect } from './components/UTSelect';
66
67
  export { default as UTSelectableCard } from './components/UTSelectableCard';
68
+ export { default as UTSkeletonLoader } from './components/UTSkeletonLoader';
67
69
  export { default as UTStatus } from './components/UTStatus';
68
70
  export { default as UTStatusMessage } from './components/UTStatusMessage';
69
71
  export { default as UTStepFeedback } from './components/UTStepFeedback';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@widergy/mobile-ui",
3
3
  "description": "Widergy Mobile Components",
4
4
  "author": "widergy",
5
- "version": "1.49.7",
5
+ "version": "1.50.1",
6
6
  "repository": "https://github.com/widergy/mobile-ui.git",
7
7
  "main": "lib/index.js",
8
8
  "files": [