@ledgerhq/lumen-ui-rnative 0.1.31 → 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.
Files changed (59) hide show
  1. package/dist/module/lib/Components/AddressInput/AddressInput.mdx +10 -12
  2. package/dist/module/lib/Components/AddressInput/AddressInput.stories.js +2 -1
  3. package/dist/module/lib/Components/AddressInput/AddressInput.stories.js.map +1 -1
  4. package/dist/module/lib/Components/AmountInput/AmountInput.mdx +3 -3
  5. package/dist/module/lib/Components/BaseInput/BaseInput.js +60 -32
  6. package/dist/module/lib/Components/BaseInput/BaseInput.js.map +1 -1
  7. package/dist/module/lib/Components/DotCount/DotCount.stories.js +9 -18
  8. package/dist/module/lib/Components/DotCount/DotCount.stories.js.map +1 -1
  9. package/dist/module/lib/Components/DotIndicator/DotIndicator.stories.js +3 -5
  10. package/dist/module/lib/Components/DotIndicator/DotIndicator.stories.js.map +1 -1
  11. package/dist/module/lib/Components/MediaImage/MediaImage.test.js +7 -3
  12. package/dist/module/lib/Components/MediaImage/MediaImage.test.js.map +1 -1
  13. package/dist/module/lib/Components/SearchInput/SearchInput.mdx +4 -10
  14. package/dist/module/lib/Components/SearchInput/SearchInput.stories.js +2 -1
  15. package/dist/module/lib/Components/SearchInput/SearchInput.stories.js.map +1 -1
  16. package/dist/module/lib/Components/SegmentedControl/SegmentedControl.js +2 -1
  17. package/dist/module/lib/Components/SegmentedControl/SegmentedControl.js.map +1 -1
  18. package/dist/module/lib/Components/SegmentedControl/SegmentedControl.mdx +34 -4
  19. package/dist/module/lib/Components/SegmentedControl/SegmentedControl.stories.js +32 -7
  20. package/dist/module/lib/Components/SegmentedControl/SegmentedControl.stories.js.map +1 -1
  21. package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js +26 -0
  22. package/dist/module/lib/Components/SegmentedControl/SegmentedControl.test.js.map +1 -1
  23. package/dist/module/lib/Components/TextInput/TextInput.js +4 -3
  24. package/dist/module/lib/Components/TextInput/TextInput.js.map +1 -1
  25. package/dist/module/lib/Components/TextInput/TextInput.mdx +8 -10
  26. package/dist/module/lib/Components/TextInput/TextInput.stories.js +40 -1
  27. package/dist/module/lib/Components/TextInput/TextInput.stories.js.map +1 -1
  28. package/dist/module/lib/Components/TextInput/TextInput.test.js +76 -0
  29. package/dist/module/lib/Components/TextInput/TextInput.test.js.map +1 -0
  30. package/dist/typescript/src/lib/Components/BaseInput/BaseInput.d.ts +1 -1
  31. package/dist/typescript/src/lib/Components/BaseInput/BaseInput.d.ts.map +1 -1
  32. package/dist/typescript/src/lib/Components/BaseInput/types.d.ts +9 -2
  33. package/dist/typescript/src/lib/Components/BaseInput/types.d.ts.map +1 -1
  34. package/dist/typescript/src/lib/Components/SegmentedControl/SegmentedControl.d.ts +1 -1
  35. package/dist/typescript/src/lib/Components/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  36. package/dist/typescript/src/lib/Components/SegmentedControl/types.d.ts +4 -0
  37. package/dist/typescript/src/lib/Components/SegmentedControl/types.d.ts.map +1 -1
  38. package/dist/typescript/src/lib/Components/TextInput/TextInput.d.ts +4 -3
  39. package/dist/typescript/src/lib/Components/TextInput/TextInput.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/lib/Components/AddressInput/AddressInput.mdx +10 -12
  42. package/src/lib/Components/AddressInput/AddressInput.stories.tsx +2 -1
  43. package/src/lib/Components/AmountInput/AmountInput.mdx +3 -3
  44. package/src/lib/Components/BaseInput/BaseInput.tsx +61 -29
  45. package/src/lib/Components/BaseInput/types.ts +10 -2
  46. package/src/lib/Components/DotCount/DotCount.stories.tsx +12 -10
  47. package/src/lib/Components/DotIndicator/DotIndicator.stories.tsx +1 -3
  48. package/src/lib/Components/MediaImage/MediaImage.test.tsx +7 -3
  49. package/src/lib/Components/SearchInput/SearchInput.mdx +4 -10
  50. package/src/lib/Components/SearchInput/SearchInput.stories.tsx +2 -1
  51. package/src/lib/Components/SegmentedControl/SegmentedControl.mdx +34 -4
  52. package/src/lib/Components/SegmentedControl/SegmentedControl.stories.tsx +34 -6
  53. package/src/lib/Components/SegmentedControl/SegmentedControl.test.tsx +27 -0
  54. package/src/lib/Components/SegmentedControl/SegmentedControl.tsx +2 -0
  55. package/src/lib/Components/SegmentedControl/types.ts +4 -0
  56. package/src/lib/Components/TextInput/TextInput.mdx +8 -10
  57. package/src/lib/Components/TextInput/TextInput.stories.tsx +41 -1
  58. package/src/lib/Components/TextInput/TextInput.test.tsx +90 -0
  59. package/src/lib/Components/TextInput/TextInput.tsx +4 -3
