jfs-components 0.1.51 → 0.1.54

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 (90) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/commonjs/components/ActionFooter/ActionFooter.js +319 -135
  3. package/lib/commonjs/components/Badge/Badge.js +20 -1
  4. package/lib/commonjs/components/Button/Button.js +139 -47
  5. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
  6. package/lib/commonjs/components/Card/Card.js +151 -112
  7. package/lib/commonjs/components/Card/assets/gold-logo-mark.png +0 -0
  8. package/lib/commonjs/components/CategoryCard/CategoryCard.js +97 -63
  9. package/lib/commonjs/components/CategoryCard/assets/sample.png +0 -0
  10. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +59 -10
  11. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +132 -49
  12. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +56 -14
  13. package/lib/commonjs/components/IconCapsule/IconCapsule.js +1 -1
  14. package/lib/commonjs/components/MetricData/MetricData.js +26 -13
  15. package/lib/commonjs/components/MoneyValue/MoneyValue.js +1 -1
  16. package/lib/commonjs/components/Nudge/Nudge.js +1 -1
  17. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +47 -91
  18. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  19. package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +1 -1
  20. package/lib/commonjs/components/Slider/Slider.js +3 -3
  21. package/lib/commonjs/components/TextInput/TextInput.js +26 -8
  22. package/lib/commonjs/components/Tooltip/Tooltip.js +3 -5
  23. package/lib/commonjs/components/index.js +7 -1
  24. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  25. package/lib/commonjs/design-tokens/figma-modes.generated.js +13 -13
  26. package/lib/commonjs/icons/registry.js +1 -1
  27. package/lib/module/components/ActionFooter/ActionFooter.js +322 -138
  28. package/lib/module/components/Badge/Badge.js +20 -1
  29. package/lib/module/components/Button/Button.js +140 -48
  30. package/lib/module/components/ButtonGroup/ButtonGroup.js +1 -1
  31. package/lib/module/components/Card/Card.js +152 -114
  32. package/lib/module/components/Card/assets/gold-logo-mark.png +0 -0
  33. package/lib/module/components/CategoryCard/CategoryCard.js +97 -63
  34. package/lib/module/components/CategoryCard/assets/sample.png +0 -0
  35. package/lib/module/components/CheckboxItem/CheckboxItem.js +59 -10
  36. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +135 -52
  37. package/lib/module/components/HelloJioInput/HelloJioInput.js +56 -14
  38. package/lib/module/components/IconCapsule/IconCapsule.js +1 -1
  39. package/lib/module/components/MetricData/MetricData.js +26 -14
  40. package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
  41. package/lib/module/components/Nudge/Nudge.js +1 -1
  42. package/lib/module/components/PdpCcCard/PdpCcCard.js +48 -92
  43. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  44. package/lib/module/components/PortfolioHero/PortfolioHero.js +1 -1
  45. package/lib/module/components/Slider/Slider.js +3 -3
  46. package/lib/module/components/TextInput/TextInput.js +27 -9
  47. package/lib/module/components/Tooltip/Tooltip.js +3 -5
  48. package/lib/module/components/index.js +1 -1
  49. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  50. package/lib/module/design-tokens/figma-modes.generated.js +13 -13
  51. package/lib/module/icons/registry.js +1 -1
  52. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +50 -21
  53. package/lib/typescript/src/components/Button/Button.d.ts +24 -1
  54. package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +2 -2
  55. package/lib/typescript/src/components/Card/Card.d.ts +54 -25
  56. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +29 -12
  57. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +27 -9
  58. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +37 -12
  59. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +1 -1
  60. package/lib/typescript/src/components/MetricData/MetricData.d.ts +7 -5
  61. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +16 -6
  62. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +9 -2
  63. package/lib/typescript/src/components/index.d.ts +2 -2
  64. package/lib/typescript/src/icons/registry.d.ts +1 -1
  65. package/package.json +1 -1
  66. package/src/components/ActionFooter/ActionFooter.tsx +375 -141
  67. package/src/components/Badge/Badge.tsx +21 -1
  68. package/src/components/Button/Button.tsx +192 -44
  69. package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
  70. package/src/components/Card/Card.tsx +370 -212
  71. package/src/components/Card/assets/gold-logo-mark.png +0 -0
  72. package/src/components/CategoryCard/CategoryCard.tsx +130 -84
  73. package/src/components/CategoryCard/assets/sample.png +0 -0
  74. package/src/components/CheckboxItem/CheckboxItem.tsx +72 -12
  75. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +186 -67
  76. package/src/components/HelloJioInput/HelloJioInput.tsx +58 -17
  77. package/src/components/IconCapsule/IconCapsule.tsx +1 -1
  78. package/src/components/MetricData/MetricData.tsx +39 -22
  79. package/src/components/MoneyValue/MoneyValue.tsx +1 -1
  80. package/src/components/Nudge/Nudge.tsx +1 -1
  81. package/src/components/PdpCcCard/PdpCcCard.tsx +41 -92
  82. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +73 -58
  83. package/src/components/PortfolioHero/PortfolioHero.tsx +1 -1
  84. package/src/components/Slider/Slider.tsx +3 -3
  85. package/src/components/TextInput/TextInput.tsx +26 -9
  86. package/src/components/Tooltip/Tooltip.tsx +3 -5
  87. package/src/components/index.ts +9 -2
  88. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  89. package/src/design-tokens/figma-modes.generated.ts +13 -13
  90. package/src/icons/registry.ts +1 -1
