jfs-components 0.0.70 → 0.0.72

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 (66) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/lib/commonjs/components/CardAdvisory/CardAdvisory.js +203 -0
  3. package/lib/commonjs/components/CardCTA/CardCTA.js +198 -16
  4. package/lib/commonjs/components/CardFinancialCondition/CardFinancialCondition.js +213 -0
  5. package/lib/commonjs/components/Carousel/Carousel.js +9 -7
  6. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +147 -0
  7. package/lib/commonjs/components/CircularProgressBarDoted/CircularProgressBarDoted.js +258 -0
  8. package/lib/commonjs/components/CircularRating/CircularRating.js +161 -0
  9. package/lib/commonjs/components/Gauge/Gauge.js +223 -0
  10. package/lib/commonjs/components/HoldingsCard/HoldingsCard.js +2 -2
  11. package/lib/commonjs/components/InstitutionBadge/InstitutionBadge.js +132 -0
  12. package/lib/commonjs/components/ListGroup/ListGroup.js +3 -1
  13. package/lib/commonjs/components/Nudge/Nudge.js +179 -87
  14. package/lib/commonjs/components/Radio/Radio.js +194 -0
  15. package/lib/commonjs/components/RadioButton/RadioButton.js +21 -188
  16. package/lib/commonjs/components/index.js +56 -0
  17. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  18. package/lib/commonjs/icons/registry.js +1 -1
  19. package/lib/module/components/CardAdvisory/CardAdvisory.js +197 -0
  20. package/lib/module/components/CardCTA/CardCTA.js +199 -17
  21. package/lib/module/components/CardFinancialCondition/CardFinancialCondition.js +207 -0
  22. package/lib/module/components/Carousel/Carousel.js +9 -7
  23. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +141 -0
  24. package/lib/module/components/CircularProgressBarDoted/CircularProgressBarDoted.js +253 -0
  25. package/lib/module/components/CircularRating/CircularRating.js +155 -0
  26. package/lib/module/components/Gauge/Gauge.js +217 -0
  27. package/lib/module/components/HoldingsCard/HoldingsCard.js +2 -2
  28. package/lib/module/components/InstitutionBadge/InstitutionBadge.js +127 -0
  29. package/lib/module/components/ListGroup/ListGroup.js +3 -1
  30. package/lib/module/components/Nudge/Nudge.js +178 -87
  31. package/lib/module/components/Radio/Radio.js +188 -0
  32. package/lib/module/components/RadioButton/RadioButton.js +20 -185
  33. package/lib/module/components/index.js +12 -0
  34. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  35. package/lib/module/icons/registry.js +1 -1
  36. package/lib/typescript/src/components/CardAdvisory/CardAdvisory.d.ts +49 -0
  37. package/lib/typescript/src/components/CardCTA/CardCTA.d.ts +16 -1
  38. package/lib/typescript/src/components/CardFinancialCondition/CardFinancialCondition.d.ts +50 -0
  39. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +27 -0
  40. package/lib/typescript/src/components/CircularProgressBarDoted/CircularProgressBarDoted.d.ts +48 -0
  41. package/lib/typescript/src/components/CircularRating/CircularRating.d.ts +49 -0
  42. package/lib/typescript/src/components/Gauge/Gauge.d.ts +53 -0
  43. package/lib/typescript/src/components/InstitutionBadge/InstitutionBadge.d.ts +30 -0
  44. package/lib/typescript/src/components/Nudge/Nudge.d.ts +14 -11
  45. package/lib/typescript/src/components/Radio/Radio.d.ts +30 -0
  46. package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +20 -28
  47. package/lib/typescript/src/components/index.d.ts +13 -1
  48. package/lib/typescript/src/icons/registry.d.ts +1 -1
  49. package/package.json +1 -1
  50. package/src/components/CardAdvisory/CardAdvisory.tsx +283 -0
  51. package/src/components/CardCTA/CardCTA.tsx +236 -13
  52. package/src/components/CardFinancialCondition/CardFinancialCondition.tsx +366 -0
  53. package/src/components/Carousel/Carousel.tsx +14 -6
  54. package/src/components/CircularProgressBar/CircularProgressBar.tsx +190 -0
  55. package/src/components/CircularProgressBarDoted/CircularProgressBarDoted.tsx +357 -0
  56. package/src/components/CircularRating/CircularRating.tsx +241 -0
  57. package/src/components/Gauge/Gauge.tsx +303 -0
  58. package/src/components/HoldingsCard/HoldingsCard.tsx +2 -2
  59. package/src/components/InstitutionBadge/InstitutionBadge.tsx +216 -0
  60. package/src/components/ListGroup/ListGroup.tsx +3 -1
  61. package/src/components/Nudge/Nudge.tsx +222 -82
  62. package/src/components/Radio/Radio.tsx +227 -0
  63. package/src/components/RadioButton/RadioButton.tsx +23 -225
  64. package/src/components/index.ts +13 -1
  65. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  66. package/src/icons/registry.ts +1 -1
