@ledgerhq/lumen-ui-rnative 0.1.30 → 0.1.32
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/dist/module/i18n/i18n.js +5 -1
- package/dist/module/i18n/i18n.js.map +1 -1
- package/dist/module/i18n/locales/en.json +5 -0
- package/dist/module/lib/Components/AddressInput/AddressInput.mdx +10 -12
- package/dist/module/lib/Components/AddressInput/AddressInput.stories.js +2 -1
- package/dist/module/lib/Components/AddressInput/AddressInput.stories.js.map +1 -1
- package/dist/module/lib/Components/AmountInput/AmountInput.mdx +3 -3
- package/dist/module/lib/Components/BaseInput/BaseInput.js +60 -32
- package/dist/module/lib/Components/BaseInput/BaseInput.js.map +1 -1
- package/dist/module/lib/Components/DotCount/DotCount.stories.js +9 -18
- package/dist/module/lib/Components/DotCount/DotCount.stories.js.map +1 -1
- package/dist/module/lib/Components/DotIndicator/DotIndicator.stories.js +3 -5
- package/dist/module/lib/Components/DotIndicator/DotIndicator.stories.js.map +1 -1
- package/dist/module/lib/Components/MediaImage/MediaImage.test.js +7 -3
- package/dist/module/lib/Components/MediaImage/MediaImage.test.js.map +1 -1
- package/dist/module/lib/Components/SearchInput/SearchInput.mdx +4 -10
- package/dist/module/lib/Components/SearchInput/SearchInput.stories.js +2 -1
- package/dist/module/lib/Components/SearchInput/SearchInput.stories.js.map +1 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.js +2 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.mdx +34 -4
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.stories.js +32 -7
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.stories.js.map +1 -1
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js +26 -0
- package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js.map +1 -1
- package/dist/module/lib/Components/TextInput/TextInput.js +4 -3
- package/dist/module/lib/Components/TextInput/TextInput.js.map +1 -1
- package/dist/module/lib/Components/TextInput/TextInput.mdx +8 -10
- package/dist/module/lib/Components/TextInput/TextInput.stories.js +40 -1
- package/dist/module/lib/Components/TextInput/TextInput.stories.js.map +1 -1
- package/dist/module/lib/Components/TextInput/TextInput.test.js +76 -0
- package/dist/module/lib/Components/TextInput/TextInput.test.js.map +1 -0
- package/dist/module/lib/Components/Trend/Trend.js +103 -0
- package/dist/module/lib/Components/Trend/Trend.js.map +1 -0
- package/dist/module/lib/Components/Trend/Trend.mdx +106 -0
- package/dist/module/lib/Components/Trend/Trend.stories.js +79 -0
- package/dist/module/lib/Components/Trend/Trend.stories.js.map +1 -0
- package/dist/module/lib/Components/Trend/Trend.test.js +149 -0
- package/dist/module/lib/Components/Trend/Trend.test.js.map +1 -0
- package/dist/module/lib/Components/Trend/index.js +5 -0
- package/dist/module/lib/Components/Trend/index.js.map +1 -0
- package/dist/module/lib/Components/Trend/types.js +4 -0
- package/dist/module/lib/Components/Trend/types.js.map +1 -0
- package/dist/module/lib/Components/index.js +5 -4
- package/dist/module/lib/Components/index.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Ar.js +2 -2
- package/dist/module/lib/Symbols/Icons/Ar.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/ArrowLeft.js +1 -1
- package/dist/module/lib/Symbols/Icons/ArrowLeft.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/ArrowRight.js +1 -1
- package/dist/module/lib/Symbols/Icons/ArrowRight.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/BasketPutIn.js +1 -1
- package/dist/module/lib/Symbols/Icons/BasketPutIn.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Clip.js +1 -1
- package/dist/module/lib/Symbols/Icons/ClockFill.js +5 -14
- package/dist/module/lib/Symbols/Icons/ClockFill.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/CloudDownload.js +1 -1
- package/dist/module/lib/Symbols/Icons/CloudDownload.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/CloudUpload.js +1 -1
- package/dist/module/lib/Symbols/Icons/CloudUpload.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/CoinsAddPlus.js +1 -1
- package/dist/module/lib/Symbols/Icons/CoinsAddPlus.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/CoinsCheck.js +1 -1
- package/dist/module/lib/Symbols/Icons/CoinsCheck.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/CoinsCross.js +1 -1
- package/dist/module/lib/Symbols/Icons/CoinsCross.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/ColorPalette.js +1 -1
- package/dist/module/lib/Symbols/Icons/ColorPalette.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/CryptoBitcoinCoin.js +1 -1
- package/dist/module/lib/Symbols/Icons/Csv.js +1 -1
- package/dist/module/lib/Symbols/Icons/Csv.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Discord.js +1 -1
- package/dist/module/lib/Symbols/Icons/Discord.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Download.js +1 -1
- package/dist/module/lib/Symbols/Icons/Download.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Exchange.js +1 -1
- package/dist/module/lib/Symbols/Icons/Exchange.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/ExchangeFill.js +1 -1
- package/dist/module/lib/Symbols/Icons/ExchangeFill.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/ExitLogout.js +1 -1
- package/dist/module/lib/Symbols/Icons/ExitLogout.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Expand.js +1 -1
- package/dist/module/lib/Symbols/Icons/Expand.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Experiment2.js +1 -1
- package/dist/module/lib/Symbols/Icons/Experiment2.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/ExternalLink.js +1 -1
- package/dist/module/lib/Symbols/Icons/ExternalLink.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/FileDownload.js +1 -1
- package/dist/module/lib/Symbols/Icons/FileDownload.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Github.js +1 -1
- package/dist/module/lib/Symbols/Icons/HandCard.js +1 -1
- package/dist/module/lib/Symbols/Icons/HandCard.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Infinite.js +1 -1
- package/dist/module/lib/Symbols/Icons/MobileArrow.js +1 -1
- package/dist/module/lib/Symbols/Icons/MobileArrow.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/NftHide.js +1 -1
- package/dist/module/lib/Symbols/Icons/NftHide.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Parachute.js +1 -1
- package/dist/module/lib/Symbols/Icons/Parachute.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/PictureImage.js +1 -1
- package/dist/module/lib/Symbols/Icons/Range.js +1 -1
- package/dist/module/lib/Symbols/Icons/Range.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Reddit.js +2 -2
- package/dist/module/lib/Symbols/Icons/Reduce.js +1 -1
- package/dist/module/lib/Symbols/Icons/Reduce.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Screens.js +1 -1
- package/dist/module/lib/Symbols/Icons/Screens.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Shapes.js +1 -1
- package/dist/module/lib/Symbols/Icons/Share.js +1 -1
- package/dist/module/lib/Symbols/Icons/Share.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/StarHalf.js +1 -1
- package/dist/module/lib/Symbols/Icons/StarHalf.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/TransferHorizontal.js +1 -1
- package/dist/module/lib/Symbols/Icons/TransferHorizontal.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/TransferVertical.js +1 -1
- package/dist/module/lib/Symbols/Icons/TransferVertical.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/Truck.js +1 -1
- package/dist/module/lib/Symbols/Icons/Usb.js +1 -1
- package/dist/module/lib/Symbols/Icons/Usb.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/UserArrowRight.js +1 -1
- package/dist/module/lib/Symbols/Icons/UserArrowRight.js.map +1 -1
- package/dist/module/lib/Symbols/Icons/WalletInput.js +1 -1
- package/dist/module/lib/Symbols/Icons/WalletInput.js.map +1 -1
- package/dist/typescript/src/i18n/i18n.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/BaseInput/BaseInput.d.ts +1 -1
- package/dist/typescript/src/lib/Components/BaseInput/BaseInput.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/BaseInput/types.d.ts +9 -2
- package/dist/typescript/src/lib/Components/BaseInput/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/SegmentedControl/SegmentedControl.d.ts +1 -1
- package/dist/typescript/src/lib/Components/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/SegmentedControl/types.d.ts +4 -0
- package/dist/typescript/src/lib/Components/SegmentedControl/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/TextInput/TextInput.d.ts +4 -3
- package/dist/typescript/src/lib/Components/TextInput/TextInput.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Trend/Trend.d.ts +3 -0
- package/dist/typescript/src/lib/Components/Trend/Trend.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Trend/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/Trend/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Trend/types.d.ts +20 -0
- package/dist/typescript/src/lib/Components/Trend/types.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/index.d.ts +5 -4
- package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/Symbols/Icons/ClockFill.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/i18n/i18n.ts +12 -1
- package/src/i18n/locales/en.json +5 -0
- package/src/lib/Components/AddressInput/AddressInput.mdx +10 -12
- package/src/lib/Components/AddressInput/AddressInput.stories.tsx +2 -1
- package/src/lib/Components/AmountInput/AmountInput.mdx +3 -3
- package/src/lib/Components/BaseInput/BaseInput.tsx +61 -29
- package/src/lib/Components/BaseInput/types.ts +10 -2
- package/src/lib/Components/DotCount/DotCount.stories.tsx +12 -10
- package/src/lib/Components/DotIndicator/DotIndicator.stories.tsx +1 -3
- package/src/lib/Components/MediaImage/MediaImage.test.tsx +7 -3
- package/src/lib/Components/SearchInput/SearchInput.mdx +4 -10
- package/src/lib/Components/SearchInput/SearchInput.stories.tsx +2 -1
- package/src/lib/Components/SegmentedControl/SegmentedControl.mdx +34 -4
- package/src/lib/Components/SegmentedControl/SegmentedControl.stories.tsx +34 -6
- package/src/lib/Components/SegmentedControl/SegmentedControl.test.tsx +27 -0
- package/src/lib/Components/SegmentedControl/SegmentedControl.tsx +2 -0
- package/src/lib/Components/SegmentedControl/types.ts +4 -0
- package/src/lib/Components/TextInput/TextInput.mdx +8 -10
- package/src/lib/Components/TextInput/TextInput.stories.tsx +41 -1
- package/src/lib/Components/TextInput/TextInput.test.tsx +90 -0
- package/src/lib/Components/TextInput/TextInput.tsx +4 -3
- package/src/lib/Components/Trend/Trend.mdx +106 -0
- package/src/lib/Components/Trend/Trend.stories.tsx +61 -0
- package/src/lib/Components/Trend/Trend.test.tsx +125 -0
- package/src/lib/Components/Trend/Trend.tsx +118 -0
- package/src/lib/Components/Trend/index.ts +2 -0
- package/src/lib/Components/Trend/types.ts +20 -0
- package/src/lib/Components/index.ts +5 -4
- package/src/lib/Symbols/Icons/Ar.tsx +2 -2
- package/src/lib/Symbols/Icons/ArrowLeft.tsx +1 -1
- package/src/lib/Symbols/Icons/ArrowRight.tsx +1 -1
- package/src/lib/Symbols/Icons/BasketPutIn.tsx +1 -1
- package/src/lib/Symbols/Icons/Clip.tsx +1 -1
- package/src/lib/Symbols/Icons/ClockFill.tsx +1 -9
- package/src/lib/Symbols/Icons/CloudDownload.tsx +1 -1
- package/src/lib/Symbols/Icons/CloudUpload.tsx +1 -1
- package/src/lib/Symbols/Icons/CoinsAddPlus.tsx +1 -1
- package/src/lib/Symbols/Icons/CoinsCheck.tsx +1 -1
- package/src/lib/Symbols/Icons/CoinsCross.tsx +1 -1
- package/src/lib/Symbols/Icons/ColorPalette.tsx +1 -1
- package/src/lib/Symbols/Icons/CryptoBitcoinCoin.tsx +1 -1
- package/src/lib/Symbols/Icons/Csv.tsx +1 -1
- package/src/lib/Symbols/Icons/Discord.tsx +1 -1
- package/src/lib/Symbols/Icons/Download.tsx +1 -1
- package/src/lib/Symbols/Icons/Exchange.tsx +1 -1
- package/src/lib/Symbols/Icons/ExchangeFill.tsx +1 -1
- package/src/lib/Symbols/Icons/ExitLogout.tsx +1 -1
- package/src/lib/Symbols/Icons/Expand.tsx +1 -1
- package/src/lib/Symbols/Icons/Experiment2.tsx +1 -1
- package/src/lib/Symbols/Icons/ExternalLink.tsx +1 -1
- package/src/lib/Symbols/Icons/FileDownload.tsx +1 -1
- package/src/lib/Symbols/Icons/Github.tsx +1 -1
- package/src/lib/Symbols/Icons/HandCard.tsx +1 -1
- package/src/lib/Symbols/Icons/Infinite.tsx +1 -1
- package/src/lib/Symbols/Icons/MobileArrow.tsx +1 -1
- package/src/lib/Symbols/Icons/NftHide.tsx +1 -1
- package/src/lib/Symbols/Icons/Parachute.tsx +1 -1
- package/src/lib/Symbols/Icons/PictureImage.tsx +1 -1
- package/src/lib/Symbols/Icons/Range.tsx +1 -1
- package/src/lib/Symbols/Icons/Reddit.tsx +2 -2
- package/src/lib/Symbols/Icons/Reduce.tsx +1 -1
- package/src/lib/Symbols/Icons/Screens.tsx +1 -1
- package/src/lib/Symbols/Icons/Shapes.tsx +1 -1
- package/src/lib/Symbols/Icons/Share.tsx +1 -1
- package/src/lib/Symbols/Icons/StarHalf.tsx +1 -1
- package/src/lib/Symbols/Icons/TransferHorizontal.tsx +1 -1
- package/src/lib/Symbols/Icons/TransferVertical.tsx +1 -1
- package/src/lib/Symbols/Icons/Truck.tsx +1 -1
- package/src/lib/Symbols/Icons/Usb.tsx +1 -1
- package/src/lib/Symbols/Icons/UserArrowRight.tsx +1 -1
- package/src/lib/Symbols/Icons/WalletInput.tsx +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DisabledProvider,
|
|
3
|
+
resolveBaseInputPlaceholder,
|
|
3
4
|
useDisabledContext,
|
|
4
5
|
} from '@ledgerhq/lumen-utils-shared';
|
|
5
6
|
import {
|
|
@@ -21,6 +22,7 @@ import { useCommonTranslation } from '../../../i18n';
|
|
|
21
22
|
import type { LumenStyleSheetTheme } from '../../../styles';
|
|
22
23
|
import { useStyleSheet, useTheme } from '../../../styles';
|
|
23
24
|
import { useTimingConfig } from '../../Animations/useTimingConfig';
|
|
25
|
+
import { CheckmarkCircleFill } from '../../Symbols/Icons/CheckmarkCircleFill';
|
|
24
26
|
import { DeleteCircleFill } from '../../Symbols/Icons/DeleteCircleFill';
|
|
25
27
|
import { RuntimeConstants } from '../../utils';
|
|
26
28
|
import { InteractiveIcon } from '../InteractiveIcon';
|
|
@@ -34,7 +36,8 @@ export const BaseInput = ({
|
|
|
34
36
|
inputStyle,
|
|
35
37
|
labelStyle,
|
|
36
38
|
label,
|
|
37
|
-
|
|
39
|
+
helperText,
|
|
40
|
+
status,
|
|
38
41
|
hideClearButton,
|
|
39
42
|
onChangeText: onChangeTextProp,
|
|
40
43
|
editable,
|
|
@@ -42,6 +45,7 @@ export const BaseInput = ({
|
|
|
42
45
|
prefix,
|
|
43
46
|
suffix,
|
|
44
47
|
ref,
|
|
48
|
+
placeholder: placeholderProp,
|
|
45
49
|
...props
|
|
46
50
|
}: BaseInputProps) => {
|
|
47
51
|
const disabled = useDisabledContext({
|
|
@@ -65,6 +69,12 @@ export const BaseInput = ({
|
|
|
65
69
|
? !!props.value && props.value.length > 0
|
|
66
70
|
: uncontrolledValue.length > 0;
|
|
67
71
|
|
|
72
|
+
const { inputPlaceholder, labelStaysFloatedWithPlaceholder } =
|
|
73
|
+
resolveBaseInputPlaceholder({
|
|
74
|
+
label,
|
|
75
|
+
placeholder: placeholderProp,
|
|
76
|
+
});
|
|
77
|
+
|
|
68
78
|
const showClearButton = hasContent && !disabled && !hideClearButton;
|
|
69
79
|
|
|
70
80
|
const handleChangeText = useCallback(
|
|
@@ -87,7 +97,7 @@ export const BaseInput = ({
|
|
|
87
97
|
};
|
|
88
98
|
|
|
89
99
|
const styles = useStyles({
|
|
90
|
-
|
|
100
|
+
status,
|
|
91
101
|
isFocused,
|
|
92
102
|
isEditable: !disabled,
|
|
93
103
|
hasLabel: !!label,
|
|
@@ -97,8 +107,9 @@ export const BaseInput = ({
|
|
|
97
107
|
hasContent,
|
|
98
108
|
isFocused,
|
|
99
109
|
showClearButton,
|
|
100
|
-
|
|
110
|
+
status,
|
|
101
111
|
isEditable: !disabled,
|
|
112
|
+
labelStaysFloatedWithPlaceholder,
|
|
102
113
|
});
|
|
103
114
|
|
|
104
115
|
return (
|
|
@@ -114,6 +125,7 @@ export const BaseInput = ({
|
|
|
114
125
|
<TextInput
|
|
115
126
|
ref={inputRef}
|
|
116
127
|
value={value}
|
|
128
|
+
placeholder={inputPlaceholder}
|
|
117
129
|
style={StyleSheet.flatten([styles.input, inputStyle])}
|
|
118
130
|
onFocus={() => setIsFocused(true)}
|
|
119
131
|
onBlur={() => setIsFocused(false)}
|
|
@@ -158,10 +170,13 @@ export const BaseInput = ({
|
|
|
158
170
|
)}
|
|
159
171
|
</Pressable>
|
|
160
172
|
|
|
161
|
-
{
|
|
162
|
-
<View style={styles.
|
|
163
|
-
<DeleteCircleFill size={16} color='error' />
|
|
164
|
-
|
|
173
|
+
{!!helperText && (
|
|
174
|
+
<View style={styles.helperContainer}>
|
|
175
|
+
{status === 'error' && <DeleteCircleFill size={16} color='error' />}
|
|
176
|
+
{status === 'success' && (
|
|
177
|
+
<CheckmarkCircleFill size={16} color='success' />
|
|
178
|
+
)}
|
|
179
|
+
<Text style={styles.helperText}>{helperText}</Text>
|
|
165
180
|
</View>
|
|
166
181
|
)}
|
|
167
182
|
</Box>
|
|
@@ -170,18 +185,25 @@ export const BaseInput = ({
|
|
|
170
185
|
};
|
|
171
186
|
|
|
172
187
|
const useStyles = ({
|
|
173
|
-
|
|
188
|
+
status,
|
|
174
189
|
isFocused,
|
|
175
190
|
isEditable,
|
|
176
191
|
hasLabel,
|
|
177
192
|
}: {
|
|
178
|
-
|
|
193
|
+
status: 'error' | 'success' | undefined;
|
|
179
194
|
isFocused: boolean;
|
|
180
195
|
isEditable: boolean;
|
|
181
196
|
hasLabel: boolean;
|
|
182
197
|
}) => {
|
|
183
198
|
return useStyleSheet(
|
|
184
199
|
(t) => {
|
|
200
|
+
const hasStatusBorder = status === 'error' || status === 'success';
|
|
201
|
+
const statusBorderColors = {
|
|
202
|
+
error: t.colors.border.error,
|
|
203
|
+
success: t.colors.border.success,
|
|
204
|
+
} as const;
|
|
205
|
+
const statusBorderColor = status ? statusBorderColors[status] : undefined;
|
|
206
|
+
|
|
185
207
|
return {
|
|
186
208
|
container: StyleSheet.flatten([
|
|
187
209
|
{
|
|
@@ -198,15 +220,16 @@ const useStyles = ({
|
|
|
198
220
|
borderColor: 'transparent',
|
|
199
221
|
overflow: 'hidden',
|
|
200
222
|
},
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
223
|
+
hasStatusBorder &&
|
|
224
|
+
statusBorderColor && {
|
|
225
|
+
borderWidth: isFocused ? t.borderWidth.s2 : t.borderWidth.s1,
|
|
226
|
+
borderColor: statusBorderColor,
|
|
227
|
+
},
|
|
205
228
|
!isEditable && {
|
|
206
229
|
backgroundColor: t.colors.bg.disabled,
|
|
207
230
|
},
|
|
208
231
|
isFocused &&
|
|
209
|
-
!
|
|
232
|
+
!hasStatusBorder &&
|
|
210
233
|
isEditable && { borderColor: t.colors.border.active },
|
|
211
234
|
]),
|
|
212
235
|
input: StyleSheet.flatten([
|
|
@@ -217,31 +240,37 @@ const useStyles = ({
|
|
|
217
240
|
color: t.colors.text.base,
|
|
218
241
|
backgroundColor: t.colors.bg.muted,
|
|
219
242
|
outline: 'none',
|
|
220
|
-
...t.typographies.
|
|
221
|
-
paddingTop: t.spacings.
|
|
222
|
-
paddingBottom: t.spacings.
|
|
243
|
+
...t.typographies.body1,
|
|
244
|
+
paddingTop: t.spacings.s4,
|
|
245
|
+
paddingBottom: t.spacings.s2,
|
|
223
246
|
},
|
|
224
247
|
hasLabel && {
|
|
225
248
|
paddingTop: t.spacings.s20,
|
|
226
249
|
paddingBottom: t.spacings.s4,
|
|
227
250
|
paddingHorizontal: 0,
|
|
251
|
+
...t.typographies.body2,
|
|
228
252
|
},
|
|
229
|
-
RuntimeConstants.isIOS &&
|
|
253
|
+
RuntimeConstants.isIOS && { lineHeight: 0 },
|
|
230
254
|
RuntimeConstants.isAndroid && { includeFontPadding: false },
|
|
231
255
|
!isEditable && {
|
|
232
256
|
backgroundColor: t.colors.bg.disabled,
|
|
233
257
|
color: t.colors.text.disabled,
|
|
234
258
|
},
|
|
235
259
|
]),
|
|
236
|
-
|
|
260
|
+
helperContainer: {
|
|
237
261
|
marginTop: t.spacings.s8,
|
|
238
262
|
flexDirection: 'row',
|
|
239
263
|
alignItems: 'center',
|
|
240
264
|
gap: t.spacings.s2,
|
|
241
265
|
},
|
|
242
|
-
|
|
243
|
-
color: t.colors.text.error,
|
|
266
|
+
helperText: {
|
|
244
267
|
...t.typographies.body3,
|
|
268
|
+
flex: 1,
|
|
269
|
+
color: {
|
|
270
|
+
error: t.colors.text.error,
|
|
271
|
+
success: t.colors.text.success,
|
|
272
|
+
default: t.colors.text.muted,
|
|
273
|
+
}[status ?? 'default'],
|
|
245
274
|
},
|
|
246
275
|
suffixContainer: {
|
|
247
276
|
minWidth: t.sizes.s20,
|
|
@@ -250,7 +279,7 @@ const useStyles = ({
|
|
|
250
279
|
},
|
|
251
280
|
};
|
|
252
281
|
},
|
|
253
|
-
[
|
|
282
|
+
[status, isFocused, isEditable, hasLabel],
|
|
254
283
|
);
|
|
255
284
|
};
|
|
256
285
|
|
|
@@ -296,14 +325,16 @@ const useFloatingLabelStyles = ({
|
|
|
296
325
|
isFocused,
|
|
297
326
|
hasContent,
|
|
298
327
|
showClearButton,
|
|
299
|
-
|
|
328
|
+
status,
|
|
300
329
|
isEditable,
|
|
330
|
+
labelStaysFloatedWithPlaceholder,
|
|
301
331
|
}: {
|
|
302
332
|
isFocused: boolean;
|
|
303
333
|
hasContent: boolean;
|
|
304
334
|
showClearButton: boolean;
|
|
305
|
-
|
|
335
|
+
status: 'error' | 'success' | undefined;
|
|
306
336
|
isEditable: boolean;
|
|
337
|
+
labelStaysFloatedWithPlaceholder: boolean;
|
|
307
338
|
}) => {
|
|
308
339
|
const { theme } = useTheme();
|
|
309
340
|
|
|
@@ -320,20 +351,21 @@ const useFloatingLabelStyles = ({
|
|
|
320
351
|
showClearButton && {
|
|
321
352
|
width: '92%',
|
|
322
353
|
},
|
|
354
|
+
status === 'error' && {
|
|
355
|
+
color: t.colors.text.error,
|
|
356
|
+
},
|
|
323
357
|
!isEditable && {
|
|
324
358
|
color: t.colors.text.disabled,
|
|
325
359
|
},
|
|
326
|
-
hasError && {
|
|
327
|
-
color: t.colors.text.error,
|
|
328
|
-
},
|
|
329
360
|
]),
|
|
330
361
|
}),
|
|
331
|
-
[hasContent, showClearButton,
|
|
362
|
+
[hasContent, showClearButton, status, isEditable],
|
|
332
363
|
);
|
|
333
364
|
|
|
334
365
|
const { animatedStyle } = useAnimatedFloatingLabel({
|
|
335
366
|
theme,
|
|
336
|
-
isFloatingLabel:
|
|
367
|
+
isFloatingLabel:
|
|
368
|
+
isFocused || hasContent || labelStaysFloatedWithPlaceholder,
|
|
337
369
|
});
|
|
338
370
|
|
|
339
371
|
return { label, animatedStyle };
|
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
} from 'react-native';
|
|
7
7
|
import type { BoxProps } from '../Utility';
|
|
8
8
|
|
|
9
|
+
export type BaseInputStatus = 'error' | 'success';
|
|
10
|
+
|
|
9
11
|
export type BaseInputProps = {
|
|
10
12
|
/**
|
|
11
13
|
* The label text that floats above the input when focused or filled.
|
|
@@ -35,9 +37,15 @@ export type BaseInputProps = {
|
|
|
35
37
|
*/
|
|
36
38
|
labelStyle?: StyleProp<TextStyle>;
|
|
37
39
|
/**
|
|
38
|
-
*
|
|
40
|
+
* Optional text shown below the input (hint, error, or success copy).
|
|
41
|
+
* Pair with `status` for error/success styling and icons; omit `status` for a neutral hint.
|
|
42
|
+
*/
|
|
43
|
+
helperText?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Visual state for border, helper text, helper icon, and the label in error state.
|
|
46
|
+
* Omit when `helperText` is a neutral hint.
|
|
39
47
|
*/
|
|
40
|
-
|
|
48
|
+
status?: BaseInputStatus;
|
|
41
49
|
/**
|
|
42
50
|
* Custom content to render after the input (right side in LTR).
|
|
43
51
|
* @example suffix={<Icon />}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import { Avatar } from '../Avatar/Avatar';
|
|
4
3
|
import { MediaImage } from '../MediaImage/MediaImage';
|
|
5
4
|
import {
|
|
6
5
|
SegmentedControl,
|
|
@@ -89,7 +88,14 @@ export const WithChildren: Story = {
|
|
|
89
88
|
|
|
90
89
|
return (
|
|
91
90
|
<Box lx={{ gap: 's24' }}>
|
|
92
|
-
<Box
|
|
91
|
+
<Box
|
|
92
|
+
lx={{
|
|
93
|
+
flexDirection: 'row',
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
justifyContent: 'center',
|
|
96
|
+
gap: 's12',
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
93
99
|
<DotCount value={5} size='md'>
|
|
94
100
|
<MediaImage
|
|
95
101
|
src='https://crypto-icons.ledger.com/BTC.png'
|
|
@@ -98,12 +104,6 @@ export const WithChildren: Story = {
|
|
|
98
104
|
shape='circle'
|
|
99
105
|
/>
|
|
100
106
|
</DotCount>
|
|
101
|
-
<DotCount value={100} size='md'>
|
|
102
|
-
<Avatar
|
|
103
|
-
src='https://plus.unsplash.com/premium_photo-1689551670902-19b441a6afde?q=80&w=774&auto=format&fit=crop'
|
|
104
|
-
size='md'
|
|
105
|
-
/>
|
|
106
|
-
</DotCount>
|
|
107
107
|
</Box>
|
|
108
108
|
<SegmentedControl
|
|
109
109
|
selectedValue={fitState}
|
|
@@ -111,9 +111,11 @@ export const WithChildren: Story = {
|
|
|
111
111
|
tabLayout='fit'
|
|
112
112
|
accessibilityLabel='Fit layout'
|
|
113
113
|
>
|
|
114
|
-
<SegmentedControlButton
|
|
114
|
+
<SegmentedControlButton
|
|
115
|
+
value='preview'
|
|
116
|
+
trailingContent={<DotCount value={3} size='md' />}
|
|
117
|
+
>
|
|
115
118
|
Preview
|
|
116
|
-
<DotCount value={3} size='md' style={{ marginLeft: 6 }} />
|
|
117
119
|
</SegmentedControlButton>
|
|
118
120
|
<SegmentedControlButton value='raw'>Raw</SegmentedControlButton>
|
|
119
121
|
<SegmentedControlButton value='blame'>Blame</SegmentedControlButton>
|
|
@@ -66,9 +66,7 @@ export const WithChildren: Story = {
|
|
|
66
66
|
<DotIndicator appearance='red'>
|
|
67
67
|
<Button size='sm'>Submit</Button>
|
|
68
68
|
</DotIndicator>
|
|
69
|
-
<
|
|
70
|
-
<Avatar size='md' />
|
|
71
|
-
</DotIndicator>
|
|
69
|
+
<Avatar size='md' showNotification />
|
|
72
70
|
<DotIndicator appearance='red'>
|
|
73
71
|
<IconButton accessibilityLabel='Settings' icon={Settings} />
|
|
74
72
|
</DotIndicator>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
|
-
import { render, waitFor } from '@testing-library/react-native';
|
|
3
|
+
import { act, render, waitFor } from '@testing-library/react-native';
|
|
4
4
|
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
5
5
|
import { MediaImage } from './MediaImage';
|
|
6
6
|
|
|
@@ -89,7 +89,9 @@ describe('MediaImage Component', () => {
|
|
|
89
89
|
);
|
|
90
90
|
|
|
91
91
|
const img = getByTestId('media-image-img');
|
|
92
|
-
|
|
92
|
+
act(() => {
|
|
93
|
+
img.props.onError();
|
|
94
|
+
});
|
|
93
95
|
|
|
94
96
|
rerender(
|
|
95
97
|
<TestWrapper>
|
|
@@ -140,7 +142,9 @@ describe('MediaImage Component', () => {
|
|
|
140
142
|
);
|
|
141
143
|
|
|
142
144
|
const img = getByTestId('media-image-img');
|
|
143
|
-
|
|
145
|
+
act(() => {
|
|
146
|
+
img.props.onError();
|
|
147
|
+
});
|
|
144
148
|
|
|
145
149
|
rerender(
|
|
146
150
|
<TestWrapper>
|
|
@@ -67,17 +67,10 @@ Alternatively, use `editable={false}` to prevent editing without applying the mu
|
|
|
67
67
|
|
|
68
68
|
### Error State
|
|
69
69
|
|
|
70
|
-
The search component supports error handling through `
|
|
70
|
+
The search component supports error handling through `helperText` and `status` (`'error'` \| `'success'`), which show copy below the input with matching border and text styling.
|
|
71
71
|
|
|
72
72
|
<Canvas of={SearchInputStories.WithError} />
|
|
73
73
|
|
|
74
|
-
The error message will be automatically:
|
|
75
|
-
|
|
76
|
-
- Connected to the input
|
|
77
|
-
- Displayed with a warning icon
|
|
78
|
-
- Styled in the error color
|
|
79
|
-
- Announced by screen readers
|
|
80
|
-
|
|
81
74
|
## Controlled vs Uncontrolled
|
|
82
75
|
|
|
83
76
|
The SearchInput component supports both controlled and uncontrolled usage.
|
|
@@ -232,13 +225,14 @@ function MyComponent() {
|
|
|
232
225
|
placeholder='Search products'
|
|
233
226
|
value={query}
|
|
234
227
|
onChangeText={handleSearch}
|
|
235
|
-
|
|
228
|
+
helperText={error || undefined}
|
|
229
|
+
status={error ? 'error' : undefined}
|
|
236
230
|
/>
|
|
237
231
|
);
|
|
238
232
|
}
|
|
239
233
|
```
|
|
240
234
|
|
|
241
|
-
> **Note:**
|
|
235
|
+
> **Note:** Helper text is optional. Use `helperText` with `status="error"` to show validation feedback below the search input.
|
|
242
236
|
|
|
243
237
|
### With Custom Styling
|
|
244
238
|
|
|
@@ -25,6 +25,7 @@ SegmentedControl is a tab bar–style component for switching between mutually e
|
|
|
25
25
|
- **Segments**: Individual options the user can select.
|
|
26
26
|
- **Selected state**: The active segment (sliding pill + semi-bold label).
|
|
27
27
|
- **Optional icon**: Icon to the left of the label (from Symbols).
|
|
28
|
+
- **Optional trailing content**: Any node (e.g. `DotCount`) rendered to the right of the label.
|
|
28
29
|
- **Appearance**: Use `appearance="background"` (default) for a surface background, or `appearance="no-background"` for a transparent container.
|
|
29
30
|
|
|
30
31
|
## Properties
|
|
@@ -40,6 +41,12 @@ You can use segments with text only (Base), or add an icon to each button (WithI
|
|
|
40
41
|
|
|
41
42
|
<Canvas of={SegmentedControlStories.WithIcons} />
|
|
42
43
|
|
|
44
|
+
### With trailing content
|
|
45
|
+
|
|
46
|
+
Use `trailingContent` to render content (e.g. `DotCount`) to the right of the label.
|
|
47
|
+
|
|
48
|
+
<Canvas of={SegmentedControlStories.WithTrailingContent} />
|
|
49
|
+
|
|
43
50
|
## Responsive Layout
|
|
44
51
|
|
|
45
52
|
SegmentedControl lays out segments in a horizontal row with equal width per segment. The sliding pill animates to the selected segment.
|
|
@@ -80,6 +87,33 @@ export default function Example() {
|
|
|
80
87
|
}
|
|
81
88
|
```
|
|
82
89
|
|
|
90
|
+
## With trailing content
|
|
91
|
+
|
|
92
|
+
Pass any node via `trailingContent` to render it to the right of the label. Use it for badges like `DotCount` to surface counts per segment.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { SegmentedControl, SegmentedControlButton, DotCount } from '@ledgerhq/lumen-ui-rnative';
|
|
96
|
+
|
|
97
|
+
function Example() {
|
|
98
|
+
const [state, setState] = React.useState('tokens');
|
|
99
|
+
return (
|
|
100
|
+
<SegmentedControl
|
|
101
|
+
selectedValue={state}
|
|
102
|
+
onSelectedChange={setState}
|
|
103
|
+
accessibilityLabel='Asset section'
|
|
104
|
+
>
|
|
105
|
+
<SegmentedControlButton value='tokens' trailingContent={<DotCount value={3} />}>
|
|
106
|
+
Tokens
|
|
107
|
+
</SegmentedControlButton>
|
|
108
|
+
<SegmentedControlButton value='nfts' trailingContent={<DotCount value={12} />}>
|
|
109
|
+
NFTs
|
|
110
|
+
</SegmentedControlButton>
|
|
111
|
+
<SegmentedControlButton value='activity'>Activity</SegmentedControlButton>
|
|
112
|
+
</SegmentedControl>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
83
117
|
## With icons
|
|
84
118
|
|
|
85
119
|
Pass an icon from symbols to each button for a left-positioned icon. Use icons on all segments or none for consistency.
|
|
@@ -113,9 +147,5 @@ function Example() {
|
|
|
113
147
|
}
|
|
114
148
|
```
|
|
115
149
|
|
|
116
|
-
<Box lx={{ flexDirection: 'column', gap: 's24' }}>
|
|
117
|
-
<CommonRulesDoAndDont />
|
|
118
|
-
</Box>
|
|
119
|
-
|
|
120
150
|
</Tab>
|
|
121
151
|
</CustomTabs>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import { Coins, Nft, TransferHorizontal
|
|
3
|
+
import { Coins, Nft, TransferHorizontal } from '../../Symbols';
|
|
4
|
+
import { DotCount } from '../DotCount';
|
|
4
5
|
import { Box } from '../Utility';
|
|
5
6
|
import { SegmentedControl, SegmentedControlButton } from './SegmentedControl';
|
|
6
7
|
|
|
@@ -74,11 +75,8 @@ export const WithIcons: Story = {
|
|
|
74
75
|
<SegmentedControlButton value='nfts' icon={Nft}>
|
|
75
76
|
NFTs
|
|
76
77
|
</SegmentedControlButton>
|
|
77
|
-
<SegmentedControlButton value='
|
|
78
|
-
|
|
79
|
-
</SegmentedControlButton>
|
|
80
|
-
<SegmentedControlButton value='settings' icon={Settings}>
|
|
81
|
-
Settings
|
|
78
|
+
<SegmentedControlButton value='trade' icon={TransferHorizontal}>
|
|
79
|
+
Trade
|
|
82
80
|
</SegmentedControlButton>
|
|
83
81
|
</SegmentedControl>
|
|
84
82
|
);
|
|
@@ -142,3 +140,33 @@ export const Disabled: Story = {
|
|
|
142
140
|
</SegmentedControl>
|
|
143
141
|
),
|
|
144
142
|
};
|
|
143
|
+
|
|
144
|
+
export const WithTrailingContent: Story = {
|
|
145
|
+
args: {} as React.ComponentProps<typeof SegmentedControl>,
|
|
146
|
+
render: (args) => {
|
|
147
|
+
const [state, setState] = useState('tokens');
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<SegmentedControl
|
|
151
|
+
{...args}
|
|
152
|
+
selectedValue={state}
|
|
153
|
+
onSelectedChange={setState}
|
|
154
|
+
accessibilityLabel='Asset section'
|
|
155
|
+
>
|
|
156
|
+
<SegmentedControlButton
|
|
157
|
+
value='tokens'
|
|
158
|
+
trailingContent={<DotCount value={3} />}
|
|
159
|
+
>
|
|
160
|
+
Tokens
|
|
161
|
+
</SegmentedControlButton>
|
|
162
|
+
<SegmentedControlButton
|
|
163
|
+
value='nfts'
|
|
164
|
+
trailingContent={<DotCount value={12} />}
|
|
165
|
+
>
|
|
166
|
+
NFTs
|
|
167
|
+
</SegmentedControlButton>
|
|
168
|
+
<SegmentedControlButton value='trade'>Trade</SegmentedControlButton>
|
|
169
|
+
</SegmentedControl>
|
|
170
|
+
);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, jest } from '@jest/globals';
|
|
2
2
|
import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
|
|
3
3
|
import { render, fireEvent } from '@testing-library/react-native';
|
|
4
|
+
import { DotCount } from '../DotCount';
|
|
4
5
|
import { ThemeProvider } from '../ThemeProvider/ThemeProvider';
|
|
5
6
|
import { SegmentedControl, SegmentedControlButton } from './SegmentedControl';
|
|
6
7
|
|
|
@@ -53,4 +54,30 @@ describe('SegmentedControl', () => {
|
|
|
53
54
|
|
|
54
55
|
expect(onSelectedChange).toHaveBeenCalledWith('receive');
|
|
55
56
|
});
|
|
57
|
+
|
|
58
|
+
it('renders trailingContent inside segment buttons', () => {
|
|
59
|
+
const { getByLabelText } = render(
|
|
60
|
+
<TestWrapper>
|
|
61
|
+
<SegmentedControl
|
|
62
|
+
selectedValue='tokens'
|
|
63
|
+
onSelectedChange={() => {
|
|
64
|
+
/* empty */
|
|
65
|
+
}}
|
|
66
|
+
accessibilityLabel='Asset section'
|
|
67
|
+
>
|
|
68
|
+
<SegmentedControlButton
|
|
69
|
+
value='tokens'
|
|
70
|
+
trailingContent={
|
|
71
|
+
<DotCount value={3} accessibilityLabel='3 tokens' />
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
Tokens
|
|
75
|
+
</SegmentedControlButton>
|
|
76
|
+
<SegmentedControlButton value='nfts'>NFTs</SegmentedControlButton>
|
|
77
|
+
</SegmentedControl>
|
|
78
|
+
</TestWrapper>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(getByLabelText('3 tokens')).toBeTruthy();
|
|
82
|
+
});
|
|
56
83
|
});
|
|
@@ -21,6 +21,7 @@ export function SegmentedControlButton({
|
|
|
21
21
|
value,
|
|
22
22
|
children,
|
|
23
23
|
icon: Icon,
|
|
24
|
+
trailingContent,
|
|
24
25
|
onPress,
|
|
25
26
|
...props
|
|
26
27
|
}: SegmentedControlButtonProps) {
|
|
@@ -71,6 +72,7 @@ export function SegmentedControlButton({
|
|
|
71
72
|
>
|
|
72
73
|
{children}
|
|
73
74
|
</Text>
|
|
75
|
+
{trailingContent}
|
|
74
76
|
</Box>
|
|
75
77
|
</Pressable>
|
|
76
78
|
);
|
|
@@ -56,6 +56,10 @@ export type SegmentedControlButtonProps = {
|
|
|
56
56
|
* Optional icon shown to the left of the label (from Symbols).
|
|
57
57
|
*/
|
|
58
58
|
icon?: IconComponent;
|
|
59
|
+
/**
|
|
60
|
+
* Optional content shown to the right of the label (e.g. DotCount badge).
|
|
61
|
+
*/
|
|
62
|
+
trailingContent?: ReactNode;
|
|
59
63
|
/**
|
|
60
64
|
* Optional callback when the button is pressed (in addition to onSelectedChange on the parent).
|
|
61
65
|
*/
|
|
@@ -34,6 +34,10 @@ The label text automatically floats above the input when content is entered, pro
|
|
|
34
34
|
|
|
35
35
|
<Canvas of={TextInputStories.WithContent} />
|
|
36
36
|
|
|
37
|
+
### Label and placeholder
|
|
38
|
+
|
|
39
|
+
<Canvas of={TextInputStories.WithLabelAndPlaceholder} />
|
|
40
|
+
|
|
37
41
|
### Clear Button
|
|
38
42
|
|
|
39
43
|
A clear button (×) appears **automatically** when input has content.
|
|
@@ -54,17 +58,10 @@ Use `onClear` to extend the default clear behavior with custom logic.
|
|
|
54
58
|
|
|
55
59
|
### Error State
|
|
56
60
|
|
|
57
|
-
The input supports error handling through `
|
|
61
|
+
The input supports error handling through `helperText` and `status` (`'error'` \| `'success'`), which show copy below the input with matching border and text styling.
|
|
58
62
|
|
|
59
63
|
<Canvas of={TextInputStories.WithError} />
|
|
60
64
|
|
|
61
|
-
The error message will be automatically:
|
|
62
|
-
|
|
63
|
-
- Connected to the input
|
|
64
|
-
- Displayed with a warning icon
|
|
65
|
-
- Styled in the error color
|
|
66
|
-
- Announced by screen readers
|
|
67
|
-
|
|
68
65
|
### Disabled State
|
|
69
66
|
|
|
70
67
|
The input can be fully disabled using the `disabled` prop, which prevents interaction and applies a muted visual style.
|
|
@@ -221,13 +218,14 @@ function MyComponent() {
|
|
|
221
218
|
label='Username'
|
|
222
219
|
value={value}
|
|
223
220
|
onChangeText={handleChange}
|
|
224
|
-
|
|
221
|
+
helperText={error || undefined}
|
|
222
|
+
status={error ? 'error' : undefined}
|
|
225
223
|
/>
|
|
226
224
|
);
|
|
227
225
|
}
|
|
228
226
|
```
|
|
229
227
|
|
|
230
|
-
> **Note:**
|
|
228
|
+
> **Note:** Helper text is optional. Use `helperText` with `status="error"` to show validation feedback below the input.
|
|
231
229
|
|
|
232
230
|
### With Custom Styling
|
|
233
231
|
|