@@ -53,6 +53,10 @@ export type SegmentedControlButtonProps = {
53
53
  * Optional icon shown to the left of the label (from Symbols).
54
54
  */
55
55
  icon?: IconComponent;
56
+ /**
57
+ * Optional content shown to the right of the label (e.g. DotCount badge).
58
+ */
59
+ trailingContent?: ReactNode;
56
60
  /**
57
61
  * Optional callback when the button is pressed (in addition to onSelectedChange on the parent).
58
62
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/SegmentedControl/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,YAAY,GAAG,eAAe,CAAC;IAC5C;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IAC5B;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AAE/B,KAAK,aAAa,GAAG,aAAa,CAAC;IACjC,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,GAAG,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/SegmentedControl/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,YAAY,GAAG,eAAe,CAAC;IAC5C;;;;;OAKG;IACH,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC;IAC5B;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AAE/B,KAAK,aAAa,GAAG,aAAa,CAAC;IACjC,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;CACjC,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG;IACxC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;OAEG;IACH,eAAe,CAAC,EAAE,SAAS,CAAC;IAC5B;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,GAAG,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC,CAAC"}
@@ -6,7 +6,7 @@ import { type TextInputProps } from './types';
6
6
  * - **Automatic clear button** appears when input has content
7
7
  * - **Floating label** with smooth animations
8
8
  * - **Suffix elements** for icons, buttons, or custom content
9
- * - **Error state styling** with errorMessage support
9
+ * - **Helper text** with optional `status` (`error` | `success`) for border and helper feedback styling
10
10
  * - **Container-based spacing** with padding and gap for clean layout
11
11
  * - **Flexible styling** via style props
12
12
  * - **React Native TextInput** with proper mobile behavior
@@ -26,12 +26,13 @@ import { type TextInputProps } from './types';
26
26
  * // Basic input with automatic clear button
27
27
  * <TextInput label="Title" value={title} onChangeText={setTitle} />
28
28
  *
29
- * // Input with error state
29
+ * // Input with error helper
30
30
  * <TextInput
31
31
  * label="Email"
32
32
  * value={email}
33
33
  * onChangeText={setEmail}
34
- * errorMessage="Please enter a valid email address"
34
+ * helperText="Please enter a valid email address"
35
+ * status="error"
35
36
  * />
36
37
  *
37
38
  * // Input with suffix element
@@ -1 +1 @@
1
- {"version":3,"file":"TextInput.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/TextInput/TextInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,cAAc,4CAE9C,CAAC"}
1
+ {"version":3,"file":"TextInput.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/TextInput/TextInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,cAAc,4CAE9C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-ui-rnative",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "license": "Apache-2.0",
5
5
  "keywords": [
6
6
  "react-native",
@@ -55,16 +55,10 @@ Use `hideClearButton` to prevent the clear button from appearing.
55
55
 
56
56
  ### Error State
57
57
 
58
- The input supports error handling through `errorMessage` which displays an error message below the input with error styling including a red border and text color.
58
+ The input supports error handling through `helperText` and `status` (`'error'` \| `'success'`), which show copy below the input with matching border and text styling.
59
59
 
60
60
  <Canvas of={AddressInputStories.WithError} />
61
61
 
62
- The error message will be automatically:
63
-
64
- - Connected to the input
65
- - Displayed with a warning icon
66
- - Styled in the error color
67
- - Announced by screen readers
68
62
 
69
63
  ### Disabled State
70
64
 
@@ -244,7 +238,8 @@ function MyComponent() {
244
238
  placeholder='Enter address or ENS'
245
239
  value={address}
246
240
  onChangeText={setAddress}
247
- errorMessage={error}
241
+ helperText={error || undefined}
242
+ status={error ? 'error' : undefined}
248
243
  onClear={() => {
249
244
  // Default clearing happens automatically
250
245
  // Add your custom logic here
@@ -289,14 +284,15 @@ function MyComponent() {
289
284
  placeholder='Enter address or ENS'
290
285
  value={address}
291
286
  onChangeText={handleChange}
292
- errorMessage={error}
287
+ helperText={error || undefined}
288
+ status={error ? 'error' : undefined}
293
289
  onQrCodeClick={() => openQrScanner()}
294
290
  />
295
291
  );
296
292
  }
297
293
  ```
298
294
 
299
- > **Note:** Error messages are optional. Use `errorMessage` to display an error message below the input.
295
+ > **Note:** Helper text is optional. Use `helperText` with `status="error"` to show validation feedback below the input.
300
296
 
301
297
  ### With Custom Styling
302
298
 
@@ -387,7 +383,8 @@ function MyComponent() {
387
383
  value={address}
388
384
  onChangeText={setAddress}
389
385
  onQrCodeClick={handleQrScan}
390
- errorMessage={error}
386
+ helperText={error || undefined}
387
+ status={error ? 'error' : undefined}
391
388
  />
392
389
  );
393
390
  }
@@ -412,7 +409,8 @@ function TransactionForm() {
412
409
  placeholder='Enter address or ENS'
413
410
  value={recipientAddress}
414
411
  onChangeText={setRecipientAddress}
415
- errorMessage={addressError}
412
+ helperText={addressError || undefined}
413
+ status={addressError ? 'error' : undefined}
416
414
  onQrCodeClick={() => openQrScanner()}
417
415
  style={{ maxWidth: 320 }}
418
416
  />
@@ -119,7 +119,8 @@ export const WithError: Story = {
119
119
  args: {
120
120
  placeholder: 'Enter address or ENS',
121
121
  value: 'invalid-address',
122
- errorMessage: 'Invalid address format',
122
+ helperText: 'Invalid address format',
123
+ status: 'error',
123
124
  prefix: 'To:',
124
125
  editable: true,
125
126
  hideClearButton: false,
@@ -385,7 +385,7 @@ const [hasError, setHasError] = useState(false);
385
385
  </DoBlockItem>
386
386
  <DontBlockItem
387
387
  title="Don't use custom error props"
388
- description='Use isInvalid prop, not error or errorMessage props'
388
+ description='Use the isInvalid prop for error styling. AmountInput does not accept ad-hoc validation props.'
389
389
  >
390
390
 
391
391
  {/* prettier-ignore */}
