jfs-components 0.0.72 → 0.0.74

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 (158) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/commonjs/components/AccordionCheckbox/AccordionCheckbox.js +239 -0
  3. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  4. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  5. package/lib/commonjs/components/BrandChip/BrandChip.js +149 -0
  6. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +229 -0
  7. package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
  8. package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
  9. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +140 -0
  10. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
  11. package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
  12. package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
  13. package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
  14. package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
  15. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  16. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  17. package/lib/commonjs/components/FormField/FormField.js +328 -178
  18. package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
  19. package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
  20. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  21. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
  22. package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
  23. package/lib/commonjs/components/OTP/OTP.js +381 -37
  24. package/lib/commonjs/components/PageHero/PageHero.js +153 -0
  25. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  26. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  27. package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
  28. package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
  29. package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
  30. package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
  31. package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
  32. package/lib/commonjs/components/StatItem/StatItem.js +65 -35
  33. package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
  34. package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
  35. package/lib/commonjs/components/Text/Text.js +9 -2
  36. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  37. package/lib/commonjs/components/index.js +231 -1
  38. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  39. package/lib/commonjs/icons/registry.js +1 -1
  40. package/lib/commonjs/utils/index.js +7 -0
  41. package/lib/commonjs/utils/number-utils.js +57 -0
  42. package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
  43. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  44. package/lib/module/components/AppBar/AppBar.js +17 -11
  45. package/lib/module/components/BrandChip/BrandChip.js +143 -0
  46. package/lib/module/components/CardBankAccount/CardBankAccount.js +223 -0
  47. package/lib/module/components/CardInsight/CardInsight.js +161 -0
  48. package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
  49. package/lib/module/components/CheckboxItem/CheckboxItem.js +134 -0
  50. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
  51. package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
  52. package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
  53. package/lib/module/components/DonutChart/DonutChart.js +303 -0
  54. package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
  55. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  56. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  57. package/lib/module/components/FormField/FormField.js +330 -180
  58. package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
  59. package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
  60. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  61. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
  62. package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
  63. package/lib/module/components/OTP/OTP.js +381 -38
  64. package/lib/module/components/PageHero/PageHero.js +147 -0
  65. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  66. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  67. package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
  68. package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
  69. package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
  70. package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
  71. package/lib/module/components/StatGroup/StatGroup.js +123 -0
  72. package/lib/module/components/StatItem/StatItem.js +66 -36
  73. package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
  74. package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
  75. package/lib/module/components/Text/Text.js +9 -2
  76. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  77. package/lib/module/components/index.js +28 -2
  78. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  79. package/lib/module/icons/registry.js +1 -1
  80. package/lib/module/utils/index.js +2 -1
  81. package/lib/module/utils/number-utils.js +53 -0
  82. package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
  83. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  84. package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
  85. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +86 -0
  86. package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
  87. package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
  88. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +72 -0
  89. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
  90. package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
  91. package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
  92. package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
  93. package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
  94. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  95. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  96. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  97. package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
  98. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  99. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
  100. package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
  101. package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
  102. package/lib/typescript/src/components/PageHero/PageHero.d.ts +53 -0
  103. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  104. package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
  105. package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
  106. package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
  107. package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
  108. package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
  109. package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
  110. package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
  111. package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
  112. package/lib/typescript/src/components/Text/Text.d.ts +12 -2
  113. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  114. package/lib/typescript/src/components/index.d.ts +29 -3
  115. package/lib/typescript/src/icons/registry.d.ts +1 -1
  116. package/lib/typescript/src/utils/index.d.ts +1 -0
  117. package/lib/typescript/src/utils/number-utils.d.ts +29 -0
  118. package/package.json +1 -3
  119. package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
  120. package/src/components/AccountCard/AccountCard.tsx +376 -0
  121. package/src/components/AppBar/AppBar.tsx +25 -14
  122. package/src/components/BrandChip/BrandChip.tsx +235 -0
  123. package/src/components/CardBankAccount/CardBankAccount.tsx +321 -0
  124. package/src/components/CardInsight/CardInsight.tsx +239 -0
  125. package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
  126. package/src/components/CheckboxItem/CheckboxItem.tsx +209 -0
  127. package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
  128. package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
  129. package/src/components/CoverageRing/CoverageRing.tsx +225 -0
  130. package/src/components/DonutChart/DonutChart.tsx +503 -0
  131. package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
  132. package/src/components/Dropdown/Dropdown.tsx +331 -0
  133. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  134. package/src/components/FormField/FormField.tsx +542 -215
  135. package/src/components/LinearMeter/LinearMeter.tsx +9 -39
  136. package/src/components/LinearProgress/LinearProgress.tsx +92 -0
  137. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  138. package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
  139. package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
  140. package/src/components/OTP/OTP.tsx +476 -29
  141. package/src/components/PageHero/PageHero.tsx +200 -0
  142. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  143. package/src/components/PoweredByLabel/finvu.png +0 -0
  144. package/src/components/ProductOverview/ProductOverview.tsx +236 -0
  145. package/src/components/RangeTrack/RangeTrack.tsx +394 -0
  146. package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
  147. package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
  148. package/src/components/StatGroup/StatGroup.tsx +169 -0
  149. package/src/components/StatItem/StatItem.tsx +117 -40
  150. package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
  151. package/src/components/SummaryTile/SummaryTile.tsx +251 -0
  152. package/src/components/Text/Text.tsx +24 -3
  153. package/src/components/Tooltip/Tooltip.tsx +50 -25
  154. package/src/components/index.ts +47 -3
  155. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  156. package/src/icons/registry.ts +1 -1
  157. package/src/utils/index.ts +1 -0
  158. package/src/utils/number-utils.ts +60 -0
