jfs-components 0.0.78 → 0.0.84

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 (119) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/commonjs/components/AppBar/AppBar.js +56 -6
  3. package/lib/commonjs/components/Attached/Attached.js +183 -0
  4. package/lib/commonjs/components/Card/Card.js +25 -2
  5. package/lib/commonjs/components/Checkbox/Checkbox.js +18 -2
  6. package/lib/commonjs/components/Drawer/Drawer.js +6 -1
  7. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -6
  8. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  9. package/lib/commonjs/components/FormField/FormField.js +1 -14
  10. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +9 -7
  11. package/lib/commonjs/components/ListItem/ListItem.js +26 -24
  12. package/lib/commonjs/components/MessageField/MessageField.js +1 -13
  13. package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +12 -9
  14. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +237 -0
  15. package/lib/commonjs/components/Slot/Slot.js +73 -0
  16. package/lib/commonjs/components/Spinner/Spinner.js +217 -0
  17. package/lib/commonjs/components/TextInput/TextInput.js +33 -18
  18. package/lib/commonjs/components/index.js +28 -0
  19. package/lib/commonjs/icons/components/IconArrowdown.js +19 -0
  20. package/lib/commonjs/icons/components/IconArrowup.js +19 -0
  21. package/lib/commonjs/icons/components/IconChevrondowncircle.js +19 -0
  22. package/lib/commonjs/icons/components/IconChevronleftcircle.js +19 -0
  23. package/lib/commonjs/icons/components/IconChevronrightcircle.js +19 -0
  24. package/lib/commonjs/icons/components/IconChevronupcircle.js +19 -0
  25. package/lib/commonjs/icons/components/IconOsnavback.js +19 -0
  26. package/lib/commonjs/icons/components/IconOsnavcenter.js +19 -0
  27. package/lib/commonjs/icons/components/IconOsnavhome.js +19 -0
  28. package/lib/commonjs/icons/components/IconOsnavtask.js +19 -0
  29. package/lib/commonjs/icons/components/IconSignin.js +19 -0
  30. package/lib/commonjs/icons/components/IconSignout.js +19 -0
  31. package/lib/commonjs/icons/components/index.js +132 -0
  32. package/lib/commonjs/icons/registry.js +2 -2
  33. package/lib/module/components/AppBar/AppBar.js +56 -6
  34. package/lib/module/components/Attached/Attached.js +178 -0
  35. package/lib/module/components/Card/Card.js +25 -2
  36. package/lib/module/components/Checkbox/Checkbox.js +18 -2
  37. package/lib/module/components/Drawer/Drawer.js +6 -1
  38. package/lib/module/components/DropdownInput/DropdownInput.js +30 -6
  39. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  40. package/lib/module/components/FormField/FormField.js +3 -16
  41. package/lib/module/components/FullscreenModal/FullscreenModal.js +9 -7
  42. package/lib/module/components/ListItem/ListItem.js +26 -24
  43. package/lib/module/components/MessageField/MessageField.js +3 -15
  44. package/lib/module/components/PaymentFeedback/PaymentFeedback.js +13 -9
  45. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +234 -0
  46. package/lib/module/components/Slot/Slot.js +68 -0
  47. package/lib/module/components/Spinner/Spinner.js +212 -0
  48. package/lib/module/components/TextInput/TextInput.js +34 -19
  49. package/lib/module/components/index.js +4 -0
  50. package/lib/module/icons/components/IconArrowdown.js +12 -0
  51. package/lib/module/icons/components/IconArrowup.js +12 -0
  52. package/lib/module/icons/components/IconChevrondowncircle.js +12 -0
  53. package/lib/module/icons/components/IconChevronleftcircle.js +12 -0
  54. package/lib/module/icons/components/IconChevronrightcircle.js +12 -0
  55. package/lib/module/icons/components/IconChevronupcircle.js +12 -0
  56. package/lib/module/icons/components/IconOsnavback.js +12 -0
  57. package/lib/module/icons/components/IconOsnavcenter.js +12 -0
  58. package/lib/module/icons/components/IconOsnavhome.js +12 -0
  59. package/lib/module/icons/components/IconOsnavtask.js +12 -0
  60. package/lib/module/icons/components/IconSignin.js +12 -0
  61. package/lib/module/icons/components/IconSignout.js +12 -0
  62. package/lib/module/icons/components/index.js +12 -0
  63. package/lib/module/icons/registry.js +2 -2
  64. package/lib/typescript/src/components/AppBar/AppBar.d.ts +12 -1
  65. package/lib/typescript/src/components/Attached/Attached.d.ts +64 -0
  66. package/lib/typescript/src/components/Card/Card.d.ts +9 -2
  67. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
  68. package/lib/typescript/src/components/ListItem/ListItem.d.ts +16 -6
  69. package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
  70. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +66 -0
  71. package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
  72. package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
  73. package/lib/typescript/src/components/index.d.ts +4 -0
  74. package/lib/typescript/src/icons/components/IconArrowdown.d.ts +3 -0
  75. package/lib/typescript/src/icons/components/IconArrowup.d.ts +3 -0
  76. package/lib/typescript/src/icons/components/IconChevrondowncircle.d.ts +3 -0
  77. package/lib/typescript/src/icons/components/IconChevronleftcircle.d.ts +3 -0
  78. package/lib/typescript/src/icons/components/IconChevronrightcircle.d.ts +3 -0
  79. package/lib/typescript/src/icons/components/IconChevronupcircle.d.ts +3 -0
  80. package/lib/typescript/src/icons/components/IconOsnavback.d.ts +3 -0
  81. package/lib/typescript/src/icons/components/IconOsnavcenter.d.ts +3 -0
  82. package/lib/typescript/src/icons/components/IconOsnavhome.d.ts +3 -0
  83. package/lib/typescript/src/icons/components/IconOsnavtask.d.ts +3 -0
  84. package/lib/typescript/src/icons/components/IconSignin.d.ts +3 -0
  85. package/lib/typescript/src/icons/components/IconSignout.d.ts +3 -0
  86. package/lib/typescript/src/icons/components/index.d.ts +12 -0
  87. package/lib/typescript/src/icons/registry.d.ts +1 -1
  88. package/package.json +3 -2
  89. package/src/components/AppBar/AppBar.tsx +79 -12
  90. package/src/components/Attached/Attached.tsx +237 -0
  91. package/src/components/Card/Card.tsx +28 -1
  92. package/src/components/Checkbox/Checkbox.tsx +14 -2
  93. package/src/components/Drawer/Drawer.tsx +4 -0
  94. package/src/components/DropdownInput/DropdownInput.tsx +54 -20
  95. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +13 -9
  96. package/src/components/FormField/FormField.tsx +3 -19
  97. package/src/components/FullscreenModal/FullscreenModal.tsx +6 -3
  98. package/src/components/ListItem/ListItem.tsx +42 -25
  99. package/src/components/MessageField/MessageField.tsx +3 -18
  100. package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
  101. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +316 -0
  102. package/src/components/Slot/Slot.tsx +91 -0
  103. package/src/components/Spinner/Spinner.tsx +273 -0
  104. package/src/components/TextInput/TextInput.tsx +37 -19
  105. package/src/components/index.ts +4 -0
  106. package/src/icons/components/IconArrowdown.tsx +11 -0
  107. package/src/icons/components/IconArrowup.tsx +11 -0
  108. package/src/icons/components/IconChevrondowncircle.tsx +11 -0
  109. package/src/icons/components/IconChevronleftcircle.tsx +11 -0
  110. package/src/icons/components/IconChevronrightcircle.tsx +11 -0
  111. package/src/icons/components/IconChevronupcircle.tsx +11 -0
  112. package/src/icons/components/IconOsnavback.tsx +11 -0
  113. package/src/icons/components/IconOsnavcenter.tsx +11 -0
  114. package/src/icons/components/IconOsnavhome.tsx +11 -0
  115. package/src/icons/components/IconOsnavtask.tsx +11 -0
  116. package/src/icons/components/IconSignin.tsx +11 -0
  117. package/src/icons/components/IconSignout.tsx +11 -0
  118. package/src/icons/components/index.ts +12 -0
  119. package/src/icons/registry.ts +49 -1