@@ -1,30 +1,34 @@
1
- import React from 'react'
1
+ import React, { useMemo } from 'react'
2
2
  import { View, Text, type ViewStyle, type TextStyle, type StyleProp } from 'react-native'
3
3
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
4
  import { useTokens } from '../../design-tokens/JFSThemeProvider'
5
5
  import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils'
6
6
  import Button from '../Button/Button'
7
+ import Icon from '../../icons/Icon'
8
+
9
+ export type NudgeType = 'stacked-prominent' | 'stacked-detailed' | 'inline-compact'
7
10
 
8
11
  export type NudgeProps = {
9
12
  /**
10
- * Controls the layout variant.
11
- * - "Default": horizontal layout with optional start icon and content (title/body/button or children slot)
12
- * - "Variant2": vertical layout with header (icon + title) and children slot below
13
+ * Controls the layout type.
14
+ * - "stacked-prominent": icon + title/body/button content
15
+ * - "inline-compact": icon + body/button in one row
16
+ * - "stacked-detailed": header + children detail slot
13
17
  */
14
- variant?: 'Default' | 'Variant2';
18
+ type?: NudgeType;
15
19
  /** Title text displayed in the nudge */
16
20
  title?: string;
17
- /** Body text displayed below the title (Default variant only, when no children are provided) */
21
+ /** Body text displayed in prominent and compact types when no children are provided */
18
22
  body?: string;
19
- /** Label for the default button (Default variant only, when no children are provided) */
23
+ /** Label for the default button when no buttonSlot is provided */
20
24
  buttonLabel?: string;
21
25
  /** Callback for the default button press */
22
26
  onPressButton?: () => void;
23
- /** Custom button slot (Default variant only, overrides buttonLabel/onPressButton) */
27
+ /** Custom button slot, overrides buttonLabel/onPressButton */
24
28
  buttonSlot?: React.ReactNode;
25
- /** Optional leading slot for an icon/element. Pass null or omit to hide. */
26
- startSlot?: React.ReactNode;
27
- /** Content slot — overrides the default title/body/button content */
29
+ /** Optional leading slot. Omit for the token-driven sparkle icon, pass null/false to hide. */
30
+ startSlot?: React.ReactNode | false;
31
+ /** Content slot — overrides default content, or provides detailed list content */
28
32
  children?: React.ReactNode;
29
33
  /** Mode configuration for design token resolution */
30
34
  modes?: Record<string, any>;
@@ -32,21 +36,32 @@ export type NudgeProps = {
32
36
  style?: StyleProp<ViewStyle>;
33
37
  };
34
38
 
35
- function Nudge({
36
- variant = 'Default',
37
- title = 'Split payment',
38
- body = 'Split this transaction into installments',
39
- buttonLabel = 'Button',
40
- onPressButton,
41
- buttonSlot,
42
- startSlot,
43
- children,
44
- modes: propModes = EMPTY_MODES,
45
- style,
46
- }: NudgeProps) {
47
- const { modes: globalModes } = useTokens()
48
- const modes = { ...globalModes, ...propModes }
39
+ interface NudgeTokens {
40
+ containerBaseStyle: ViewStyle;
41
+ prominentContainerStyle: ViewStyle;
42
+ compactContainerStyle: ViewStyle;
43
+ detailedContainerStyle: ViewStyle;
44
+ contentStyle: ViewStyle;
45
+ compactOuterContentStyle: ViewStyle;
46
+ compactContentWrapStyle: ViewStyle;
47
+ textWrapStyle: ViewStyle;
48
+ compactTextWrapStyle: ViewStyle;
49
+ headerStyle: ViewStyle;
50
+ detailSlotStyle: ViewStyle;
51
+ titleTextStyle: TextStyle;
52
+ bodyTextStyle: TextStyle;
53
+ iconColor: string;
54
+ iconSize: number;
55
+ startSlotGap: number;
56
+ }
57
+
58
+ function toFontWeight(value: unknown, fallback: TextStyle['fontWeight']): TextStyle['fontWeight'] {
59
+ if (typeof value === 'number') return value.toString() as TextStyle['fontWeight']
60
+ if (typeof value === 'string') return value as TextStyle['fontWeight']
61
+ return fallback
62
+ }
49
63
 
64
+ function resolveNudgeTokens(modes: Record<string, any>): NudgeTokens {
50
65
  const background = getVariableByName('nudge/background', modes) || '#f5f5f5'
51
66
  const radius = getVariableByName('nudge/radius', modes) || 12
52
67
  const paddingH = getVariableByName('nudge/padding/horizontal', modes) || 12
@@ -55,96 +70,221 @@ function Nudge({
55
70
 
56
71
  const titleColor = getVariableByName('nudge/title/color', modes) || '#0d0d0f'
57
72
  const titleFontSize = getVariableByName('nudge/title/fontSize', modes) || 14
58
- const titleFontFamily = getVariableByName('nudge/title/fontFamily', modes) || 'JioType Var'
73
+ const titleFontFamily = getVariableByName('nudge/title/fontFamily', modes) || 'System'
59
74
  const titleLineHeight = getVariableByName('nudge/title/lineHeight', modes) || 15
60
- const titleFontWeightRaw = getVariableByName('nudge/title/fontWeight', modes) || 700
61
- const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw
75
+ const titleFontWeight = toFontWeight(getVariableByName('nudge/title/fontWeight', modes), '700')
62
76
 
63
77
  const bodyColor = getVariableByName('nudge/body/color', modes) || '#1a1c1f'
64
78
  const bodyFontSize = getVariableByName('nudge/body/fontSize', modes) || 12
65
- const bodyFontFamily = getVariableByName('nudge/body/fontFamily', modes) || 'JioType Var'
79
+ const bodyFontFamily = getVariableByName('nudge/body/fontFamily', modes) || 'System'
66
80
  const bodyLineHeight = getVariableByName('nudge/body/lineHeight', modes) || 16
67
- const bodyFontWeightRaw = getVariableByName('nudge/body/fontWeight', modes) || 500
68
- const bodyFontWeight = typeof bodyFontWeightRaw === 'number' ? bodyFontWeightRaw.toString() : bodyFontWeightRaw
81
+ const bodyFontWeight = toFontWeight(getVariableByName('nudge/body/fontWeight', modes), '500')
69
82
 
70
83
  const textGap = getVariableByName('nudge/text/gap', modes) || 4
71
84
  const contentGap = getVariableByName('nudge/content/gap', modes) || 8
72
85
  const contentMinHeight = getVariableByName('nudge/content/minHeight', modes) || 20
86
+ const startSlotGap = getVariableByName('nudge/startSlot/gap', modes) || 4
73
87
 
74
- const containerStyle: ViewStyle = {
75
- backgroundColor: background,
76
- borderRadius: radius,
77
- paddingHorizontal: paddingH,
78
- paddingVertical: paddingV,
79
- gap,
80
- overflow: 'hidden',
81
- ...(variant === 'Variant2'
82
- ? { flexDirection: 'column', alignItems: 'flex-start' }
83
- : { flexDirection: 'row', alignItems: 'flex-start' }),
88
+ return {
89
+ containerBaseStyle: {
90
+ backgroundColor: background as string,
91
+ borderRadius: radius as number,
92
+ paddingHorizontal: paddingH as number,
93
+ paddingVertical: paddingV as number,
94
+ gap: gap as number,
95
+ overflow: 'hidden',
96
+ },
97
+ prominentContainerStyle: {
98
+ flexDirection: 'row',
99
+ alignItems: 'flex-start',
100
+ },
101
+ compactContainerStyle: {
102
+ flexDirection: 'row',
103
+ alignItems: 'center',
104
+ },
105
+ detailedContainerStyle: {
106
+ flexDirection: 'column',
107
+ alignItems: 'flex-start',
108
+ },
109
+ contentStyle: {
110
+ flex: 1,
111
+ minWidth: 1,
112
+ minHeight: contentMinHeight as number,
113
+ justifyContent: 'center',
114
+ overflow: 'hidden',
115
+ },
116
+ compactOuterContentStyle: {
117
+ flex: 1,
118
+ minWidth: 1,
119
+ alignSelf: 'stretch',
120
+ justifyContent: 'center',
121
+ },
122
+ compactContentWrapStyle: {
123
+ flexDirection: 'row',
124
+ alignItems: 'center',
125
+ gap: contentGap as number,
126
+ width: '100%',
127
+ },
128
+ textWrapStyle: {
129
+ gap: textGap as number,
130
+ alignItems: 'flex-start',
131
+ width: '100%',
132
+ },
133
+ compactTextWrapStyle: {
134
+ flex: 1,
135
+ minWidth: 1,
136
+ alignItems: 'flex-start',
137
+ },
138
+ headerStyle: {
139
+ flexDirection: 'row',
140
+ alignItems: 'center',
141
+ gap: gap as number,
142
+ width: '100%',
143
+ },
144
+ detailSlotStyle: {
145
+ gap: getVariableByName('slot/gap', modes) || 8,
146
+ width: '100%',
147
+ },
148
+ titleTextStyle: {
149
+ color: titleColor as string,
150
+ fontSize: titleFontSize as number,
151
+ fontFamily: titleFontFamily as string,
152
+ lineHeight: titleLineHeight as number,
153
+ fontWeight: titleFontWeight,
154
+ },
155
+ bodyTextStyle: {
156
+ color: bodyColor as string,
157
+ fontSize: bodyFontSize as number,
158
+ fontFamily: bodyFontFamily as string,
159
+ lineHeight: bodyLineHeight as number,
160
+ fontWeight: bodyFontWeight,
161
+ },
162
+ iconColor: (getVariableByName('appearance/nudge/icon/color', modes) || '#5d00b5') as string,
163
+ iconSize: (getVariableByName('nudge/icon/size', modes) || 20) as number,
164
+ startSlotGap: startSlotGap as number,
84
165
  }
166
+ }
85
167
 
86
- const titleStyle: TextStyle = {
87
- color: titleColor,
88
- fontSize: titleFontSize,
89
- fontFamily: titleFontFamily,
90
- lineHeight: titleLineHeight,
91
- fontWeight: titleFontWeight,
92
- }
168
+ function NudgeImpl({
169
+ type = 'stacked-prominent',
170
+ title = 'Split payment',
171
+ body = 'Split this transaction into installments',
172
+ buttonLabel = 'Button',
173
+ onPressButton,
174
+ buttonSlot,
175
+ startSlot,
176
+ children,
177
+ modes: propModes = EMPTY_MODES,
178
+ style,
179
+ }: NudgeProps) {
180
+ const { modes: globalModes } = useTokens()
181
+ const modes = useMemo(
182
+ () => (globalModes === EMPTY_MODES && propModes === EMPTY_MODES
183
+ ? EMPTY_MODES
184
+ : { ...globalModes, ...propModes }),
185
+ [globalModes, propModes]
186
+ )
93
187
 
94
- const bodyStyle: TextStyle = {
95
- color: bodyColor,
96
- fontSize: bodyFontSize,
97
- fontFamily: bodyFontFamily,
98
- lineHeight: bodyLineHeight,
99
- fontWeight: bodyFontWeight,
100
- }
188
+ const tokens = useMemo(() => resolveNudgeTokens(modes), [modes])
189
+
190
+ const startSlotElement = useMemo(() => {
191
+ if (startSlot === null || startSlot === false) return null
192
+
193
+ if (startSlot !== undefined) {
194
+ const processed = cloneChildrenWithModes(React.Children.toArray(startSlot), modes)
195
+ return processed.length === 1 ? processed[0] : processed
196
+ }
197
+
198
+ return (
199
+ <Icon
200
+ name="ic_ai_sparkle"
201
+ size={tokens.iconSize}
202
+ color={tokens.iconColor}
203
+ accessibilityElementsHidden={true}
204
+ importantForAccessibility="no"
205
+ />
206
+ )
207
+ }, [startSlot, modes, tokens.iconColor, tokens.iconSize])
208
+
209
+ const startSlotWrapper = startSlotElement ? (
210
+ <View style={{ gap: tokens.startSlotGap, alignItems: 'center' }}>
211
+ {startSlotElement}
212
+ </View>
213
+ ) : null
101
214
 
102
- const processedStartSlot = startSlot
103
- ? cloneChildrenWithModes(React.Children.toArray(startSlot), modes)
104
- : null
215
+ const processedChildren = useMemo(() => {
216
+ if (!children) return null
217
+ const processed = cloneChildrenWithModes(React.Children.toArray(children), modes)
218
+ return processed.length === 1 ? processed[0] : processed
219
+ }, [children, modes])
105
220
 
106
- const startSlotElement = processedStartSlot && processedStartSlot.length > 0
107
- ? (processedStartSlot.length === 1 ? processedStartSlot[0] : processedStartSlot)
108
- : null
221
+ const buttonElement = buttonSlot
222
+ ? cloneChildrenWithModes(React.Children.toArray(buttonSlot), modes)
223
+ : (
224
+ <Button
225
+ label={buttonLabel}
226
+ modes={modes}
227
+ {...(onPressButton ? { onPress: onPressButton } : {})}
228
+ />
229
+ )
109
230
 
110
- if (variant === 'Variant2') {
231
+ if (type === 'stacked-detailed') {
111
232
  return (
112
- <View style={[containerStyle, style]}>
113
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, width: '100%' }}>
114
- {startSlotElement}
115
- <Text style={[titleStyle, { flex: 1 }]}>{title}</Text>
233
+ <View style={[tokens.containerBaseStyle, tokens.detailedContainerStyle, style]}>
234
+ <View style={tokens.headerStyle}>
235
+ {startSlotWrapper}
236
+ <Text style={[tokens.titleTextStyle, { flex: 1 }]}>{title}</Text>
116
237
  </View>
117
238
 
118
- {children ? (
119
- cloneChildrenWithModes(React.Children.toArray(children), modes)
239
+ {processedChildren ? (
240
+ <View style={tokens.detailSlotStyle}>
241
+ {processedChildren}
242
+ </View>
120
243
  ) : null}
121
244
  </View>
122
245
  )
123
246
  }
124
247
 
125
- const defaultContent = (
126
- <View style={{ gap: contentGap, alignItems: 'flex-start', width: '100%' }}>
127
- <View style={{ gap: textGap, alignItems: 'flex-start', width: '100%' }}>
128
- <Text style={titleStyle}>{title}</Text>
129
- <Text style={bodyStyle}>{body}</Text>
248
+ if (type === 'inline-compact') {
249
+ return (
250
+ <View style={[tokens.containerBaseStyle, tokens.compactContainerStyle, style]}>
251
+ {startSlotWrapper}
252
+
253
+ <View style={tokens.compactOuterContentStyle}>
254
+ {processedChildren || (
255
+ <View style={tokens.compactContentWrapStyle}>
256
+ <View style={tokens.compactTextWrapStyle}>
257
+ <Text style={tokens.bodyTextStyle}>{body}</Text>
258
+ </View>
259
+ {buttonElement}
260
+ </View>
261
+ )}
262
+ </View>
263
+ </View>
264
+ )
265
+ }
266
+
267
+ const prominentContent = (
268
+ <View style={{ gap: tokens.compactContentWrapStyle.gap as number, alignItems: 'flex-start', width: '100%' }}>
269
+ <View style={tokens.textWrapStyle}>
270
+ <Text style={tokens.titleTextStyle}>{title}</Text>
271
+ <Text style={tokens.bodyTextStyle}>{body}</Text>
130
272
  </View>
131
- {buttonSlot
132
- ? cloneChildrenWithModes(React.Children.toArray(buttonSlot), modes)
133
- : <Button label={buttonLabel} onPress={onPressButton} modes={modes} />}
273
+ {buttonElement}
134
274
  </View>
135
275
  )
136
276
 
137
277
  return (
138
- <View style={[containerStyle, style]}>
139
- {startSlotElement}
278
+ <View style={[tokens.containerBaseStyle, tokens.prominentContainerStyle, style]}>
279
+ {startSlotWrapper}
140
280
 
141
- <View style={{ flex: 1, minWidth: 1, minHeight: contentMinHeight, justifyContent: 'center', overflow: 'hidden' }}>
142
- {children
143
- ? cloneChildrenWithModes(React.Children.toArray(children), modes)
144
- : defaultContent}
281
+ <View style={tokens.contentStyle}>
282
+ {processedChildren || prominentContent}
145
283
  </View>
146
284
  </View>
147
285
  )
148
286
  }
149
287
 
288
+ const Nudge = React.memo(NudgeImpl)
289
+
150
290
  export default Nudge
@@ -0,0 +1,227 @@
1
+ import React, { useMemo, useState } from 'react'
2
+ import {
3
+ Pressable,
4
+ View,
5
+ StyleSheet,
6
+ Platform,
7
+ ViewStyle,
8
+ DimensionValue,
9
+ StyleProp,
10
+ } from 'react-native'
11
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
12
+ import { EMPTY_MODES } from '../../utils/react-utils'
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Props
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface RadioProps {
19
+ /**
20
+ * Whether the radio is selected.
21
+ */
22
+ selected?: boolean
23
+ /**
24
+ * Whether the radio is disabled.
25
+ */
26
+ disabled?: boolean
27
+ /**
28
+ * Function to call when the radio is pressed.
29
+ */
30
+ onPress?: () => void
31
+ /**
32
+ * Modes object for design-token resolution.
33
+ */
34
+ modes?: Record<string, any>
35
+ /**
36
+ * Custom style for the radio container.
37
+ */
38
+ style?: StyleProp<ViewStyle>
39
+ /**
40
+ * Test ID for testing.
41
+ */
42
+ testID?: string
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Radio
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export function Radio({
50
+ selected = false,
51
+ disabled = false,
52
+ onPress,
53
+ modes = EMPTY_MODES,
54
+ style,
55
+ testID,
56
+ }: RadioProps) {
57
+ // ---- Refs & State ----
58
+ const [hovered, setHovered] = useState(false)
59
+ const [focused, setFocused] = useState(false)
60
+ const [pressed, setPressed] = useState(false)
61
+
62
+ // ---- Dimensions ----
63
+ const widthStr = getVariableByName('radio/width', modes) || '18'
64
+ const heightStr = getVariableByName('radio/height', modes) || '18'
65
+ const selectorSizeStr = getVariableByName('radio/selector/size', modes) || '10'
66
+
67
+ const width = parseFloat(widthStr?.toString() || '18')
68
+ const height = parseFloat(heightStr?.toString() || '18')
69
+ const selectorSize = parseFloat(selectorSizeStr?.toString() || '10')
70
+
71
+ // ---- State Logic ----
72
+ // Priority: Disabled -> Focused -> Hover/Pressed -> Idle
73
+ // Note: Design treats Active (Pressed) similar to Selected for some styles,
74
+ // but usually in Radios, Pressed is a transient state.
75
+ // We will map:
76
+ // - Disabled -> 'disabled'
77
+ // - Focused -> 'focus'
78
+ // - Hovered -> 'hover'
79
+ // - Idle -> 'idle'
80
+
81
+ // We handle `selected` as a separate dimension derived from state.
82
+
83
+ let visualState = 'idle'
84
+ if (disabled) {
85
+ visualState = 'disabled'
86
+ } else if (focused) {
87
+ visualState = 'focus'
88
+ } else if (hovered || pressed) {
89
+ visualState = 'hover'
90
+ }
91
+
92
+ // Construct token paths based on state + selected
93
+ let prefix = `radio/${visualState}`
94
+ if (visualState === 'idle' && selected) {
95
+ prefix = `radio/selected`
96
+ } else if (selected) {
97
+ prefix = `radio/${visualState}Selected`
98
+ }
99
+
100
+ // ---- Colors & Border ----
101
+
102
+ const resolveColor = (path: string, fallback: string) => {
103
+ return getVariableByName(path, modes) || fallback
104
+ }
105
+
106
+ // Background Color
107
+ let bgColorVar = `${prefix}/background/color`
108
+ // Fix for disabledSelected weirdness if needed
109
+ if (visualState === 'disabled' && selected) {
110
+ // Check specific path from dump: `radio/disabledSelected/background`
111
+ if (!getVariableByName(`${prefix}/background/color`, modes)) {
112
+ bgColorVar = `${prefix}/background`
113
+ }
114
+ }
115
+
116
+ // Border Color
117
+ let borderColorVar = `${prefix}/border/color`
118
+
119
+ // Border Width
120
+ let borderWidthVar = `${prefix}/border/size`
121
+ // Fix for huge path: `radio/disabled/radio/disabled/border/size`
122
+ if (visualState === 'disabled' && !selected) {
123
+ if (getVariableByName('radio/disabled/radio/disabled/border/size', modes)) {
124
+ borderWidthVar = 'radio/disabled/radio/disabled/border/size'
125
+ }
126
+ }
127
+
128
+ // Selector Color
129
+ let selectorBgVar = `${prefix}/selector/background/color`
130
+ if (!selected) {
131
+ selectorBgVar = 'transparent'
132
+ }
133
+
134
+ // Shadows (Glow)
135
+ let shadowSizeVar = `${prefix}/boxShadow/size`
136
+ let shadowColorVar = `${prefix}/shadow/color`
137
+
138
+ // Resolve Values
139
+ const backgroundColor = resolveColor(bgColorVar, 'transparent')
140
+ const borderColor = resolveColor(borderColorVar, 'transparent')
141
+ const borderWidth = parseFloat(getVariableByName(borderWidthVar, modes)?.toString() || '1')
142
+ const selectorColor = resolveColor(selectorBgVar, 'transparent')
143
+
144
+ const shadowSize = parseFloat(getVariableByName(shadowSizeVar, modes)?.toString() || '0')
145
+ const shadowColor = resolveColor(shadowColorVar, 'transparent')
146
+
147
+ // Styles
148
+ const containerStyle: any = {
149
+ width,
150
+ height,
151
+ borderRadius: width / 2, // 9999px -> circle
152
+ borderWidth,
153
+ borderColor,
154
+ backgroundColor,
155
+ justifyContent: 'center',
156
+ alignItems: 'center',
157
+ // Web shadow (ring)
158
+ ...(Platform.OS === 'web' && shadowSize > 0 ? {
159
+ boxShadow: `0px 0px 0px ${shadowSize}px ${shadowColor}`,
160
+ } : {}),
161
+ }
162
+
163
+ const selectorStyle: ViewStyle = {
164
+ width: selectorSize,
165
+ height: selectorSize,
166
+ borderRadius: selectorSize / 2,
167
+ backgroundColor: selectorColor,
168
+ }
169
+
170
+ // Dummy block for token extraction (static analysis)
171
+ if (false as boolean) {
172
+ getVariableByName('radio/idle/background/color')
173
+ getVariableByName('radio/idle/border/color')
174
+ getVariableByName('radio/selector/size')
175
+ getVariableByName('radio/width')
176
+ getVariableByName('radio/height')
177
+ getVariableByName('radio/background/color')
178
+ getVariableByName('radio/hover/background/color')
179
+ getVariableByName('radio/hover/border/color')
180
+ getVariableByName('radio/hover/boxShadow/size')
181
+ getVariableByName('radio/hover/shadow/color')
182
+ getVariableByName('radio/selected/background/color')
183
+ getVariableByName('radio/selected/border/color')
184
+ getVariableByName('radio/selected/selector/background/color')
185
+ getVariableByName('radio/hoverSelected/background/color')
186
+ getVariableByName('radio/hoverSelected/border/color')
187
+ getVariableByName('radio/hoverSelected/boxShadow/size')
188
+ getVariableByName('radio/hoverSelected/shadow/color')
189
+ getVariableByName('radio/hoverSelected/selector/background/color')
190
+ getVariableByName('radio/focus/background/color')
191
+ getVariableByName('radio/focus/border/color')
192
+ getVariableByName('radio/focus/border/size')
193
+ getVariableByName('radio/focus/boxShadow/size')
194
+ getVariableByName('radio/focus/shadow/color')
195
+ getVariableByName('radio/focusSelected/background/color')
196
+ getVariableByName('radio/focusSelected/selector/background/color')
197
+ getVariableByName('radio/focusSelected/border/size')
198
+ getVariableByName('radio/disabled/radio/disabled/border/size')
199
+ getVariableByName('radio/disabled/background/color')
200
+ getVariableByName('radio/disabled/border/color')
201
+ getVariableByName('radio/disabledSelected/selector/background/color')
202
+ getVariableByName('radio/disabledSelected/background')
203
+ getVariableByName('radio/disabledSelected/border/color')
204
+ }
205
+
206
+ return (
207
+ <Pressable
208
+ testID={testID}
209
+ disabled={disabled}
210
+ onPress={onPress}
211
+ onHoverIn={() => setHovered(true)}
212
+ onHoverOut={() => setHovered(false)}
213
+ onFocus={() => setFocused(true)}
214
+ onBlur={() => setFocused(false)}
215
+ onPressIn={() => setPressed(true)}
216
+ onPressOut={() => setPressed(false)}
217
+ style={({ pressed: isPressed }) => [
218
+ containerStyle,
219
+ style,
220
+ ]}
221
+ >
222
+ <View style={selectorStyle} />
223
+ </Pressable>
224
+ )
225
+ }
226
+
227
+ export default Radio