@@ -400,8 +400,8 @@ const [hasError, setHasError] = useState(false);
400
400
  value={value}
401
401
  onChangeText={setValue}
402
402
  currencyText='USD'
403
- error={true}
404
- errorMessage='Invalid amount'
403
+ status={hasError ? 'error' : undefined}
404
+ helperText={hasError ? 'Invalid amount' : undefined}
405
405
  />
406
406
  ```
407
407
 
@@ -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
- errorMessage,
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
- hasError: !!errorMessage,
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
- hasError: !!errorMessage,
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
- {errorMessage && (
162
- <View style={styles.errorContainer}>
163
- <DeleteCircleFill size={16} color='error' />
164
- <Text style={styles.errorText}>{errorMessage}</Text>
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
- hasError,
188
+ status,
174
189
  isFocused,
175
190
  isEditable,
176
191
  hasLabel,
177
192
  }: {
178
- hasError: boolean;
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
- hasError && {
202
- borderWidth: 1,
203
- borderColor: t.colors.border.error,
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
- !hasError &&
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.body2,
221
- paddingTop: t.spacings.s12,
222
- paddingBottom: t.spacings.s12,
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 && hasLabel && { lineHeight: 0 },
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
- errorContainer: {
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
- errorText: {
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
- [hasError, isFocused, isEditable, hasLabel],
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
- hasError,
328
+ status,
300
329
  isEditable,
330
+ labelStaysFloatedWithPlaceholder,
301
331
  }: {
302
332
  isFocused: boolean;
303
333
  hasContent: boolean;
304
334
  showClearButton: boolean;
305
- hasError: boolean;
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, hasError, isEditable],
362
+ [hasContent, showClearButton, status, isEditable],
332
363
  );
333
364
 
334
365
  const { animatedStyle } = useAnimatedFloatingLabel({
335
366
  theme,
336
- isFloatingLabel: isFocused || hasContent,
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
- * An optional error message displayed below the input.
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
- errorMessage?: string;
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 lx={{ flexDirection: 'row', alignItems: 'center', gap: 's12' }}>
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 value='preview'>
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
- <DotIndicator appearance='red'>
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
- img.props.onError();
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
- img.props.onError();
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 `errorMessage` which displays an error message below the input with error styling including a red border and text color.
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
- errorMessage={error}
228
+ helperText={error || undefined}
229
+ status={error ? 'error' : undefined}
236
230
  />
237
231
  );
238
232
  }
239
233
  ```
240
234
 
241
- > **Note:** Error messages are optional. Use `errorMessage` to display an error message below the search input.
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
 
@@ -75,7 +75,8 @@ export const WithError: Story = {
75
75
  ),
76
76
  args: {
77
77
  placeholder: 'Search products',
78
- errorMessage: 'Search term is invalid',
78
+ helperText: 'Search term is invalid',
79
+ status: 'error',
79
80
  editable: true,
80
81
  hideClearButton: false,
81
82
  },
@@ -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>