@@ -129,10 +129,6 @@ function ExpandableCheckbox({
129
129
 
130
130
  const rowGap =
131
131
  (getVariableByName('checkboxItem/gap', modes) as number | null) ?? 8
132
- const rowPaddingHorizontal =
133
- (getVariableByName('checkboxItem/padding/horizontal', modes) as number | null) ?? 0
134
- const rowPaddingVertical =
135
- (getVariableByName('checkboxItem/padding/vertical', modes) as number | null) ?? 0
136
132
 
137
133
  const labelColor =
138
134
  (getVariableByName('checkboxItem/foreground', modes) as string | null) ?? '#1a1c1f'
@@ -163,12 +159,10 @@ function ExpandableCheckbox({
163
159
  alignSelf: isExpanded ? 'stretch' : 'auto',
164
160
  minWidth: 0,
165
161
  flexDirection: 'row',
166
- alignItems: 'flex-start',
162
+ alignItems: isExpanded ? 'flex-start' : 'center',
167
163
  gap: rowGap,
168
- paddingHorizontal: rowPaddingHorizontal,
169
- paddingVertical: rowPaddingVertical,
170
164
  }),
171
- [isExpanded, rowGap, rowPaddingHorizontal, rowPaddingVertical]
165
+ [isExpanded, rowGap]
172
166
  )
173
167
 
174
168
  const resolvedLabelStyle: TextStyle = useMemo(
@@ -180,6 +174,13 @@ function ExpandableCheckbox({
180
174
  fontSize: labelFontSize,
181
175
  lineHeight: labelLineHeight,
182
176
  fontWeight: labelFontWeight,
177
+ // Android adds asymmetric font padding and top-aligns the glyph inside
178
+ // an inflated line box when `lineHeight` is set. That makes the centered
179
+ // checkbox look like it drops below the text. Disabling the extra
180
+ // padding + centering the glyph keeps the single-line label optically
181
+ // aligned with the checkbox. No-op on iOS / web.
182
+ includeFontPadding: false,
183
+ textAlignVertical: isExpanded ? 'top' : 'center',
183
184
  }),
184
185
  [
185
186
  labelColor,
@@ -187,11 +188,14 @@ function ExpandableCheckbox({
187
188
  labelFontSize,
188
189
  labelLineHeight,
189
190
  labelFontWeight,
191
+ isExpanded,
190
192
  ]
191
193
  )
192
194
 
195
+ // Layer component modes first (e.g. Color Mode), then button defaults so
196
+ // Secondary / XS / Low always win unless a dedicated override prop is added.
193
197
  const buttonModes = useMemo(
194
- () => ({ ...BUTTON_DEFAULT_MODES, ...modes }),
198
+ () => ({ ...modes, ...BUTTON_DEFAULT_MODES }),
195
199
  [modes]
196
200
  )
197
201
 
@@ -1,8 +1,7 @@
1
- import React, { useCallback, useMemo, useRef, useState } from 'react'
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
2
  import {
3
3
  View,
4
4
  Text,
5
- Pressable,
6
5
  TextInput as RNTextInput,
7
6
  type StyleProp,
8
7
  type TextInputProps as RNTextInputProps,
@@ -347,16 +346,6 @@ function FormField({
347
346
  const [isFocused, setIsFocused] = useState(false)
348
347
  const interactive = !isDisabled && !isReadOnly
349
348
 
350
- // Ref to the native input so tapping anywhere in the input row (padding,
351
- // leading/trailing gutters) focuses it on the FIRST tap — fixing the Android
352
- // "two taps to open the keyboard" issue caused by the row intercepting the
353
- // initial touch.
354
- const inputRef = useRef<RNTextInput>(null)
355
- const focusInput = useCallback(() => {
356
- if (!interactive) return
357
- inputRef.current?.focus()
358
- }, [interactive])
359
-
360
349
  // FormField States cascade — error > read only/disabled > active (focused) > idle.
361
350
  // Disabled maps to "Read Only" since there is no dedicated disabled mode and
362
351
  // the visual treatment is closest. This is only the DEFAULT — an explicit
@@ -552,11 +541,7 @@ function FormField({
552
541
  </View>
553
542
  )}
554
543
 
555
- <Pressable
556
- style={[inputRowStyle, inputStyle]}
557
- onPress={focusInput}
558
- accessible={false}
559
- >
544
+ <View style={[inputRowStyle, inputStyle]}>
560
545
  {processedLeading != null && (
561
546
  <View
562
547
  accessibilityElementsHidden
@@ -566,7 +551,6 @@ function FormField({
566
551
  </View>
567
552
  )}
568
553
  <RNTextInput
569
- ref={inputRef}
570
554
  style={[inputTextStyles, inputTextStyle]}
571
555
  value={value ?? ''}
572
556
  onChangeText={handleChangeText}
@@ -594,7 +578,7 @@ function FormField({
594
578
  {processedTrailing}
595
579
  </View>
596
580
  )}
597
- </Pressable>
581
+ </View>
598
582
 
599
583
  {supportLabel != null && supportLabel !== '' && (
600
584
  <SupportText
@@ -21,6 +21,7 @@ import Button from '../Button/Button'
21
21
  import Disclaimer from '../Disclaimer/Disclaimer'
22
22
  import IconButton from '../IconButton/IconButton'
23
23
  import ActionFooter from '../ActionFooter/ActionFooter'
24
+ import Slot from '../Slot/Slot'
24
25
 
25
26
  // ---------------------------------------------------------------------------
26
27
  // Forced modes
@@ -351,7 +352,7 @@ function FullscreenModal({
351
352
  footerContent = footer
352
353
  } else if (primaryActionLabel) {
353
354
  footerContent = (
354
- <View style={footerColumnStyle}>
355
+ <Slot layoutDirection="vertical" modes={modes}>
355
356
  <Button
356
357
  label={primaryActionLabel}
357
358
  modes={modes}
@@ -359,7 +360,7 @@ function FullscreenModal({
359
360
  {...(onPrimaryAction ? { onPress: onPrimaryAction } : {})}
360
361
  />
361
362
  {disclaimer ? <Disclaimer disclaimer={disclaimer} modes={modes} /> : null}
362
- </View>
363
+ </Slot>
363
364
  )
364
365
  }
365
366
 
@@ -373,6 +374,9 @@ function FullscreenModal({
373
374
  showsVerticalScrollIndicator={false}
374
375
  onScroll={onScroll}
375
376
  scrollEventThrottle={16}
377
+ // Tap an input in the body and it focuses on the FIRST tap, even when
378
+ // the keyboard is already open (default 'never' eats that tap).
379
+ keyboardShouldPersistTaps="handled"
376
380
  >
377
381
  <View style={heroTextRegionStyle}>
378
382
  <HeroText
@@ -407,7 +411,6 @@ function FullscreenModal({
407
411
  const rootStyle: ViewStyle = { flex: 1, width: '100%', position: 'relative' }
408
412
  const scrollViewStyle: ViewStyle = { flex: 1 }
409
413
  const scrollContentStyle: ViewStyle = { flexGrow: 1 }
410
- const footerColumnStyle: ViewStyle = { width: '100%', gap: 8 }
411
414
  const fullWidthStyle: ViewStyle = { width: '100%' }
412
415
  const closeButtonStyle: ViewStyle = { position: 'absolute', top: 12, right: 12 }
413
416
 
@@ -11,7 +11,6 @@ import {
11
11
  type PressableStateCallbackType,
12
12
  } from 'react-native'
13
13
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
14
- import IconCapsule from '../IconCapsule/IconCapsule'
15
14
  import NavArrow from '../NavArrow/NavArrow'
16
15
  import { usePressableWebSupport, type WebAccessibilityProps } from '../../utils/web-platform-utils'
17
16
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
@@ -21,8 +20,16 @@ type ListItemProps = {
21
20
  title?: string;
22
21
  supportText?: string;
23
22
  showSupportText?: boolean;
24
- leading?: React.ReactNode;
23
+ /** Leading slot (Figma "leading"). Omitted or `null` renders nothing. */
24
+ leading?: React.ReactNode | null;
25
25
  supportSlot?: React.ReactNode;
26
+ /** Trailing slot (Figma "trailing"), e.g. `MoneyValue` or `Button`. Horizontal layout only. */
27
+ trailing?: React.ReactNode;
28
+ /**
29
+ * @deprecated Renamed to `trailing` for a symmetric `leading` / `trailing`
30
+ * slot API. Still honored for backward compatibility; `trailing` wins when
31
+ * both are provided. Will be removed in a future major version.
32
+ */
26
33
  endSlot?: React.ReactNode;
27
34
  /** Whether to show the NavArrow on the far right (Horizontal layout only). Defaults to true. */
28
35
  navArrow?: boolean;
@@ -46,9 +53,10 @@ type ListItemProps = {
46
53
  const IS_IOS = Platform.OS === 'ios'
47
54
  const PRESS_DELAY = IS_IOS ? 130 : 0
48
55
 
49
- // Forced modes for the endSlot — `Context: 'ListItem'` can never be
50
- // overridden by external modes. Frozen so identity is stable across renders.
51
- const END_SLOT_FORCED_MODES = Object.freeze({ Context: 'ListItem' })
56
+ // Forced modes for the leading/trailing slots — `Context: 'ListItem'` can
57
+ // never be overridden by external modes. Frozen so identity is stable across
58
+ // renders. Applied to both slots so they cascade modes identically.
59
+ const SLOT_FORCED_MODES = Object.freeze({ Context: 'ListItem' })
52
60
 
53
61
  // Pressed visual is applied on the host view through Pressable's style
54
62
  // callback, so a scroll-cancelled touch never schedules a React render.
@@ -72,7 +80,7 @@ interface ListItemTokens {
72
80
  }
73
81
 
74
82
  function resolveListItemTokens(modes: Record<string, any>): ListItemTokens {
75
- // Modes used to cascade into slot children (leading / supportSlot / endSlot).
83
+ // Modes used to cascade into slot children (leading / supportSlot / trailing).
76
84
  // We do NOT inject an `AppearanceBrand` default here: slot content such as
77
85
  // Buttons or Badges carry their own intended appearance, so forcing one onto
78
86
  // them would be surprising.
@@ -167,9 +175,11 @@ const verticalSupportTextOverride: TextStyle = { textAlign: 'center' }
167
175
  * - **design-token driven styling** via `getVariableByName` and `modes`
168
176
  *
169
177
  * Wherever the Figma layer name contains "Slot", this component exposes a
170
- * dedicated React "slot" prop:
178
+ * dedicated React "slot" prop. The leading and trailing edges share a
179
+ * symmetric `leading` / `trailing` slot API:
180
+ * - Slot "leading" → `leading`
171
181
  * - Slot "support text" → `supportSlot`
172
- * - Slot "end" → `endSlot`
182
+ * - Slot "trailing" → `trailing`
173
183
  *
174
184
  * @component
175
185
  * @param {Object} props
@@ -177,9 +187,9 @@ const verticalSupportTextOverride: TextStyle = { textAlign: 'center' }
177
187
  * @param {string} [props.title='Title'] - Primary title used in the horizontal layout.
178
188
  * @param {string} [props.supportText='Support Text'] - Support text used in both layouts when `supportSlot` is not provided.
179
189
  * @param {boolean} [props.showSupportText=true] - Toggles rendering of the support text in Horizontal layout.
180
- * @param {React.ReactNode} [props.leading] - Optional leading element. Defaults to `IconCapsule`.
190
+ * @param {React.ReactNode|null} [props.leading] - Optional leading slot. Omitted or `null` renders nothing.
181
191
  * @param {React.ReactNode} [props.supportSlot] - Optional custom slot used instead of the default support text block.
182
- * @param {React.ReactNode} [props.endSlot] - Optional custom trailing slot (Figma Slot "end").
192
+ * @param {React.ReactNode} [props.trailing] - Optional trailing slot (Figma Slot "trailing"). Horizontal layout only.
183
193
  * @param {boolean} [props.navArrow=true] - Whether to show NavArrow on the far right (Horizontal layout only).
184
194
  * @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens.
185
195
  * @param {Function} [props.onPress] - When provided, the entire item becomes pressable (navigation variant).
@@ -208,6 +218,7 @@ function ListItemImpl({
208
218
  showSupportText = true,
209
219
  leading,
210
220
  supportSlot,
221
+ trailing,
211
222
  endSlot,
212
223
  navArrow = true,
213
224
  modes = EMPTY_MODES,
@@ -251,12 +262,15 @@ function ListItemImpl({
251
262
  // Process leading slot to pass modes to children. Memoized on
252
263
  // (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
253
264
  const leadingElement = useMemo(() => {
254
- const processed = leading
255
- ? cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes)
256
- : []
257
- if (processed.length === 0) {
258
- return <IconCapsule modes={tokens.resolvedModes} accessibilityLabel={undefined} />
259
- }
265
+ if (leading == null) return null
266
+
267
+ const processed = cloneChildrenWithModes(
268
+ React.Children.toArray(leading),
269
+ tokens.resolvedModes,
270
+ SLOT_FORCED_MODES
271
+ )
272
+ if (processed.length === 0) return null
273
+
260
274
  return processed.length === 1 ? processed[0] : processed
261
275
  }, [leading, tokens.resolvedModes])
262
276
 
@@ -269,15 +283,18 @@ function ListItemImpl({
269
283
  return processed.length === 1 ? processed[0] : processed
270
284
  }, [supportSlot, tokens.resolvedModes])
271
285
 
272
- const processedEndSlot = useMemo(() => {
273
- if (!endSlot) return null
286
+ // `trailing` wins; `endSlot` is the deprecated alias kept for back-compat.
287
+ const trailingContent = trailing ?? endSlot
288
+
289
+ const processedTrailing = useMemo(() => {
290
+ if (!trailingContent) return null
274
291
  const processed = cloneChildrenWithModes(
275
- React.Children.toArray(endSlot),
292
+ React.Children.toArray(trailingContent),
276
293
  tokens.resolvedModes,
277
- END_SLOT_FORCED_MODES
294
+ SLOT_FORCED_MODES
278
295
  )
279
296
  return processed.length === 1 ? processed[0] : processed
280
- }, [endSlot, tokens.resolvedModes])
297
+ }, [trailingContent, tokens.resolvedModes])
281
298
 
282
299
  const renderSupportContent = () => {
283
300
  if (processedSupportSlot) return processedSupportSlot
@@ -354,7 +371,7 @@ function ListItemImpl({
354
371
  if (layout === 'Horizontal') {
355
372
  const innerContent = (
356
373
  <View style={innerContentStyleArray}>
357
- {leadingElement}
374
+ {leadingElement ?? null}
358
375
  <View
359
376
  style={{
360
377
  flex: 1,
@@ -370,8 +387,8 @@ function ListItemImpl({
370
387
  </Text>
371
388
  {showSupportText && renderSupportContent()}
372
389
  </View>
373
- {processedEndSlot ? (
374
- <View style={tokens.trailingWrapperStyle}>{processedEndSlot}</View>
390
+ {processedTrailing ? (
391
+ <View style={tokens.trailingWrapperStyle}>{processedTrailing}</View>
375
392
  ) : null}
376
393
  {navArrow && <NavArrow direction="Forward" modes={tokens.resolvedModes} />}
377
394
  </View>
@@ -412,7 +429,7 @@ function ListItemImpl({
412
429
  // Vertical layout — icon on top, support text/slot below
413
430
  const verticalContent = (
414
431
  <View style={verticalContentStyleArray}>
415
- {leadingElement}
432
+ {leadingElement ?? null}
416
433
  {renderSupportContent()}
417
434
  </View>
418
435
  )
@@ -1,8 +1,7 @@
1
- import React, { useCallback, useMemo, useRef, useState } from 'react'
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
2
  import {
3
3
  View,
4
4
  Text,
5
- Pressable,
6
5
  TextInput as RNTextInput,
7
6
  type StyleProp,
8
7
  type TextInputProps as RNTextInputProps,
@@ -297,15 +296,6 @@ function MessageField({
297
296
  const [isFocused, setIsFocused] = useState(false)
298
297
  const interactive = !isDisabled && !isReadOnly
299
298
 
300
- // Ref to the native textarea so tapping anywhere in the (padded) textarea
301
- // container focuses it on the FIRST tap, fixing the Android "two taps to
302
- // open the keyboard" issue.
303
- const inputRef = useRef<RNTextInput>(null)
304
- const focusInput = useCallback(() => {
305
- if (!interactive) return
306
- inputRef.current?.focus()
307
- }, [interactive])
308
-
309
299
  const { modes: globalModes } = useTokens()
310
300
  const baseModes = useMemo(
311
301
  () => ({ ...globalModes, ...propModes }),
@@ -508,13 +498,8 @@ function MessageField({
508
498
  </View>
509
499
  )}
510
500
 
511
- <Pressable
512
- style={[textareaContainerStyle, textareaStyle]}
513
- onPress={focusInput}
514
- accessible={false}
515
- >
501
+ <View style={[textareaContainerStyle, textareaStyle]}>
516
502
  <RNTextInput
517
- ref={inputRef}
518
503
  multiline
519
504
  value={currentValue}
520
505
  onChangeText={handleChangeText}
@@ -529,7 +514,7 @@ function MessageField({
529
514
  accessibilityHint={accessibilityHint}
530
515
  style={[inputTextStyle, inputStyle]}
531
516
  />
532
- </Pressable>
517
+ </View>
533
518
 
534
519
  {shouldShowCounter && (
535
520
  <Text style={counterTextStyle} accessibilityElementsHidden>
@@ -1,9 +1,9 @@
1
- import React, { type ReactNode, isValidElement, cloneElement } from 'react'
1
+ import React, { type ReactNode } from 'react'
2
2
  import { View, Text, type ViewStyle, type TextStyle } from 'react-native'
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import { useTokens } from '../../design-tokens/JFSThemeProvider'
5
5
  import IconCapsule from '../IconCapsule/IconCapsule'
6
- import { EMPTY_MODES } from '../../utils/react-utils'
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
7
7
 
8
8
  export type PaymentFeedbackProps = {
9
9
  /** Large heading text, typically a monetary value (e.g. "₹50,000") */
@@ -20,7 +20,11 @@ export type PaymentFeedbackProps = {
20
20
  iconName?: string
21
21
  /** Optional custom media slot that replaces the default IconCapsule */
22
22
  renderMedia?: ReactNode
23
- /** Mode configuration for design tokens */
23
+ /**
24
+ * Mode configuration for design tokens. Also drives the default
25
+ * IconCapsule's color — pass `AppearanceSystem: 'positive' | 'warning' |
26
+ * 'negative'` to render a green/orange/red capsule (defaults to `positive`).
27
+ */
24
28
  modes?: Record<string, any>
25
29
  style?: ViewStyle
26
30
  }
@@ -28,7 +32,7 @@ export type PaymentFeedbackProps = {
28
32
  export default function PaymentFeedback({
29
33
  title = '₹50,000',
30
34
  subtitle = 'Payment successful',
31
- body = 'Your payment has been\nsuccessfully processed.',
35
+ body,
32
36
  details = '18 March 2025, 4:15 pm\nTransaction ID: TXN121466784',
33
37
  showDetails = true,
34
38
  iconName = 'ic_confirm',
@@ -123,14 +127,17 @@ export default function PaymentFeedback({
123
127
  textAlign: 'center',
124
128
  }
125
129
 
126
- const mediaContent = isValidElement(renderMedia)
127
- ? cloneElement(renderMedia as React.ReactElement<any>, { modes })
128
- : renderMedia
130
+ // Cascade modes into a custom media slot (per the modes-cascade convention);
131
+ // any modes the consumer set on the slot child still take precedence.
132
+ const mediaContent =
133
+ renderMedia != null ? cloneChildrenWithModes(renderMedia, modes) : null
129
134
 
130
135
  const defaultMedia = (
131
136
  <IconCapsule
132
137
  iconName={iconName}
133
- modes={{ ...modes, 'Icon Capsule Size': 'L', Emphasis: 'High', 'Semantic Intent': 'System', AppearanceSystem: 'positive' }}
138
+ // `positive` is the default; consumers override the capsule color by
139
+ // passing `AppearanceSystem` (or any other mode) via the `modes` prop.
140
+ modes={{ AppearanceSystem: 'positive', ...modes, 'Icon Capsule Size': 'L', Emphasis: 'High', 'Semantic Intent': 'System' }}
134
141
  />
135
142
  )
136
143