@@ -1,20 +1,41 @@
1
1
  import React, { useCallback, useMemo, useState } from 'react'
2
2
  import {
3
3
  View,
4
- Text,
5
4
  type StyleProp,
6
5
  type ViewStyle,
7
6
  type TextStyle,
7
+ type GestureResponderEvent,
8
8
  } from 'react-native'
9
9
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
10
- import { EMPTY_MODES } from '../../utils/react-utils'
11
- import Checkbox from '../Checkbox/Checkbox'
10
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
11
+ import CheckboxItem from '../CheckboxItem/CheckboxItem'
12
12
  import Button from '../Button/Button'
13
+ import Text from '../Text/Text'
14
+ import Link from '../Link/Link'
15
+ import ScrollArea from '../ScrollArea/ScrollArea'
13
16
  import type { Modes } from '../../design-tokens'
14
17
 
15
18
  export type ExpandableCheckboxProps = {
16
- /** Long text label rendered next to the checkbox. */
19
+ /**
20
+ * Primary label rendered next to the checkbox (Figma `Text` instance).
21
+ * Ignored when {@link children} is provided.
22
+ */
17
23
  label?: string
24
+ /**
25
+ * Optional underlined link rendered after {@link label} (Figma `Link`
26
+ * instance, e.g. "Terms & Conditions"). Ignored when {@link children} is
27
+ * provided.
28
+ */
29
+ linkLabel?: string
30
+ /** Press handler for {@link linkLabel}. Navigation is left to the consumer. */
31
+ onLinkPress?: (event: GestureResponderEvent) => void
32
+ /**
33
+ * Full label-slot override matching the Figma Checkbox Item `slot`. When
34
+ * provided, {@link label} / {@link linkLabel} are ignored and `modes` are
35
+ * cascaded into the slot children. Prefer this for fully custom label
36
+ * compositions.
37
+ */
38
+ children?: React.ReactNode
18
39
  /** Whether the checkbox is checked (controlled). */
19
40
  checked?: boolean
20
41
  /** Initial checked state (uncontrolled). */
@@ -45,7 +66,10 @@ export type ExpandableCheckboxProps = {
45
66
  modes?: Modes
46
67
  /** Override outer container styles. */
47
68
  style?: StyleProp<ViewStyle>
48
- /** Override the label text styles. */
69
+ /**
70
+ * Override styles applied to the composed {@link label} `Text`. No-op when
71
+ * {@link children} is provided.
72
+ */
49
73
  labelStyle?: StyleProp<TextStyle>
50
74
  /** Accessibility label for the checkbox. Falls back to `label`. */
51
75
  accessibilityLabel?: string
@@ -63,15 +87,21 @@ const BUTTON_DEFAULT_MODES = {
63
87
  Emphasis: 'Low',
64
88
  } as const
65
89
 
90
+ /** Figma Idle label-slot itemSpacing. */
91
+ const IDLE_SLOT_GAP = 4
92
+ /** Figma Open scroll-slot itemSpacing. */
93
+ const OPEN_SLOT_GAP = 2
94
+
66
95
  /**
67
- * ExpandableCheckbox composes a `Checkbox`, a long-form label and a
68
- * "Read more" / "Read less" toggle. Mirrors the Figma "Expandable Checkbox"
69
- * component with two states:
96
+ * ExpandableCheckbox composes a `CheckboxItem` (checkbox + label slot), optional
97
+ * `Text`/`Link` label content, and a "Read more" / "Read less" toggle. Mirrors
98
+ * the Figma "Expandable Checkbox" component with two states:
70
99
  *
71
- * - **Idle (collapsed)** — checkbox + truncated label + toggle button arranged
72
- * in a horizontal row (cross-axis centered).
73
- * - **Open (expanded)** — checkbox + full multi-line label, with the toggle
74
- * button right-aligned beneath the row.
100
+ * - **Idle (collapsed)** — horizontal row: CheckboxItem (fill) + toggle button
101
+ * (hug), cross-axis centered.
102
+ * - **Open (expanded)** — vertical stack aligned to the end: CheckboxItem with
103
+ * the label slot wrapped in a `ScrollArea` (capped by `checkbox/maxHeight`),
104
+ * then the toggle button right-aligned beneath.
75
105
  *
76
106
  * The checkbox and the toggle button have independent press handlers — pressing
77
107
  * the toggle does not affect the checked state, and toggling the checkbox does
@@ -83,7 +113,9 @@ const BUTTON_DEFAULT_MODES = {
83
113
  * @example
84
114
  * ```tsx
85
115
  * <ExpandableCheckbox
86
- * label="By checking this box, I (a) acknowledge and (b) agree to the full terms…"
116
+ * label="I agree"
117
+ * linkLabel="Terms & Conditions"
118
+ * onLinkPress={openTerms}
87
119
  * defaultChecked
88
120
  * onValueChange={setAccepted}
89
121
  * modes={{ 'Color Mode': 'Light' }}
@@ -92,6 +124,9 @@ const BUTTON_DEFAULT_MODES = {
92
124
  */
93
125
  function ExpandableCheckbox({
94
126
  label = '',
127
+ linkLabel,
128
+ onLinkPress,
129
+ children,
95
130
  checked: controlledChecked,
96
131
  defaultChecked = false,
97
132
  onValueChange,
@@ -132,23 +167,30 @@ function ExpandableCheckbox({
132
167
  onExpandedChange?.(next)
133
168
  }, [disabled, isExpanded, isExpandedControlled, onExpandedChange])
134
169
 
170
+ // Isolate link presses from the CheckboxItem row Pressable so activating the
171
+ // Terms link never toggles the checkbox.
172
+ const handleLinkPress = useCallback(
173
+ (event: GestureResponderEvent) => {
174
+ if (disabled) return
175
+ // RN web: stop the parent Pressable from also receiving the click.
176
+ event.stopPropagation?.()
177
+ onLinkPress?.(event)
178
+ },
179
+ [disabled, onLinkPress]
180
+ )
181
+
135
182
  const gap =
136
183
  (getVariableByName('expandableCheckbox/gap', modes) as number | null) ?? 8
137
-
138
- const rowGap =
139
- (getVariableByName('checkboxItem/gap', modes) as number | null) ?? 8
140
-
141
- const labelColor =
142
- (getVariableByName('checkboxItem/foreground', modes) as string | null) ?? '#1a1c1f'
143
- const labelFontFamily =
144
- (getVariableByName('checkboxItem/label/fontFamily', modes) as string | null) ?? 'JioType Var'
145
- const labelFontSize =
146
- (getVariableByName('checkboxItem/label/fontSize', modes) as number | null) ?? 14
147
- const labelLineHeight =
148
- (getVariableByName('checkboxItem/label/lineHeight', modes) as number | null) ?? 19
149
- const labelFontWeightRaw =
150
- getVariableByName('checkboxItem/label/fontWeight', modes) ?? 400
151
- const labelFontWeight = String(labelFontWeightRaw) as TextStyle['fontWeight']
184
+ const maxHeight =
185
+ (getVariableByName('checkbox/maxHeight', modes) as number | null) ?? 450
186
+ const scrollPaddingLeft =
187
+ (getVariableByName('scrollArea/padding/left', modes) as number | null) ?? 0
188
+ const scrollPaddingTop =
189
+ (getVariableByName('scrollArea/padding/top', modes) as number | null) ?? 0
190
+ const scrollPaddingRight =
191
+ (getVariableByName('scrollArea/padding/right', modes) as number | null) ?? 0
192
+ const scrollPaddingBottom =
193
+ (getVariableByName('scrollArea/padding/bottom', modes) as number | null) ?? 0
152
194
 
153
195
  const containerStyle: ViewStyle = useMemo(
154
196
  () => ({
@@ -161,43 +203,36 @@ function ExpandableCheckbox({
161
203
  [isExpanded, gap, disabled]
162
204
  )
163
205
 
164
- const rowStyle: ViewStyle = useMemo(
206
+ const itemWrapperStyle: ViewStyle = useMemo(
165
207
  () => ({
166
208
  flex: isExpanded ? undefined : 1,
167
209
  alignSelf: isExpanded ? 'stretch' : 'auto',
168
210
  minWidth: 0,
169
- flexDirection: 'row',
211
+ width: isExpanded ? '100%' : undefined,
212
+ }),
213
+ [isExpanded]
214
+ )
215
+
216
+ const checkboxItemStyle: ViewStyle = useMemo(
217
+ () => ({
218
+ // CheckboxItem hard-codes width: '100%'; clear it in the Idle row so the
219
+ // Read more button can sit beside a flex-growing item.
220
+ width: isExpanded ? '100%' : undefined,
170
221
  alignItems: isExpanded ? 'flex-start' : 'center',
171
- gap: rowGap,
172
222
  }),
173
- [isExpanded, rowGap]
223
+ [isExpanded]
174
224
  )
175
225
 
176
- const resolvedLabelStyle: TextStyle = useMemo(
226
+ const slotRowStyle: ViewStyle = useMemo(
177
227
  () => ({
228
+ flexDirection: 'row',
229
+ flexWrap: 'wrap',
230
+ alignItems: isExpanded ? 'flex-start' : 'center',
231
+ gap: isExpanded ? OPEN_SLOT_GAP : IDLE_SLOT_GAP,
178
232
  flex: 1,
179
233
  minWidth: 0,
180
- color: labelColor,
181
- fontFamily: labelFontFamily,
182
- fontSize: labelFontSize,
183
- lineHeight: labelLineHeight,
184
- fontWeight: labelFontWeight,
185
- // Android adds asymmetric font padding and top-aligns the glyph inside
186
- // an inflated line box when `lineHeight` is set. That makes the centered
187
- // checkbox look like it drops below the text. Disabling the extra
188
- // padding + centering the glyph keeps the single-line label optically
189
- // aligned with the checkbox. No-op on iOS / web.
190
- includeFontPadding: false,
191
- textAlignVertical: isExpanded ? 'top' : 'center',
192
234
  }),
193
- [
194
- labelColor,
195
- labelFontFamily,
196
- labelFontSize,
197
- labelLineHeight,
198
- labelFontWeight,
199
- isExpanded,
200
- ]
235
+ [isExpanded]
201
236
  )
202
237
 
203
238
  // Layer component modes first (e.g. Color Mode), then button defaults so
@@ -208,31 +243,115 @@ function ExpandableCheckbox({
208
243
  )
209
244
 
210
245
  const a11yLabel =
211
- accessibilityLabel ?? (typeof label === 'string' ? label : undefined)
212
- const buttonLabel = isExpanded ? readLessLabel : readMoreLabel
246
+ accessibilityLabel ??
247
+ (typeof label === 'string' && label.length > 0
248
+ ? linkLabel
249
+ ? `${label} ${linkLabel}`
250
+ : label
251
+ : linkLabel)
213
252
 
214
- const labelNumberOfLinesProps =
253
+ const buttonLabel = isExpanded ? readLessLabel : readMoreLabel
254
+ const shouldTruncate =
215
255
  !disableTruncation && !isExpanded && collapsedLines > 0
216
- ? { numberOfLines: collapsedLines, ellipsizeMode: 'tail' as const }
217
- : null
256
+
257
+ const hasChildren = children != null && children !== false
258
+
259
+ const composedLabel = hasChildren ? (
260
+ cloneChildrenWithModes(children, modes)
261
+ ) : (
262
+ <View style={slotRowStyle}>
263
+ {label ? (
264
+ <Text
265
+ text={label}
266
+ modes={modes}
267
+ textAlign="Left"
268
+ style={labelStyle}
269
+ {...(shouldTruncate
270
+ ? { numberOfLines: collapsedLines }
271
+ : { disableTruncation: true })}
272
+ />
273
+ ) : null}
274
+ {linkLabel ? (
275
+ // Capture the touch so the parent CheckboxItem Pressable does not also
276
+ // toggle the checkbox when the Terms link is activated.
277
+ <View onStartShouldSetResponder={() => true}>
278
+ <Link
279
+ text={linkLabel}
280
+ onPress={handleLinkPress}
281
+ disabled={disabled}
282
+ modes={modes}
283
+ textAlign="Left"
284
+ // Idle: Fill (Figma). Open: Hug (Figma).
285
+ autolayout={isExpanded || !label ? 'Hug' : 'Fill'}
286
+ />
287
+ </View>
288
+ ) : null}
289
+ </View>
290
+ )
291
+
292
+ const labelSlot = isExpanded ? (
293
+ <ScrollArea
294
+ direction="vertical"
295
+ modes={modes}
296
+ showScrollIndicator={false}
297
+ paddingLeft={scrollPaddingLeft}
298
+ paddingTop={scrollPaddingTop}
299
+ paddingRight={scrollPaddingRight}
300
+ paddingBottom={scrollPaddingBottom}
301
+ style={{ maxHeight, width: '100%', flexGrow: 1, flexShrink: 1 }}
302
+ contentContainerStyle={{
303
+ flexDirection: 'row',
304
+ flexWrap: 'wrap',
305
+ alignItems: 'flex-start',
306
+ gap: OPEN_SLOT_GAP,
307
+ }}
308
+ >
309
+ {hasChildren ? (
310
+ composedLabel
311
+ ) : (
312
+ <>
313
+ {label ? (
314
+ <Text
315
+ text={label}
316
+ modes={modes}
317
+ textAlign="Left"
318
+ style={labelStyle}
319
+ disableTruncation
320
+ />
321
+ ) : null}
322
+ {linkLabel ? (
323
+ <View onStartShouldSetResponder={() => true}>
324
+ <Link
325
+ text={linkLabel}
326
+ onPress={handleLinkPress}
327
+ disabled={disabled}
328
+ modes={modes}
329
+ textAlign="Left"
330
+ autolayout="Hug"
331
+ />
332
+ </View>
333
+ ) : null}
334
+ </>
335
+ )}
336
+ </ScrollArea>
337
+ ) : (
338
+ composedLabel
339
+ )
218
340
 
219
341
  return (
220
342
  <View style={[containerStyle, style]}>
221
- <View style={rowStyle}>
222
- <Checkbox
343
+ <View style={itemWrapperStyle}>
344
+ <CheckboxItem
223
345
  checked={isChecked}
224
346
  disabled={disabled}
225
347
  onValueChange={handleToggleChecked}
226
348
  modes={modes}
349
+ control="leading"
350
+ style={checkboxItemStyle}
227
351
  {...(a11yLabel !== undefined ? { accessibilityLabel: a11yLabel } : {})}
228
- />
229
- <Text
230
- style={[resolvedLabelStyle, labelStyle]}
231
- selectable={false}
232
- {...(labelNumberOfLinesProps ?? {})}
233
352
  >
234
- {label}
235
- </Text>
353
+ {labelSlot}
354
+ </CheckboxItem>
236
355
  </View>
237
356
  <Button
238
357
  label={buttonLabel}
@@ -28,7 +28,8 @@ import IconButton from '../IconButton/IconButton'
28
28
  import type { Modes } from '../../design-tokens'
29
29
 
30
30
  const IS_WEB = Platform.OS === 'web'
31
- const STATE_COLLECTION = 'HelloJioInput State'
31
+ /** Figma collection name — must match `figma-modes.generated.ts` exactly. */
32
+ const STATE_COLLECTION = 'Hello Jio Input State'
32
33
 
33
34
  /** Default IconButton modes matching the Figma send control. */
34
35
  const DEFAULT_SEND_BUTTON_MODES = Object.freeze({
@@ -36,6 +37,14 @@ const DEFAULT_SEND_BUTTON_MODES = Object.freeze({
36
37
  'Button / Size': 'S',
37
38
  }) as Modes
38
39
 
40
+ /**
41
+ * Figma `Hello Jio Input State` only exposes Idle | Active. The component-set
42
+ * variant IdleJioPlus is glass chrome on top of Idle tokens — never a mode
43
+ * value for this collection.
44
+ */
45
+ type TokenState = 'Idle' | 'Active'
46
+ type VisualState = TokenState | 'IdleJioPlus'
47
+
39
48
  const toNumber = (value: unknown, fallback: number): number => {
40
49
  if (typeof value === 'number') {
41
50
  return Number.isFinite(value) ? value : fallback
@@ -56,7 +65,27 @@ const toFontWeight = (
56
65
  return fallback
57
66
  }
58
67
 
59
- type VisualState = 'Idle' | 'Active' | 'IdleJioPlus'
68
+ /** True when a CSS/RN color string already carries a non-opaque alpha channel. */
69
+ const hasAlphaChannel = (color: string): boolean => {
70
+ const trimmed = color.trim()
71
+ if (/^#[0-9a-fA-F]{8}$/.test(trimmed)) {
72
+ const alpha = parseInt(trimmed.slice(7, 9), 16)
73
+ return alpha < 255
74
+ }
75
+ if (/^#[0-9a-fA-F]{4}$/.test(trimmed)) {
76
+ const alphaNibble = parseInt(trimmed.slice(3, 4), 16)
77
+ return alphaNibble < 15
78
+ }
79
+ const func = trimmed.match(/^(rgba|hsla)\(/i)
80
+ if (func) {
81
+ const parts = trimmed.slice(trimmed.indexOf('(') + 1, -1).split(',')
82
+ if (parts.length === 4) {
83
+ const alpha = Number(parts[3].trim())
84
+ return Number.isFinite(alpha) && alpha < 1
85
+ }
86
+ }
87
+ return false
88
+ }
60
89
 
61
90
  /**
62
91
  * Fallback values from the Figma `HelloJioInput` component when local token
@@ -85,14 +114,11 @@ const STATE_FALLBACKS: Record<
85
114
  iconColor: '#24262b',
86
115
  },
87
116
  IdleJioPlus: {
88
- // Glass fill. In Figma the IdleJioPlus surface resolves
89
- // `helloJioInput/background` under the `Page type = JioPlus` variable mode,
90
- // which yields a TRANSLUCENT light fill (not the opaque `#f5f5f5` the
91
- // MainPage mode gives). Painted over the backdrop blur, an opaque value
92
- // would read as a solid bar and hide the blur entirely — so, matching the
93
- // translucent glass fallbacks the sibling GlassFill consumers use
94
- // (FavoriteToggle `#ffffff33`, NumberPagination `rgba(141,141,141,0.4)`),
95
- // this fallback carries alpha so the frost tints the blurred content.
117
+ // Frosted IdleJioPlus fill. Figma paints `helloJioInput/background` over
118
+ // `glass/minimal` blur; the synced token is often opaque `#f5f5f5`, which
119
+ // would hide the blur. Match sibling GlassFill consumers (FavoriteToggle
120
+ // `#ffffff33`, NumberPagination `rgba(141,141,141,0.4)`) with alpha so the
121
+ // frost tints the blurred content.
96
122
  background: '#f5f5f566',
97
123
  borderColor: '#f5f5f5',
98
124
  labelColor: '#545961',
@@ -173,6 +199,8 @@ interface HelloJioInputTokens {
173
199
  iconSize: number
174
200
  blurIntensity: number
175
201
  useGlass: boolean
202
+ /** Tint laid over GlassFill; translucent so the frost remains visible. */
203
+ glassOverlay: string
176
204
  }
177
205
 
178
206
  function resolveHelloJioInputTokens(
@@ -247,6 +275,15 @@ function resolveHelloJioInputTokens(
247
275
  const blurIntensity = Math.max(0, Math.min(100, Math.round(blurMinimal)))
248
276
  const useGlass = visualState === 'IdleJioPlus'
249
277
 
278
+ // Figma IdleJioPlus paints `helloJioInput/background` over `glass/minimal`
279
+ // blur. The exported token is often opaque `#f5f5f5` (alpha lost in sync),
280
+ // which would fully hide the blur — keep a translucent frost when needed.
281
+ const glassOverlay = useGlass
282
+ ? hasAlphaChannel(background)
283
+ ? background
284
+ : STATE_FALLBACKS.IdleJioPlus.background
285
+ : background
286
+
250
287
  return {
251
288
  containerStyle: {
252
289
  position: 'relative',
@@ -291,6 +328,7 @@ function resolveHelloJioInputTokens(
291
328
  iconSize,
292
329
  blurIntensity,
293
330
  useGlass,
331
+ glassOverlay,
294
332
  }
295
333
  }
296
334
 
@@ -341,21 +379,28 @@ const HelloJioInput = forwardRef<RNTextInput, HelloJioInputProps>(
341
379
  const isControlled = value !== undefined
342
380
  const currentValue = isControlled ? value : uncontrolledValue
343
381
 
382
+ // Component-set visual vs token mode:
383
+ // - Idle / Active map 1:1 onto `Hello Jio Input State`
384
+ // - IdleJioPlus is Figma chrome (glass) on Idle tokens — never pass
385
+ // "IdleJioPlus" into the state collection (that mode does not exist)
344
386
  const visualState: VisualState = isFocused
345
387
  ? 'Active'
346
388
  : jioPlus
347
389
  ? 'IdleJioPlus'
348
390
  : 'Idle'
391
+ const tokenState: TokenState = isFocused ? 'Active' : 'Idle'
349
392
 
350
393
  const modes = useMemo(
351
394
  () => ({
352
395
  ...globalModes,
353
396
  ...propModes,
354
- [STATE_COLLECTION]: visualState,
397
+ [STATE_COLLECTION]: tokenState,
355
398
  }),
356
- [globalModes, propModes, visualState]
399
+ [globalModes, propModes, tokenState]
357
400
  )
358
401
 
402
+ // Defaults first; caller/`modes` may still override Size/Appearance.
403
+ // Custom chrome that must ignore parent modes belongs in `trailing`.
359
404
  const sendButtonModes = useMemo(
360
405
  () => ({ ...DEFAULT_SEND_BUTTON_MODES, ...modes }),
361
406
  [modes]
@@ -449,17 +494,13 @@ const HelloJioInput = forwardRef<RNTextInput, HelloJioInputProps>(
449
494
  style,
450
495
  ]
451
496
 
452
- const glassOverlay =
453
- (getVariableByName('helloJioInput/background', modes) as string | null) ??
454
- STATE_FALLBACKS.IdleJioPlus.background
455
-
456
497
  const inner = (
457
498
  <>
458
499
  {tokens.useGlass ? (
459
500
  <GlassFill
460
501
  tint="light"
461
502
  intensity={tokens.blurIntensity}
462
- overlayColor={glassOverlay}
503
+ overlayColor={tokens.glassOverlay}
463
504
  androidTintWash={false}
464
505
  />
465
506
  ) : null}
@@ -67,7 +67,7 @@ function resolveIconCapsuleTokens(modes: Modes): IconCapsuleTokens {
67
67
  * @param {Object} props - Component props
68
68
  * @param {string} [props.iconName="ic_card"] - The name of the icon to display from the icon registry
69
69
  * @param {UnifiedSource} [props.source] - Fallback source (remote URI, inline SVG XML, `require()` asset, SVG React component, or React element). Used when `iconName` is missing or unknown. Tinted with the mode-resolved icon color so it follows design tokens just like a built-in icon.
70
- * @param {Object} [props.modes={}] - Mode configuration for design tokens (e.g., {"Appearance": "Primary"})
70
+ * @param {Object} [props.modes={}] - Mode configuration for design tokens (e.g., {"AppearanceBrand": "Primary"})
71
71
  * @param {string} [props.accessibilityLabel] - Accessibility label for screen readers
72
72
  * @param {string} [props.accessibilityRole] - Accessibility role (defaults to "image" for decorative icons)
73
73
  *
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { useMemo } from 'react'
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -7,12 +7,11 @@ import {
7
7
  type TextStyle,
8
8
  } from 'react-native'
9
9
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
10
- import { EMPTY_MODES } from '../../utils/react-utils'
10
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
11
11
  import Icon from '../Icon/Icon'
12
12
  import type { Modes } from '../../design-tokens'
13
13
 
14
14
  const LINE_HEIGHT_RATIO = 1.2
15
- const ICON_SIZE = 18
16
15
  // Figma renders the value-row icon with no surrounding padding; the shared Icon
17
16
  // component pulls padding from `icon/padding/*` tokens, so we zero it here.
18
17
  const ICON_PADDING_RESET: ViewStyle = {
@@ -34,8 +33,9 @@ export type MetricDataProps = {
34
33
  caption?: string
35
34
  /**
36
35
  * Optional glyph shown to the left of the value. Pass a registry icon name
37
- * (e.g. `'ic_card'`) or a custom node. Rendered at 18px, tinted to the value
38
- * colour. Hidden when omitted.
36
+ * (e.g. `'ic_card'`) or a custom node. Size/colour resolve from `icon/size`
37
+ * and `icon/color` (Figma seeds `AppearanceBrand: Secondary` → purple).
38
+ * Opt-in — omitted keeps existing no-icon instances unchanged.
39
39
  */
40
40
  icon?: string | React.ReactNode
41
41
  /** Design-token modes for theming. */
@@ -59,9 +59,10 @@ export type MetricDataProps = {
59
59
  * MetricData — a compact, centered metric block.
60
60
  *
61
61
  * Stacks a small `title`, a prominent `value` (with an optional leading
62
- * `icon`), and a muted `caption`. Typography, colours, gaps and padding all
63
- * resolve from the `metricdata/*` design tokens. `title`, `caption` and `icon`
64
- * are optional; only `value` is shown by default.
62
+ * `icon` in a `value wrap` row), and a muted `caption`. Typography, colours,
63
+ * gaps and padding resolve from the `metricdata/*` design tokens; the leading
64
+ * glyph uses `icon/*`. `title`, `caption` and `icon` are optional — omit
65
+ * `icon` to keep the legacy text-only layout.
65
66
  *
66
67
  * @example
67
68
  * ```tsx
@@ -81,10 +82,17 @@ function MetricData({
81
82
  captionStyle,
82
83
  accessibilityLabel,
83
84
  }: MetricDataProps) {
85
+ // Figma's Metricdata instance seeds Secondary on the nested Icon so the
86
+ // default glyph is brand purple (`#5d00b5`). Caller `modes` still win.
87
+ const iconModes = useMemo<Modes>(
88
+ () => ({ AppearanceBrand: 'Secondary', ...modes }),
89
+ [modes]
90
+ )
91
+
84
92
  const titleColor = (getVariableByName('metricdata/title/color', modes) as string | null) ?? '#000000'
85
93
  const titleFontFamily = (getVariableByName('metricdata/title/fontfamily', modes) as string | null) ?? 'JioType Var'
86
94
  const titleFontSize = (getVariableByName('metricdata/title/fontsize', modes) as number | null) ?? 12
87
- const titleFontWeight = String(getVariableByName('metricdata/title/fontweight', modes) ?? 400) as TextStyle['fontWeight']
95
+ const titleFontWeight = String(getVariableByName('metricdata/title/fontweight', modes) ?? 500) as TextStyle['fontWeight']
88
96
 
89
97
  const valueColor = (getVariableByName('metricdata/value/color', modes) as string | null) ?? '#000000'
90
98
  const valueFontFamily = (getVariableByName('metricdata/value/fontfamily', modes) as string | null) ?? 'JioType Var'
@@ -94,13 +102,18 @@ function MetricData({
94
102
  const captionColor = (getVariableByName('metricdata/caption/color', modes) as string | null) ?? '#777777'
95
103
  const captionFontFamily = (getVariableByName('metricdata/caption/fontfamily', modes) as string | null) ?? 'JioType Var'
96
104
  const captionFontSize = (getVariableByName('metricdata/caption/fontsize', modes) as number | null) ?? 12
97
- const captionFontWeight = String(getVariableByName('metricdata/caption/fontweight', modes) ?? 500) as TextStyle['fontWeight']
105
+ const captionFontWeight = String(getVariableByName('metricdata/caption/fontweight', modes) ?? 400) as TextStyle['fontWeight']
98
106
 
99
107
  const gap = (getVariableByName('metricdata/gap', modes) as number | null) ?? 4
100
108
  const valueWrapGap = (getVariableByName('metricdata/valueWrap/gap', modes) as number | null) ?? 4
101
109
  const paddingHorizontal = (getVariableByName('metricdata/padding/horizontal', modes) as number | null) ?? 10
102
110
  const paddingVertical = (getVariableByName('metricdata/padding/vertical', modes) as number | null) ?? 10
103
111
 
112
+ // Resolve via getVariableByName so extract-tokens picks up the new bindings;
113
+ // Icon reads the same tokens when color/size props are omitted.
114
+ const iconColor = (getVariableByName('icon/color', iconModes) as string | null) ?? '#5d00b5'
115
+ const iconSize = (getVariableByName('icon/size', iconModes) as number | null) ?? 18
116
+
104
117
  const containerStyle: ViewStyle = {
105
118
  flexDirection: 'column',
106
119
  alignItems: 'center',
@@ -141,15 +154,17 @@ function MetricData({
141
154
  icon == null ? null : typeof icon === 'string' ? (
142
155
  <Icon
143
156
  iconName={icon}
144
- size={ICON_SIZE}
145
- color={valueColor}
146
- modes={modes}
157
+ size={iconSize}
158
+ color={iconColor}
159
+ modes={iconModes}
147
160
  style={ICON_PADDING_RESET}
148
161
  />
149
162
  ) : (
150
- icon
163
+ cloneChildrenWithModes(icon, iconModes)
151
164
  )
152
165
 
166
+ const showValueWrap = iconNode != null || value !== undefined
167
+
153
168
  const resolvedLabel =
154
169
  accessibilityLabel ??
155
170
  ([title, typeof value === 'string' ? value : undefined, caption]
@@ -166,14 +181,16 @@ function MetricData({
166
181
  <Text style={[titleTextStyle, titleStyle]}>{title}</Text>
167
182
  ) : null}
168
183
 
169
- <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: valueWrapGap }}>
170
- {iconNode}
171
- {typeof value === 'string' ? (
172
- <Text style={[valueTextStyle, valueStyle]}>{value}</Text>
173
- ) : (
174
- value
175
- )}
176
- </View>
184
+ {showValueWrap ? (
185
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: valueWrapGap }}>
186
+ {iconNode}
187
+ {typeof value === 'string' ? (
188
+ <Text style={[valueTextStyle, valueStyle]}>{value}</Text>
189
+ ) : (
190
+ value
191
+ )}
192
+ </View>
193
+ ) : null}
177
194
 
178
195
  {caption !== undefined && caption !== '' ? (
179
196
  <Text style={[captionTextStyle, captionStyle]}>{caption}</Text>
@@ -245,7 +245,7 @@ function MoneyValue({
245
245
  }, [focused, cursorOpacity])
246
246
 
247
247
  // Keyboard avoidance — lift the component when the keyboard would cover it.
248
- // Follows the same pattern as ActionFooter (uses Keyboard.addListener).
248
+ // Uses Keyboard.addListener + measureInWindow overlap math (field-local).
249
249
  // Resets when editing ends or the keyboard hides.
250
250
  const rootRef = useRef<View>(null)
251
251
  const keyboardLift = useRef(new Animated.Value(0)).current
@@ -64,7 +64,7 @@ function toFontWeight(value: unknown, fallback: TextStyle['fontWeight']): TextSt
64
64
 
65
65
  function resolveNudgeTokens(modes: Modes): NudgeTokens {
66
66
  const background = getVariableByName('nudge/background', modes)
67
- const radius = getVariableByName('nudge/radius', modes)
67
+ const radius = getVariableByName('nudge/borderRadius', modes)
68
68
  const paddingH = getVariableByName('nudge/padding/horizontal', modes)
69
69
  const paddingV = getVariableByName('nudge/padding/vertical', modes)
70
70
  const gap = getVariableByName('nudge/gap', modes)