@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 +14 -0
- package/lib/components/UTActionCard/constants.js +31 -0
- package/lib/components/UTActionCard/index.js +78 -50
- package/lib/components/UTCuit/constants.js +8 -0
- package/lib/components/UTCuit/index.js +191 -0
- package/lib/components/UTCuit/styles.js +33 -0
- package/lib/components/UTSkeletonLoader/README.md +285 -0
- package/lib/components/UTSkeletonLoader/components/SkeletonItem/constants.js +18 -0
- package/lib/components/UTSkeletonLoader/components/SkeletonItem/index.js +58 -0
- package/lib/components/UTSkeletonLoader/components/SkeletonItem/styles.js +10 -0
- package/lib/components/UTSkeletonLoader/components/SkeletonItem/utils.js +6 -0
- package/lib/components/UTSkeletonLoader/constants.js +8 -0
- package/lib/components/UTSkeletonLoader/index.js +84 -0
- package/lib/components/UTSkeletonLoader/styles.js +31 -0
- package/lib/components/UTSkeletonLoader/utils.js +27 -0
- package/lib/constants/testIds.js +2 -1
- package/lib/index.js +2 -0
- package/package.json +1 -1
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 {
|
|
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
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
</
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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,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,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,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
|
+
};
|
package/lib/constants/testIds.js
CHANGED
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