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.
- package/CHANGELOG.md +49 -0
- package/lib/commonjs/components/CardAdvisory/CardAdvisory.js +203 -0
- package/lib/commonjs/components/CardCTA/CardCTA.js +198 -16
- package/lib/commonjs/components/CardFinancialCondition/CardFinancialCondition.js +213 -0
- package/lib/commonjs/components/Carousel/Carousel.js +9 -7
- package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +147 -0
- package/lib/commonjs/components/CircularProgressBarDoted/CircularProgressBarDoted.js +258 -0
- package/lib/commonjs/components/CircularRating/CircularRating.js +161 -0
- package/lib/commonjs/components/Gauge/Gauge.js +223 -0
- package/lib/commonjs/components/HoldingsCard/HoldingsCard.js +2 -2
- package/lib/commonjs/components/InstitutionBadge/InstitutionBadge.js +132 -0
- package/lib/commonjs/components/ListGroup/ListGroup.js +3 -1
- package/lib/commonjs/components/Nudge/Nudge.js +179 -87
- package/lib/commonjs/components/Radio/Radio.js +194 -0
- package/lib/commonjs/components/RadioButton/RadioButton.js +21 -188
- package/lib/commonjs/components/index.js +56 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CardAdvisory/CardAdvisory.js +197 -0
- package/lib/module/components/CardCTA/CardCTA.js +199 -17
- package/lib/module/components/CardFinancialCondition/CardFinancialCondition.js +207 -0
- package/lib/module/components/Carousel/Carousel.js +9 -7
- package/lib/module/components/CircularProgressBar/CircularProgressBar.js +141 -0
- package/lib/module/components/CircularProgressBarDoted/CircularProgressBarDoted.js +253 -0
- package/lib/module/components/CircularRating/CircularRating.js +155 -0
- package/lib/module/components/Gauge/Gauge.js +217 -0
- package/lib/module/components/HoldingsCard/HoldingsCard.js +2 -2
- package/lib/module/components/InstitutionBadge/InstitutionBadge.js +127 -0
- package/lib/module/components/ListGroup/ListGroup.js +3 -1
- package/lib/module/components/Nudge/Nudge.js +178 -87
- package/lib/module/components/Radio/Radio.js +188 -0
- package/lib/module/components/RadioButton/RadioButton.js +20 -185
- package/lib/module/components/index.js +12 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CardAdvisory/CardAdvisory.d.ts +49 -0
- package/lib/typescript/src/components/CardCTA/CardCTA.d.ts +16 -1
- package/lib/typescript/src/components/CardFinancialCondition/CardFinancialCondition.d.ts +50 -0
- package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +27 -0
- package/lib/typescript/src/components/CircularProgressBarDoted/CircularProgressBarDoted.d.ts +48 -0
- package/lib/typescript/src/components/CircularRating/CircularRating.d.ts +49 -0
- package/lib/typescript/src/components/Gauge/Gauge.d.ts +53 -0
- package/lib/typescript/src/components/InstitutionBadge/InstitutionBadge.d.ts +30 -0
- package/lib/typescript/src/components/Nudge/Nudge.d.ts +14 -11
- package/lib/typescript/src/components/Radio/Radio.d.ts +30 -0
- package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +20 -28
- package/lib/typescript/src/components/index.d.ts +13 -1
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/CardAdvisory/CardAdvisory.tsx +283 -0
- package/src/components/CardCTA/CardCTA.tsx +236 -13
- package/src/components/CardFinancialCondition/CardFinancialCondition.tsx +366 -0
- package/src/components/Carousel/Carousel.tsx +14 -6
- package/src/components/CircularProgressBar/CircularProgressBar.tsx +190 -0
- package/src/components/CircularProgressBarDoted/CircularProgressBarDoted.tsx +357 -0
- package/src/components/CircularRating/CircularRating.tsx +241 -0
- package/src/components/Gauge/Gauge.tsx +303 -0
- package/src/components/HoldingsCard/HoldingsCard.tsx +2 -2
- package/src/components/InstitutionBadge/InstitutionBadge.tsx +216 -0
- package/src/components/ListGroup/ListGroup.tsx +3 -1
- package/src/components/Nudge/Nudge.tsx +222 -82
- package/src/components/Radio/Radio.tsx +227 -0
- package/src/components/RadioButton/RadioButton.tsx +23 -225
- package/src/components/index.ts +13 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- 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
|
|
11
|
-
* - "
|
|
12
|
-
* - "
|
|
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
|
-
|
|
18
|
+
type?: NudgeType;
|
|
15
19
|
/** Title text displayed in the nudge */
|
|
16
20
|
title?: string;
|
|
17
|
-
/** Body text displayed
|
|
21
|
+
/** Body text displayed in prominent and compact types when no children are provided */
|
|
18
22
|
body?: string;
|
|
19
|
-
/** Label for the default button
|
|
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
|
|
27
|
+
/** Custom button slot, overrides buttonLabel/onPressButton */
|
|
24
28
|
buttonSlot?: React.ReactNode;
|
|
25
|
-
/** Optional leading slot for
|
|
26
|
-
startSlot?: React.ReactNode;
|
|
27
|
-
/** Content slot — overrides
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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) || '
|
|
73
|
+
const titleFontFamily = getVariableByName('nudge/title/fontFamily', modes) || 'System'
|
|
59
74
|
const titleLineHeight = getVariableByName('nudge/title/lineHeight', modes) || 15
|
|
60
|
-
const
|
|
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) || '
|
|
79
|
+
const bodyFontFamily = getVariableByName('nudge/body/fontFamily', modes) || 'System'
|
|
66
80
|
const bodyLineHeight = getVariableByName('nudge/body/lineHeight', modes) || 16
|
|
67
|
-
const
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
107
|
-
? (
|
|
108
|
-
:
|
|
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 (
|
|
231
|
+
if (type === 'stacked-detailed') {
|
|
111
232
|
return (
|
|
112
|
-
<View style={[
|
|
113
|
-
<View style={
|
|
114
|
-
{
|
|
115
|
-
<Text style={[
|
|
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
|
-
{
|
|
119
|
-
|
|
239
|
+
{processedChildren ? (
|
|
240
|
+
<View style={tokens.detailSlotStyle}>
|
|
241
|
+
{processedChildren}
|
|
242
|
+
</View>
|
|
120
243
|
) : null}
|
|
121
244
|
</View>
|
|
122
245
|
)
|
|
123
246
|
}
|
|
124
247
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<View style={
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
{
|
|
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={[
|
|
139
|
-
{
|
|
278
|
+
<View style={[tokens.containerBaseStyle, tokens.prominentContainerStyle, style]}>
|
|
279
|
+
{startSlotWrapper}
|
|
140
280
|
|
|
141
|
-
<View style={
|
|
142
|
-
{
|
|
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
|