@@ -0,0 +1,256 @@
1
+ import React from 'react'
2
+ import { View, type StyleProp, type ViewStyle } from 'react-native'
3
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
5
+ import { EMPTY_MODES } from '../../utils/react-utils'
6
+ import DonutChart, { type DonutChartSegmentData } from '../DonutChart/DonutChart'
7
+ import MetricLegendItem from '../MetricLegendItem/MetricLegendItem'
8
+
9
+ /**
10
+ * One row of the `DonutChartSummary`. Each item drives BOTH a donut
11
+ * segment and the matching legend row, so the segment and indicator
12
+ * always share the same color by construction.
13
+ */
14
+ export type DonutChartSummaryItem = {
15
+ /** Stable React key. Falls back to the item label / index. */
16
+ key?: React.Key
17
+ /** The descriptive label rendered next to the indicator dot. */
18
+ label?: React.ReactNode
19
+ /**
20
+ * The data point — drives the segment's angular share. Same
21
+ * semantics as `DonutChartSegmentData.value`. When the legend's
22
+ * right-side text is shown, the resolution order is:
23
+ * `displayValue` → `formatValue(value)` → hide.
24
+ */
25
+ value: number
26
+ /**
27
+ * Optional override for the legend row's right-side text. Falls
28
+ * back to `formatValue(value)` if a parent-level `formatValue` is
29
+ * provided; otherwise the value slot is hidden (matches the Figma
30
+ * `data` boolean toggle on `MetricLegendItem`). Pass `null` or
31
+ * `false` to explicitly hide on a per-item basis.
32
+ */
33
+ displayValue?: React.ReactNode
34
+ /**
35
+ * Hard-override the shared color used for both the donut segment
36
+ * and the legend indicator. Bypasses `dataViz/bg` token resolution.
37
+ */
38
+ color?: string
39
+ /**
40
+ * Per-item design token mode overrides. Merged on top of parent
41
+ * `modes` and the per-index `Appearance / DataViz` defaults
42
+ * (`Senary`, `Primary`, `Secondary`, `Tertiary`, `Quaternary`,
43
+ * `Quinary`, then cycles).
44
+ */
45
+ modes?: Record<string, any>
46
+ /** Accessibility label for the segment + legend row pairing. */
47
+ accessibilityLabel?: string
48
+ }
49
+
50
+ export type DonutChartSummaryProps = {
51
+ /**
52
+ * Data-driven rows. Each entry produces a donut segment AND a
53
+ * matching `MetricLegendItem` — the count of legend rows is locked
54
+ * in step with the count of donut segments by construction.
55
+ */
56
+ items?: DonutChartSummaryItem[]
57
+ /**
58
+ * Optional formatter applied to each item's numeric `value` when
59
+ * the item does not provide an explicit `displayValue`. Useful for
60
+ * the common "format every weight the same way" case
61
+ * (e.g. `(v) => `${v}%``).
62
+ */
63
+ formatValue?: (value: number) => React.ReactNode
64
+ /** Center value text inside the donut (large/bold). */
65
+ centerValue?: React.ReactNode
66
+ /** Center label text inside the donut (smaller, beneath the value). */
67
+ centerLabel?: React.ReactNode
68
+ /**
69
+ * Slot for fully-custom donut center content. When provided it
70
+ * replaces the default `centerValue`/`centerLabel` text wrap.
71
+ */
72
+ donutCenter?: React.ReactNode
73
+ /** Outer donut diameter in px. Defaults to 194 (Figma reference). */
74
+ donutSize?: number
75
+ /** Donut ring stroke width in px. Defaults to ~18% of `donutSize`. */
76
+ donutStrokeWidth?: number
77
+ /**
78
+ * Visual gap between adjacent donut segments expressed in degrees
79
+ * of arc. Defaults to 0 (segments touch).
80
+ */
81
+ donutGap?: number
82
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
83
+ modes?: Record<string, any>
84
+ /** Override outer container styles. */
85
+ style?: StyleProp<ViewStyle>
86
+ /** Override the legend container styles. */
87
+ legendStyle?: StyleProp<ViewStyle>
88
+ /** Accessibility label announced for the whole summary. */
89
+ accessibilityLabel?: string
90
+ }
91
+
92
+ const DEFAULT_APPEARANCE_CYCLE = [
93
+ 'Senary',
94
+ 'Primary',
95
+ 'Secondary',
96
+ 'Tertiary',
97
+ 'Quaternary',
98
+ 'Quinary',
99
+ ] as const
100
+
101
+ const DEFAULT_ITEMS: DonutChartSummaryItem[] = [
102
+ { label: 'Equity', value: 40, displayValue: '40%' },
103
+ { label: 'Recommended coverage', value: 25, displayValue: '25%' },
104
+ { label: 'Additional benefits', value: 20, displayValue: '20%' },
105
+ { label: 'Cost analysis', value: 15, displayValue: '15%' },
106
+ ]
107
+
108
+ const defaultAppearanceFor = (index: number) =>
109
+ DEFAULT_APPEARANCE_CYCLE[index % DEFAULT_APPEARANCE_CYCLE.length]
110
+
111
+ /**
112
+ * Resolve the shared color for an item. Honors any explicit `color`
113
+ * override, then falls back to `dataViz/bg` for the merged mode set,
114
+ * then to the Figma reference purple.
115
+ */
116
+ function resolveItemColor(
117
+ parentModes: Record<string, any>,
118
+ item: DonutChartSummaryItem,
119
+ index: number
120
+ ): string {
121
+ if (item.color) return item.color
122
+ const itemModes = {
123
+ ...parentModes,
124
+ 'Appearance / DataViz': defaultAppearanceFor(index),
125
+ ...(item.modes || {}),
126
+ }
127
+ return (
128
+ (getVariableByName('dataViz/bg', itemModes) as string | null) ?? '#5d00b5'
129
+ )
130
+ }
131
+
132
+ /**
133
+ * `DonutChartSummary` pairs a `DonutChart` with a vertical list of
134
+ * `MetricLegendItem` rows. The component takes a single `items` array,
135
+ * so the number of legend rows is locked in step with the number of
136
+ * donut segments by construction — every segment has exactly one
137
+ * legend row, and they share the same color through the same
138
+ * `Appearance / DataViz` cascade as the standalone `DonutChart`.
139
+ *
140
+ * The default 4-item layout receives per-index `Appearance / DataViz`
141
+ * defaults (item 1 → `Senary`, 2 → `Primary`, 3 → `Secondary`, 4 →
142
+ * `Tertiary`, then cycles). Override `modes` per item to remix, or set
143
+ * `Emphasis / DataViz` on the parent `modes` to dim or brighten the
144
+ * whole component at once.
145
+ *
146
+ * @component
147
+ * @param {DonutChartSummaryProps} props
148
+ */
149
+ function DonutChartSummary({
150
+ items,
151
+ formatValue,
152
+ centerValue = '₹51,230',
153
+ centerLabel = 'Total invested',
154
+ donutCenter,
155
+ donutSize = 194,
156
+ donutStrokeWidth,
157
+ donutGap = 0,
158
+ modes: propModes = EMPTY_MODES,
159
+ style,
160
+ legendStyle,
161
+ accessibilityLabel,
162
+ }: DonutChartSummaryProps) {
163
+ const { modes: globalModes } = useTokens()
164
+ const modes = { ...globalModes, ...propModes }
165
+
166
+ const gap =
167
+ (getVariableByName('donutChartSummary/gap', modes) as number | null) ?? 16
168
+ const legendGap =
169
+ (getVariableByName('donutChartSummary/legend/gap', modes) as
170
+ | number
171
+ | null) ?? 8
172
+
173
+ const resolvedItems = items && items.length > 0 ? items : DEFAULT_ITEMS
174
+
175
+ const segments: DonutChartSegmentData[] = resolvedItems.map(
176
+ (item, index) => ({
177
+ key: item.key ?? `segment-${index}`,
178
+ value: item.value,
179
+ color: resolveItemColor(modes, item, index),
180
+ accessibilityLabel: item.accessibilityLabel,
181
+ })
182
+ )
183
+
184
+ const showCustomCenter = donutCenter !== undefined && donutCenter !== null
185
+
186
+ return (
187
+ <View
188
+ accessibilityRole="summary"
189
+ accessibilityLabel={accessibilityLabel}
190
+ style={[
191
+ {
192
+ width: '100%',
193
+ alignItems: 'center',
194
+ gap,
195
+ },
196
+ style,
197
+ ]}
198
+ >
199
+ <DonutChart
200
+ size={donutSize}
201
+ {...(donutStrokeWidth !== undefined && {
202
+ strokeWidth: donutStrokeWidth,
203
+ })}
204
+ gap={donutGap}
205
+ segments={segments}
206
+ modes={modes}
207
+ {...(!showCustomCenter && {
208
+ value: centerValue,
209
+ label: centerLabel,
210
+ })}
211
+ >
212
+ {showCustomCenter ? donutCenter : null}
213
+ </DonutChart>
214
+
215
+ <View
216
+ style={[
217
+ {
218
+ width: '100%',
219
+ gap: legendGap,
220
+ },
221
+ legendStyle,
222
+ ]}
223
+ >
224
+ {resolvedItems.map((item, index) => (
225
+ <MetricLegendItem
226
+ key={item.key ?? `legend-${index}`}
227
+ label={item.label}
228
+ value={resolveLegendValue(item, formatValue)}
229
+ indicatorColor={resolveItemColor(modes, item, index)}
230
+ modes={modes}
231
+ />
232
+ ))}
233
+ </View>
234
+ </View>
235
+ )
236
+ }
237
+
238
+ /**
239
+ * Resolve what to render in the legend row's right-side slot. The
240
+ * order of precedence is:
241
+ * 1. `item.displayValue` if explicitly provided (including `null` /
242
+ * `false`, which the underlying `MetricLegendItem` treats as
243
+ * "hide the value slot").
244
+ * 2. `formatValue(item.value)` when a parent-level formatter exists.
245
+ * 3. `undefined` — the value slot is hidden.
246
+ */
247
+ function resolveLegendValue(
248
+ item: DonutChartSummaryItem,
249
+ formatValue: ((value: number) => React.ReactNode) | undefined
250
+ ): React.ReactNode {
251
+ if (item.displayValue !== undefined) return item.displayValue
252
+ if (formatValue) return formatValue(item.value)
253
+ return undefined
254
+ }
255
+
256
+ export default DonutChartSummary
@@ -0,0 +1,331 @@
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ ScrollView,
6
+ Text,
7
+ View,
8
+ type AccessibilityProps,
9
+ type PressableStateCallbackType,
10
+ type StyleProp,
11
+ type TextStyle,
12
+ type ViewStyle,
13
+ } from 'react-native'
14
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
15
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
16
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
17
+ import Icon from '../../icons/Icon'
18
+
19
+ const IS_WEB = Platform.OS === 'web'
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // DropdownItem
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export type DropdownItemProps = {
26
+ /** Display text for the item. Ignored when `children` is provided. */
27
+ label?: string
28
+ /**
29
+ * Value associated with the item. Used by the parent `Dropdown` /
30
+ * `DropdownInput` to identify the selected item. Optional when the item
31
+ * is purely informational or used outside of a selection context.
32
+ */
33
+ value?: string | number | null
34
+ /** Whether the item is currently selected (renders the trailing check). */
35
+ selected?: boolean
36
+ /** Whether the item is disabled and non-interactive. */
37
+ disabled?: boolean
38
+ /** Optional element rendered before the label (icon, avatar, etc.). */
39
+ leading?: React.ReactNode
40
+ /**
41
+ * Optional element rendered after the label. When omitted and `selected`
42
+ * is true, a check icon is rendered automatically.
43
+ */
44
+ trailing?: React.ReactNode
45
+ /** Press handler. Receives the item's `value` if provided. */
46
+ onPress?: (value: DropdownItemProps['value']) => void
47
+ /** Optional custom child content (overrides `label`). */
48
+ children?: React.ReactNode
49
+ /** Modes for design token resolution. */
50
+ modes?: Record<string, any>
51
+ /** Style overrides for the item container. */
52
+ style?: StyleProp<ViewStyle>
53
+ /** Style overrides for the item label text. */
54
+ labelStyle?: StyleProp<TextStyle>
55
+ /** Accessibility label (defaults to label / inferred from children). */
56
+ accessibilityLabel?: string
57
+ /** Accessibility hint. */
58
+ accessibilityHint?: string
59
+ }
60
+
61
+ function useDropdownItemTokens(modes: Record<string, any>) {
62
+ return useMemo(() => {
63
+ // The `dropdownItem/background` token aliases through the
64
+ // `Dropdown Item State` collection (Idle | Selected), so we resolve
65
+ // both possibilities up-front and pick at render time.
66
+ const idleBackground =
67
+ (getVariableByName('dropdownItem/background', {
68
+ ...modes,
69
+ 'Dropdown Item State': 'Idle',
70
+ }) as string) || '#ffffff'
71
+ const selectedBackground =
72
+ (getVariableByName('dropdownItem/background', {
73
+ ...modes,
74
+ 'Dropdown Item State': 'Selected',
75
+ }) as string) || '#f5f5f5'
76
+ const foreground =
77
+ (getVariableByName('dropdownItem/foreground', modes) as string) ||
78
+ '#000000'
79
+ const fontFamily =
80
+ (getVariableByName('dropdownItem/fontFamily', modes) as string) ||
81
+ 'JioType Var'
82
+ const fontSize =
83
+ parseInt(getVariableByName('dropdownItem/fontSize', modes), 10) || 16
84
+ const fontWeight =
85
+ (getVariableByName('dropdownItem/fontWeight', modes) as string) ||
86
+ '400'
87
+ const lineHeight =
88
+ parseInt(getVariableByName('dropdownItem/lineHeight', modes), 10) || 19
89
+ const gap = parseInt(getVariableByName('dropdownItem/gap', modes), 10) || 8
90
+ const paddingHorizontal =
91
+ parseInt(
92
+ getVariableByName('dropdownItem/padding/horizontal', modes),
93
+ 10
94
+ ) || 12
95
+ const paddingVertical =
96
+ parseInt(
97
+ getVariableByName('dropdownItem/padding/vertical', modes),
98
+ 10
99
+ ) || 12
100
+
101
+ return {
102
+ idleBackground,
103
+ selectedBackground,
104
+ foreground,
105
+ fontFamily,
106
+ fontSize,
107
+ fontWeight,
108
+ lineHeight,
109
+ gap,
110
+ paddingHorizontal,
111
+ paddingVertical,
112
+ }
113
+ }, [modes])
114
+ }
115
+
116
+ export function DropdownItem({
117
+ label,
118
+ value = null,
119
+ selected = false,
120
+ disabled = false,
121
+ leading,
122
+ trailing,
123
+ onPress,
124
+ children,
125
+ modes: propModes = EMPTY_MODES,
126
+ style,
127
+ labelStyle,
128
+ accessibilityLabel,
129
+ accessibilityHint,
130
+ }: DropdownItemProps) {
131
+ const { modes: globalModes } = useTokens()
132
+ const modes = useMemo(
133
+ () => ({ ...globalModes, ...propModes }),
134
+ [globalModes, propModes]
135
+ )
136
+
137
+ const tokens = useDropdownItemTokens(modes)
138
+ const [isHovered, setIsHovered] = useState(false)
139
+
140
+ const handlePress = useCallback(() => {
141
+ if (disabled) return
142
+ onPress?.(value)
143
+ }, [disabled, onPress, value])
144
+
145
+ const containerStyle = useCallback(
146
+ ({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => {
147
+ const showSelected =
148
+ pressed || (isHovered && IS_WEB) || selected
149
+ const base: ViewStyle = {
150
+ flexDirection: 'row',
151
+ alignItems: 'center',
152
+ gap: tokens.gap,
153
+ paddingHorizontal: tokens.paddingHorizontal,
154
+ paddingVertical: tokens.paddingVertical,
155
+ backgroundColor: showSelected
156
+ ? tokens.selectedBackground
157
+ : tokens.idleBackground,
158
+ opacity: disabled ? 0.4 : 1,
159
+ width: '100%',
160
+ }
161
+ return [base, style]
162
+ },
163
+ [
164
+ tokens.gap,
165
+ tokens.paddingHorizontal,
166
+ tokens.paddingVertical,
167
+ tokens.idleBackground,
168
+ tokens.selectedBackground,
169
+ isHovered,
170
+ selected,
171
+ disabled,
172
+ style,
173
+ ]
174
+ )
175
+
176
+ const textStyle: TextStyle = {
177
+ color: tokens.foreground,
178
+ fontFamily: tokens.fontFamily,
179
+ fontSize: tokens.fontSize,
180
+ fontWeight: tokens.fontWeight as TextStyle['fontWeight'],
181
+ lineHeight: tokens.lineHeight,
182
+ flexShrink: 1,
183
+ }
184
+
185
+ const processedLeading = leading
186
+ ? cloneChildrenWithModes(React.Children.toArray(leading), modes)
187
+ : null
188
+
189
+ const customTrailing = trailing
190
+ ? cloneChildrenWithModes(React.Children.toArray(trailing), modes)
191
+ : null
192
+
193
+ const showDefaultCheck = !trailing && selected
194
+
195
+ const fallbackA11yLabel =
196
+ accessibilityLabel ||
197
+ (typeof label === 'string' ? label : 'Dropdown item')
198
+
199
+ const a11yProps: AccessibilityProps = {
200
+ accessibilityRole: 'menuitem',
201
+ accessibilityLabel: fallbackA11yLabel,
202
+ accessibilityState: { selected, disabled },
203
+ }
204
+ if (accessibilityHint) {
205
+ a11yProps.accessibilityHint = accessibilityHint
206
+ }
207
+
208
+ const handleHoverIn = useCallback(() => {
209
+ if (IS_WEB && !disabled) setIsHovered(true)
210
+ }, [disabled])
211
+ const handleHoverOut = useCallback(() => {
212
+ if (IS_WEB) setIsHovered(false)
213
+ }, [])
214
+
215
+ return (
216
+ <Pressable
217
+ onPress={handlePress}
218
+ disabled={disabled}
219
+ onHoverIn={handleHoverIn}
220
+ onHoverOut={handleHoverOut}
221
+ style={containerStyle}
222
+ {...a11yProps}
223
+ >
224
+ {processedLeading}
225
+ {children != null ? (
226
+ children
227
+ ) : (
228
+ <Text style={[textStyle, labelStyle]} numberOfLines={1}>
229
+ {label}
230
+ </Text>
231
+ )}
232
+ {customTrailing}
233
+ {showDefaultCheck && (
234
+ <Icon name="ic_confirm" size={16} color={tokens.foreground} />
235
+ )}
236
+ </Pressable>
237
+ )
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Dropdown (popup surface)
242
+ // ---------------------------------------------------------------------------
243
+
244
+ export type DropdownProps = {
245
+ /** Dropdown items, typically `<DropdownItem />` children. */
246
+ children?: React.ReactNode
247
+ /**
248
+ * Maximum height of the popup before its content becomes scrollable.
249
+ * Defaults to no limit.
250
+ */
251
+ maxHeight?: number
252
+ /** Modes for design token resolution. */
253
+ modes?: Record<string, any>
254
+ /** Style overrides for the popup container. */
255
+ style?: StyleProp<ViewStyle>
256
+ /** Accessibility label for the menu surface. */
257
+ accessibilityLabel?: string
258
+ }
259
+
260
+ /**
261
+ * `Dropdown` is the visual surface (popup) that contains a list of
262
+ * `DropdownItem`s. It is responsible for the background, rounded corners,
263
+ * elevation/shadow, and clipping. Use it standalone for menu UIs, or rely on
264
+ * `DropdownInput` which composes it into a form-field experience.
265
+ */
266
+ export function Dropdown({
267
+ children,
268
+ maxHeight,
269
+ modes: propModes = EMPTY_MODES,
270
+ style,
271
+ accessibilityLabel,
272
+ }: DropdownProps) {
273
+ const { modes: globalModes } = useTokens()
274
+ const modes = useMemo(
275
+ () => ({ ...globalModes, ...propModes }),
276
+ [globalModes, propModes]
277
+ )
278
+
279
+ const radius =
280
+ parseInt(getVariableByName('dropdown/radius', modes), 10) || 8
281
+ const background =
282
+ (getVariableByName('dropdown/background', modes) as string) || '#ffffff'
283
+ const shadowColor =
284
+ (getVariableByName('dropdown/shadow/color', modes) as string) ||
285
+ 'rgba(0, 0, 0, 0.08)'
286
+ const shadowOffsetX =
287
+ parseInt(getVariableByName('dropdown/shadow/offsetX', modes), 10) || 0
288
+ const shadowOffsetY =
289
+ parseInt(getVariableByName('dropdown/shadow/offsetY', modes), 10) || 4
290
+ const shadowBlur =
291
+ parseInt(getVariableByName('dropdown/shadow/blur', modes), 10) || 16
292
+
293
+ const containerStyle: ViewStyle = {
294
+ backgroundColor: background,
295
+ borderRadius: radius,
296
+ overflow: 'hidden',
297
+ shadowColor,
298
+ shadowOffset: { width: shadowOffsetX, height: shadowOffsetY },
299
+ shadowOpacity: 1,
300
+ shadowRadius: shadowBlur / 2,
301
+ elevation: 4,
302
+ }
303
+
304
+ const content = (
305
+ <View style={{ flexDirection: 'column' }}>
306
+ {cloneChildrenWithModes(children, modes)}
307
+ </View>
308
+ )
309
+
310
+ return (
311
+ <View
312
+ style={[containerStyle, style]}
313
+ accessibilityRole="menu"
314
+ accessibilityLabel={accessibilityLabel || 'Dropdown menu'}
315
+ >
316
+ {maxHeight != null ? (
317
+ <ScrollView
318
+ style={{ maxHeight }}
319
+ showsVerticalScrollIndicator={true}
320
+ keyboardShouldPersistTaps="handled"
321
+ >
322
+ {content}
323
+ </ScrollView>
324
+ ) : (
325
+ content
326
+ )}
327
+ </View>
328
+ )
329
+ }
330
+
331
+ export default Dropdown