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.
- package/CHANGELOG.md +11 -0
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +319 -135
- package/lib/commonjs/components/Badge/Badge.js +20 -1
- package/lib/commonjs/components/Button/Button.js +139 -47
- package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
- package/lib/commonjs/components/Card/Card.js +151 -112
- package/lib/commonjs/components/Card/assets/gold-logo-mark.png +0 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +97 -63
- package/lib/commonjs/components/CategoryCard/assets/sample.png +0 -0
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +59 -10
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +132 -49
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +56 -14
- package/lib/commonjs/components/IconCapsule/IconCapsule.js +1 -1
- package/lib/commonjs/components/MetricData/MetricData.js +26 -13
- package/lib/commonjs/components/MoneyValue/MoneyValue.js +1 -1
- package/lib/commonjs/components/Nudge/Nudge.js +1 -1
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +47 -91
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
- package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +1 -1
- package/lib/commonjs/components/Slider/Slider.js +3 -3
- package/lib/commonjs/components/TextInput/TextInput.js +26 -8
- package/lib/commonjs/components/Tooltip/Tooltip.js +3 -5
- package/lib/commonjs/components/index.js +7 -1
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +13 -13
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/ActionFooter/ActionFooter.js +322 -138
- package/lib/module/components/Badge/Badge.js +20 -1
- package/lib/module/components/Button/Button.js +140 -48
- package/lib/module/components/ButtonGroup/ButtonGroup.js +1 -1
- package/lib/module/components/Card/Card.js +152 -114
- package/lib/module/components/Card/assets/gold-logo-mark.png +0 -0
- package/lib/module/components/CategoryCard/CategoryCard.js +97 -63
- package/lib/module/components/CategoryCard/assets/sample.png +0 -0
- package/lib/module/components/CheckboxItem/CheckboxItem.js +59 -10
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +135 -52
- package/lib/module/components/HelloJioInput/HelloJioInput.js +56 -14
- package/lib/module/components/IconCapsule/IconCapsule.js +1 -1
- package/lib/module/components/MetricData/MetricData.js +26 -14
- package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
- package/lib/module/components/Nudge/Nudge.js +1 -1
- package/lib/module/components/PdpCcCard/PdpCcCard.js +48 -92
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
- package/lib/module/components/PortfolioHero/PortfolioHero.js +1 -1
- package/lib/module/components/Slider/Slider.js +3 -3
- package/lib/module/components/TextInput/TextInput.js +27 -9
- package/lib/module/components/Tooltip/Tooltip.js +3 -5
- package/lib/module/components/index.js +1 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/design-tokens/figma-modes.generated.js +13 -13
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +50 -21
- package/lib/typescript/src/components/Button/Button.d.ts +24 -1
- package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +2 -2
- package/lib/typescript/src/components/Card/Card.d.ts +54 -25
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +29 -12
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +27 -9
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +37 -12
- package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +1 -1
- package/lib/typescript/src/components/MetricData/MetricData.d.ts +7 -5
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +16 -6
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +9 -2
- package/lib/typescript/src/components/index.d.ts +2 -2
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/ActionFooter/ActionFooter.tsx +375 -141
- package/src/components/Badge/Badge.tsx +21 -1
- package/src/components/Button/Button.tsx +192 -44
- package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
- package/src/components/Card/Card.tsx +370 -212
- package/src/components/Card/assets/gold-logo-mark.png +0 -0
- package/src/components/CategoryCard/CategoryCard.tsx +130 -84
- package/src/components/CategoryCard/assets/sample.png +0 -0
- package/src/components/CheckboxItem/CheckboxItem.tsx +72 -12
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +186 -67
- package/src/components/HelloJioInput/HelloJioInput.tsx +58 -17
- package/src/components/IconCapsule/IconCapsule.tsx +1 -1
- package/src/components/MetricData/MetricData.tsx +39 -22
- package/src/components/MoneyValue/MoneyValue.tsx +1 -1
- package/src/components/Nudge/Nudge.tsx +1 -1
- package/src/components/PdpCcCard/PdpCcCard.tsx +41 -92
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +73 -58
- package/src/components/PortfolioHero/PortfolioHero.tsx +1 -1
- package/src/components/Slider/Slider.tsx +3 -3
- package/src/components/TextInput/TextInput.tsx +26 -9
- package/src/components/Tooltip/Tooltip.tsx +3 -5
- package/src/components/index.ts +9 -2
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/design-tokens/figma-modes.generated.ts +13 -13
- 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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 `
|
|
68
|
-
* "Read more" / "Read less" toggle. Mirrors
|
|
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)** —
|
|
72
|
-
*
|
|
73
|
-
* - **Open (expanded)** —
|
|
74
|
-
*
|
|
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="
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
(getVariableByName('
|
|
143
|
-
const
|
|
144
|
-
(getVariableByName('
|
|
145
|
-
const
|
|
146
|
-
(getVariableByName('
|
|
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
|
|
206
|
+
const itemWrapperStyle: ViewStyle = useMemo(
|
|
165
207
|
() => ({
|
|
166
208
|
flex: isExpanded ? undefined : 1,
|
|
167
209
|
alignSelf: isExpanded ? 'stretch' : 'auto',
|
|
168
210
|
minWidth: 0,
|
|
169
|
-
|
|
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
|
|
223
|
+
[isExpanded]
|
|
174
224
|
)
|
|
175
225
|
|
|
176
|
-
const
|
|
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 ??
|
|
212
|
-
|
|
246
|
+
accessibilityLabel ??
|
|
247
|
+
(typeof label === 'string' && label.length > 0
|
|
248
|
+
? linkLabel
|
|
249
|
+
? `${label} ${linkLabel}`
|
|
250
|
+
: label
|
|
251
|
+
: linkLabel)
|
|
213
252
|
|
|
214
|
-
const
|
|
253
|
+
const buttonLabel = isExpanded ? readLessLabel : readMoreLabel
|
|
254
|
+
const shouldTruncate =
|
|
215
255
|
!disableTruncation && !isExpanded && collapsedLines > 0
|
|
216
|
-
|
|
217
|
-
|
|
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={
|
|
222
|
-
<
|
|
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
|
-
{
|
|
235
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
89
|
-
// `
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
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]:
|
|
397
|
+
[STATE_COLLECTION]: tokenState,
|
|
355
398
|
}),
|
|
356
|
-
[globalModes, propModes,
|
|
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., {"
|
|
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.
|
|
38
|
-
*
|
|
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,
|
|
63
|
-
* resolve from the `metricdata/*` design tokens
|
|
64
|
-
*
|
|
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) ??
|
|
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) ??
|
|
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={
|
|
145
|
-
color={
|
|
146
|
-
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
|
-
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
//
|
|
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/
|
|
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)
|