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
|
@@ -26,16 +26,24 @@ import Badge from '../Badge/Badge'
|
|
|
26
26
|
import Image from '../Image/Image'
|
|
27
27
|
import type { Modes } from '../../design-tokens'
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/** Canonical Figma collection that drives idle vs active chrome. */
|
|
30
|
+
const CARD_TAB_STATE = 'Card Tab State'
|
|
31
|
+
/**
|
|
32
|
+
* Legacy collection name used by earlier CategoryCard builds. Still accepted on
|
|
33
|
+
* `modes` so existing callers keep resolving the correct Active/Default tokens.
|
|
34
|
+
*/
|
|
35
|
+
const LEGACY_CARD_TAB_STATE = 'Category Card State'
|
|
36
|
+
|
|
30
37
|
const BADGE_OVERLAP = 11
|
|
31
38
|
const DEFAULT_CARD_WIDTH = 60
|
|
39
|
+
const DEFAULT_IMAGE_ASSET = require('./assets/sample.png')
|
|
32
40
|
|
|
33
41
|
// Default modes from the Figma CategoryCard component. Overridable via `modes`.
|
|
34
42
|
const CATEGORY_CARD_DEFAULT_MODES = Object.freeze({
|
|
35
43
|
AppearanceBrand: 'Tertiary',
|
|
36
44
|
'Badge Size': 'Small',
|
|
37
45
|
Context4: 'Badge',
|
|
38
|
-
|
|
46
|
+
Radius: 'None',
|
|
39
47
|
})
|
|
40
48
|
|
|
41
49
|
const toNumber = (value: unknown, fallback: number): number => {
|
|
@@ -58,25 +66,67 @@ const toFontWeight = (
|
|
|
58
66
|
return fallback
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Maps any accepted state token (`Default` / `Active`, plus legacy `Idle`) onto
|
|
71
|
+
* the canonical `Card Tab State` mode names used by the variable resolver.
|
|
72
|
+
*/
|
|
73
|
+
const normalizeCardTabState = (
|
|
74
|
+
value: unknown
|
|
75
|
+
): 'Default' | 'Active' | undefined => {
|
|
76
|
+
if (value === 'Active') return 'Active'
|
|
77
|
+
if (value === 'Default' || value === 'Idle') return 'Default'
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolves whether the card should render in the Active (gold stroke) state.
|
|
83
|
+
*
|
|
84
|
+
* Precedence (backward compatible):
|
|
85
|
+
* 1. Explicit `active` boolean prop (existing public API)
|
|
86
|
+
* 2. `modes['Card Tab State']` / legacy `modes['Category Card State']`
|
|
87
|
+
* 3. Default → idle (`Default`)
|
|
88
|
+
*/
|
|
89
|
+
const resolveIsActive = (
|
|
90
|
+
active: boolean | undefined,
|
|
91
|
+
propModes: Modes
|
|
92
|
+
): boolean => {
|
|
93
|
+
if (typeof active === 'boolean') return active
|
|
94
|
+
const fromModes = normalizeCardTabState(
|
|
95
|
+
propModes[CARD_TAB_STATE] ?? propModes[LEGACY_CARD_TAB_STATE]
|
|
96
|
+
)
|
|
97
|
+
return fromModes === 'Active'
|
|
98
|
+
}
|
|
99
|
+
|
|
61
100
|
export type CategoryCardProps = SafePressableProps & {
|
|
62
101
|
/** Bottom label (e.g. `"Popular"`). */
|
|
63
102
|
label?: string
|
|
64
103
|
/**
|
|
65
|
-
* Image shown in the card centre. Ignored when `imageSlot` is
|
|
66
|
-
* Accepts a remote URL string, `{ uri }` object, or `require()`-ed
|
|
104
|
+
* Image shown in the card centre. Ignored when `imageSlot` / `children` is
|
|
105
|
+
* provided. Accepts a remote URL string, `{ uri }` object, or `require()`-ed
|
|
106
|
+
* asset.
|
|
67
107
|
*/
|
|
68
108
|
imageSource?: ImageSourcePropType | string
|
|
69
109
|
/**
|
|
70
|
-
* Slot replacing the default `Image
|
|
71
|
-
* components inherit theming.
|
|
110
|
+
* Slot replacing the default `Image` (Figma Image instance / Code Connect).
|
|
111
|
+
* Receives `modes` recursively so inner components inherit theming.
|
|
112
|
+
* Prefer this or `children` when swapping the media.
|
|
72
113
|
*/
|
|
73
114
|
imageSlot?: React.ReactNode
|
|
115
|
+
/**
|
|
116
|
+
* Alias for {@link imageSlot} — the Figma image slot. When both are set,
|
|
117
|
+
* `imageSlot` wins so existing `imageSlot` callers stay authoritative.
|
|
118
|
+
*/
|
|
119
|
+
children?: React.ReactNode
|
|
74
120
|
/**
|
|
75
121
|
* Optional badge label rendered as an overlapping pill at the top edge.
|
|
76
122
|
* Omit or pass `undefined` to hide the badge.
|
|
77
123
|
*/
|
|
78
124
|
badge?: string
|
|
79
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Whether the card is in the selected / active state (gold stroke).
|
|
127
|
+
* When omitted, falls back to `modes['Card Tab State'] === 'Active'`
|
|
128
|
+
* (and the legacy `modes['Category Card State'] === 'Active'` alias).
|
|
129
|
+
*/
|
|
80
130
|
active?: boolean
|
|
81
131
|
/**
|
|
82
132
|
* Fixed width of the card in dp. Defaults to `60` per the Figma spec.
|
|
@@ -84,15 +134,16 @@ export type CategoryCardProps = SafePressableProps & {
|
|
|
84
134
|
width?: number
|
|
85
135
|
/**
|
|
86
136
|
* When `true`, disables text truncation: the trailing ellipsis (`…`) is never
|
|
87
|
-
* shown and any `numberOfLines` clamp is ignored
|
|
88
|
-
*
|
|
89
|
-
*
|
|
137
|
+
* shown and any `numberOfLines` clamp is ignored. Defaults to `true` (same
|
|
138
|
+
* overflow behaviour as {@link Text}'s `disableTruncation`). Pass `false` to
|
|
139
|
+
* restore single-line ellipsis clamping.
|
|
90
140
|
*/
|
|
91
141
|
disableTruncation?: boolean
|
|
92
142
|
/**
|
|
93
143
|
* When `true`, forces the label onto a single line with no wrapping (implies
|
|
94
144
|
* `disableTruncation`). On web the line overflows horizontally instead of
|
|
95
|
-
* wrapping or truncating.
|
|
145
|
+
* wrapping or truncating. Defaults to `true` — same API as {@link Text}'s
|
|
146
|
+
* `singleLine`. Pass `false` to allow wrapping.
|
|
96
147
|
*/
|
|
97
148
|
singleLine?: boolean
|
|
98
149
|
/** Press handler — the card is always a `Pressable`. */
|
|
@@ -114,70 +165,69 @@ export type CategoryCardProps = SafePressableProps & {
|
|
|
114
165
|
interface CategoryCardTokens {
|
|
115
166
|
containerStyle: ViewStyle
|
|
116
167
|
labelStyle: TextStyle
|
|
117
|
-
|
|
168
|
+
imageWidth: number
|
|
169
|
+
imageHeight: number
|
|
118
170
|
imageRadius: number
|
|
119
171
|
}
|
|
120
172
|
|
|
121
|
-
function resolveCategoryCardTokens(
|
|
122
|
-
modes
|
|
123
|
-
active: boolean
|
|
124
|
-
): CategoryCardTokens {
|
|
125
|
-
const idleModes = { ...modes, [CATEGORY_CARD_STATE]: 'Idle' }
|
|
126
|
-
const activeModes = { ...modes, [CATEGORY_CARD_STATE]: 'Active' }
|
|
127
|
-
const resolvedModes = active ? activeModes : idleModes
|
|
128
|
-
|
|
129
|
-
const gap = toNumber(getVariableByName('cardTab/gap', resolvedModes), 6)
|
|
173
|
+
function resolveCategoryCardTokens(modes: Modes): CategoryCardTokens {
|
|
174
|
+
const gap = toNumber(getVariableByName('cardTab/gap', modes), 6)
|
|
130
175
|
const paddingHorizontal = toNumber(
|
|
131
|
-
getVariableByName('cardTab/padding/horizontal',
|
|
176
|
+
getVariableByName('cardTab/padding/horizontal', modes),
|
|
132
177
|
8
|
|
133
178
|
)
|
|
134
179
|
const paddingVertical = toNumber(
|
|
135
|
-
getVariableByName('cardTab/padding/vertical',
|
|
180
|
+
getVariableByName('cardTab/padding/vertical', modes),
|
|
136
181
|
8
|
|
137
182
|
)
|
|
138
|
-
const radius = toNumber(getVariableByName('cardTab/radius',
|
|
183
|
+
const radius = toNumber(getVariableByName('cardTab/radius', modes), 8)
|
|
139
184
|
const strokeWidth = toNumber(
|
|
140
|
-
getVariableByName('cardTab/strokeWidth',
|
|
185
|
+
getVariableByName('cardTab/strokeWidth', modes),
|
|
141
186
|
1
|
|
142
187
|
)
|
|
188
|
+
// Active → gold (`#cea15a`); Default → grey (`#f5f5f5`). Both resolve through
|
|
189
|
+
// `Card Tab State` on `cardTab/stroke/color`.
|
|
143
190
|
const strokeColor =
|
|
144
|
-
(getVariableByName('cardTab/stroke/color',
|
|
191
|
+
(getVariableByName('cardTab/stroke/color', modes) as string | null) ??
|
|
145
192
|
'#f5f5f5'
|
|
146
193
|
|
|
147
|
-
|
|
148
|
-
|
|
194
|
+
// Both Idle and Active use a white surface — selection is stroke-only.
|
|
195
|
+
const backgroundColor =
|
|
196
|
+
(getVariableByName('cardTab/background/color', modes) as string | null) ??
|
|
149
197
|
'#ffffff'
|
|
150
|
-
const activeBackground =
|
|
151
|
-
(getVariableByName('cardTab/background/color', activeModes) as string | null) ??
|
|
152
|
-
'#f5f5f5'
|
|
153
|
-
const backgroundColor = active ? activeBackground : idleBackground
|
|
154
198
|
|
|
155
|
-
const
|
|
156
|
-
getVariableByName('cardTab/image/width',
|
|
199
|
+
const imageWidth = toNumber(
|
|
200
|
+
getVariableByName('cardTab/image/width', modes),
|
|
157
201
|
36
|
|
158
202
|
)
|
|
159
|
-
const
|
|
160
|
-
getVariableByName('image/
|
|
161
|
-
|
|
203
|
+
const imageHeight = toNumber(
|
|
204
|
+
getVariableByName('cardTab/image/height', modes),
|
|
205
|
+
imageWidth
|
|
162
206
|
)
|
|
207
|
+
const imageRadius = toNumber(getVariableByName('image/radius', modes), 0)
|
|
163
208
|
|
|
209
|
+
// Prefer component-scoped `cardTab/label/*` (Figma bindings). Do not fall
|
|
210
|
+
// back to bare `label/fontWeight` / `label/lineHeight` — those currently
|
|
211
|
+
// resolve from an unrelated Donut Chart collection (400 / 15.6).
|
|
164
212
|
const fontFamily =
|
|
165
|
-
(getVariableByName('label/fontFamily',
|
|
213
|
+
(getVariableByName('cardTab/label/fontFamily', modes) as string | null) ??
|
|
214
|
+
(getVariableByName('label/fontFamily', modes) as string | null) ??
|
|
166
215
|
'JioType Var'
|
|
167
216
|
const fontSize = toNumber(
|
|
168
|
-
getVariableByName('label/fontSize',
|
|
217
|
+
getVariableByName('cardTab/label/fontSize', modes) ??
|
|
218
|
+
getVariableByName('label/fontSize', modes),
|
|
169
219
|
12
|
|
170
220
|
)
|
|
171
221
|
const lineHeight = toNumber(
|
|
172
|
-
getVariableByName('label/lineHeight',
|
|
173
|
-
|
|
222
|
+
getVariableByName('cardTab/label/lineHeight', modes),
|
|
223
|
+
16
|
|
174
224
|
)
|
|
175
225
|
const fontWeight = toFontWeight(
|
|
176
|
-
getVariableByName('label/fontWeight',
|
|
226
|
+
getVariableByName('cardTab/label/fontWeight', modes),
|
|
177
227
|
'500'
|
|
178
228
|
)
|
|
179
229
|
const labelColor =
|
|
180
|
-
(getVariableByName('cardTab/label/color',
|
|
230
|
+
(getVariableByName('cardTab/label/color', modes) as string | null) ??
|
|
181
231
|
'#000000'
|
|
182
232
|
|
|
183
233
|
return {
|
|
@@ -203,7 +253,8 @@ function resolveCategoryCardTokens(
|
|
|
203
253
|
lineHeight,
|
|
204
254
|
textAlign: 'center',
|
|
205
255
|
},
|
|
206
|
-
|
|
256
|
+
imageWidth,
|
|
257
|
+
imageHeight,
|
|
207
258
|
imageRadius,
|
|
208
259
|
}
|
|
209
260
|
}
|
|
@@ -217,8 +268,13 @@ const disabledOverlayStyle: ViewStyle = { opacity: 0.5 }
|
|
|
217
268
|
* optionally show an overlapping badge at the top edge. Cards are typically
|
|
218
269
|
* laid out in a horizontal `ScrollView` or `HStack`.
|
|
219
270
|
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
271
|
+
* Selection is communicated with a gold stroke (`Card Tab State = Active`);
|
|
272
|
+
* the surface stays white in both Idle and Active. All visual values resolve
|
|
273
|
+
* through the `cardTab/*` design tokens with sensible Figma defaults so the
|
|
274
|
+
* card renders correctly out of the box.
|
|
275
|
+
*
|
|
276
|
+
* The image area is a real slot (`imageSlot` or `children`) matching the Figma
|
|
277
|
+
* Image instance — omit both to keep the default `Image`.
|
|
222
278
|
*
|
|
223
279
|
* @component
|
|
224
280
|
* @param {CategoryCardProps} props
|
|
@@ -227,11 +283,12 @@ function CategoryCard({
|
|
|
227
283
|
label = 'Popular',
|
|
228
284
|
imageSource,
|
|
229
285
|
imageSlot,
|
|
286
|
+
children,
|
|
230
287
|
badge,
|
|
231
|
-
active
|
|
288
|
+
active: activeProp,
|
|
232
289
|
width = DEFAULT_CARD_WIDTH,
|
|
233
|
-
disableTruncation,
|
|
234
|
-
singleLine,
|
|
290
|
+
disableTruncation = true,
|
|
291
|
+
singleLine = true,
|
|
235
292
|
onPress,
|
|
236
293
|
disabled = false,
|
|
237
294
|
modes: propModes = EMPTY_MODES,
|
|
@@ -245,28 +302,26 @@ function CategoryCard({
|
|
|
245
302
|
...rest
|
|
246
303
|
}: CategoryCardProps) {
|
|
247
304
|
const { modes: globalModes } = useTokens()
|
|
305
|
+
const active = resolveIsActive(activeProp, propModes)
|
|
306
|
+
const cardTabState = active ? 'Active' : 'Default'
|
|
307
|
+
|
|
248
308
|
const modes = useMemo(
|
|
249
309
|
() => ({
|
|
250
310
|
...globalModes,
|
|
251
311
|
...CATEGORY_CARD_DEFAULT_MODES,
|
|
252
312
|
...propModes,
|
|
313
|
+
// Canonical collection always wins so stroke/background tokens resolve.
|
|
314
|
+
[CARD_TAB_STATE]: cardTabState,
|
|
315
|
+
// Keep the legacy key in sync for any nested consumers still reading it.
|
|
316
|
+
[LEGACY_CARD_TAB_STATE]: active ? 'Active' : 'Idle',
|
|
253
317
|
}),
|
|
254
|
-
[globalModes, propModes]
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
const resolvedModes = useMemo(
|
|
258
|
-
() => ({
|
|
259
|
-
...modes,
|
|
260
|
-
[CATEGORY_CARD_STATE]: active ? 'Active' : 'Idle',
|
|
261
|
-
}),
|
|
262
|
-
[modes, active]
|
|
318
|
+
[globalModes, propModes, cardTabState, active]
|
|
263
319
|
)
|
|
264
320
|
|
|
265
|
-
const tokens = useMemo(
|
|
266
|
-
() => resolveCategoryCardTokens(resolvedModes, active),
|
|
267
|
-
[resolvedModes, active]
|
|
268
|
-
)
|
|
321
|
+
const tokens = useMemo(() => resolveCategoryCardTokens(modes), [modes])
|
|
269
322
|
|
|
323
|
+
// Same layout merge order as `Text`: consumer `style` first, then layout so
|
|
324
|
+
// single-line / no-truncation overflow always wins.
|
|
270
325
|
const { style: labelLayoutStyle, ...labelTruncation } = useMemo(
|
|
271
326
|
() =>
|
|
272
327
|
resolveTextLayout({
|
|
@@ -278,37 +333,27 @@ function CategoryCard({
|
|
|
278
333
|
[disableTruncation, singleLine]
|
|
279
334
|
)
|
|
280
335
|
|
|
336
|
+
const slotContent = imageSlot ?? children
|
|
281
337
|
const processedImageSlot = useMemo(() => {
|
|
282
|
-
if (
|
|
283
|
-
const processed = cloneChildrenWithModes(
|
|
338
|
+
if (slotContent == null) return null
|
|
339
|
+
const processed = cloneChildrenWithModes(slotContent, modes)
|
|
284
340
|
return processed.length === 1 ? processed[0] : processed
|
|
285
|
-
}, [
|
|
341
|
+
}, [slotContent, modes])
|
|
286
342
|
|
|
287
343
|
const imageNode =
|
|
288
344
|
processedImageSlot ??
|
|
289
|
-
(
|
|
290
|
-
<Image
|
|
291
|
-
imageSource={imageSource}
|
|
292
|
-
width={tokens.imageSize}
|
|
293
|
-
height={tokens.imageSize}
|
|
294
|
-
ratio={1}
|
|
295
|
-
resizeMode="cover"
|
|
296
|
-
borderRadius={tokens.imageRadius}
|
|
297
|
-
accessibilityElementsHidden
|
|
298
|
-
importantForAccessibility="no"
|
|
299
|
-
/>
|
|
300
|
-
) : (
|
|
345
|
+
(
|
|
301
346
|
<Image
|
|
302
|
-
imageSource=
|
|
303
|
-
width={tokens.
|
|
304
|
-
height={tokens.
|
|
347
|
+
imageSource={imageSource ?? DEFAULT_IMAGE_ASSET}
|
|
348
|
+
width={tokens.imageWidth}
|
|
349
|
+
height={tokens.imageHeight}
|
|
305
350
|
ratio={1}
|
|
306
351
|
resizeMode="cover"
|
|
307
352
|
borderRadius={tokens.imageRadius}
|
|
308
353
|
accessibilityElementsHidden
|
|
309
354
|
importantForAccessibility="no"
|
|
310
355
|
/>
|
|
311
|
-
)
|
|
356
|
+
)
|
|
312
357
|
|
|
313
358
|
const a11yLabel = accessibilityLabel ?? (badge ? `${label}, ${badge}` : label)
|
|
314
359
|
|
|
@@ -322,7 +367,8 @@ function CategoryCard({
|
|
|
322
367
|
|
|
323
368
|
const badgeNode = badge ? (
|
|
324
369
|
<View style={BADGE_ANCHOR} pointerEvents="none">
|
|
325
|
-
|
|
370
|
+
{/* Figma Idle caps the badge at 38dp so it stays within the 60dp card. */}
|
|
371
|
+
<Badge label={badge} modes={modes} style={BADGE_STYLE} />
|
|
326
372
|
</View>
|
|
327
373
|
) : null
|
|
328
374
|
|
|
@@ -365,7 +411,7 @@ function CategoryCard({
|
|
|
365
411
|
{badgeNode}
|
|
366
412
|
{imageNode}
|
|
367
413
|
<Text
|
|
368
|
-
style={[tokens.labelStyle, labelWidthStyle,
|
|
414
|
+
style={[tokens.labelStyle, labelWidthStyle, labelStyle, labelLayoutStyle]}
|
|
369
415
|
{...labelTruncation}
|
|
370
416
|
accessibilityElementsHidden
|
|
371
417
|
importantForAccessibility="no"
|
|
@@ -386,7 +432,7 @@ const BADGE_ANCHOR: ViewStyle = {
|
|
|
386
432
|
zIndex: 1,
|
|
387
433
|
}
|
|
388
434
|
|
|
389
|
-
const
|
|
435
|
+
const BADGE_STYLE: ViewStyle = { alignSelf: 'center', maxWidth: 38 }
|
|
390
436
|
|
|
391
437
|
/** Lets the label fill the card width so the one-line ellipsis has a bound. */
|
|
392
438
|
const LABEL_FILL_STYLE: TextStyle = { width: '100%', alignSelf: 'stretch' }
|
|
Binary file
|
|
@@ -6,12 +6,21 @@ import {
|
|
|
6
6
|
type StyleProp,
|
|
7
7
|
type ViewStyle,
|
|
8
8
|
type TextStyle,
|
|
9
|
+
type GestureResponderEvent,
|
|
9
10
|
} from 'react-native'
|
|
10
11
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
11
12
|
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
12
13
|
import Checkbox from '../Checkbox/Checkbox'
|
|
13
14
|
import type { Modes } from '../../design-tokens'
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Figma's Checkbox Item label slot lays out `Text` + `Link` (etc.) in a
|
|
18
|
+
* horizontal auto-layout frame with `Slot gap = XS` (2px). Consumers may still
|
|
19
|
+
* override via `modes['Slot gap']`. Applied only when resolving the slot gap —
|
|
20
|
+
* not merged into child `modes` — so nested components are unaffected.
|
|
21
|
+
*/
|
|
22
|
+
const LABEL_SLOT_GAP_DEFAULTS = { 'Slot gap': 'XS' } as const
|
|
23
|
+
|
|
15
24
|
export type CheckboxItemProps = {
|
|
16
25
|
/** Whether the checkbox is checked (controlled). */
|
|
17
26
|
checked?: boolean
|
|
@@ -24,9 +33,13 @@ export type CheckboxItemProps = {
|
|
|
24
33
|
/**
|
|
25
34
|
* Content rendered in the row's label slot (between the leading and trailing
|
|
26
35
|
* edges). This is the primary way to supply the label and mirrors the Figma
|
|
27
|
-
* "
|
|
28
|
-
* custom node
|
|
29
|
-
*
|
|
36
|
+
* "Text" slot — pass a plain string for the default token-styled label, or any
|
|
37
|
+
* custom node(s) for full control.
|
|
38
|
+
*
|
|
39
|
+
* Multiple children are laid out in a horizontal row with `slot/gap` (Figma
|
|
40
|
+
* default `Slot gap = XS` → 2px), matching the `Text` + `Link` composition in
|
|
41
|
+
* the design. A single custom node (e.g. a vertical stack) is left as-is.
|
|
42
|
+
* Custom nodes receive the same `modes` as the parent.
|
|
30
43
|
*
|
|
31
44
|
* When both `children` and the legacy {@link label} prop are provided,
|
|
32
45
|
* `children` wins.
|
|
@@ -44,7 +57,8 @@ export type CheckboxItemProps = {
|
|
|
44
57
|
/**
|
|
45
58
|
* Position of the checkbox control relative to the label.
|
|
46
59
|
* - `'leading'` (default): checkbox is rendered on the left, then label, then optional `endSlot` on the right.
|
|
47
|
-
* - `'trailing'`: optional `endSlot` is rendered on the left, then label, then checkbox on the right
|
|
60
|
+
* - `'trailing'`: optional `endSlot` is rendered on the left, then label, then checkbox on the right
|
|
61
|
+
* (row uses `space-between` so the checkbox sits on the trailing edge).
|
|
48
62
|
*
|
|
49
63
|
* Matches the Figma "Control" variant on the Checkbox Item component.
|
|
50
64
|
*/
|
|
@@ -52,7 +66,8 @@ export type CheckboxItemProps = {
|
|
|
52
66
|
/**
|
|
53
67
|
* Optional content rendered in the row's end slot (e.g. a `Button`). When provided,
|
|
54
68
|
* it receives the same `modes` as the parent. Wrapped in a fixed-width container
|
|
55
|
-
* matching the Figma `endSlot` design.
|
|
69
|
+
* matching the Figma `endSlot` design. Presses on the end slot do not toggle
|
|
70
|
+
* the checkbox.
|
|
56
71
|
*
|
|
57
72
|
* Placement depends on `control`:
|
|
58
73
|
* - With `control="leading"` the slot appears on the right edge (after the label).
|
|
@@ -72,15 +87,21 @@ export type CheckboxItemProps = {
|
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
/**
|
|
75
|
-
* CheckboxItem composes a `Checkbox`, a label and an optional `endSlot` into a
|
|
90
|
+
* CheckboxItem composes a `Checkbox`, a label slot and an optional `endSlot` into a
|
|
76
91
|
* single horizontal pressable row. Pressing anywhere on the row (outside of the
|
|
77
|
-
* `endSlot`
|
|
92
|
+
* `endSlot` and nested interactive children such as `Link`) toggles the checkbox,
|
|
93
|
+
* mirroring the typical native form pattern.
|
|
78
94
|
*
|
|
79
95
|
* Use the `control` prop to swap the checkbox between the leading (left, default)
|
|
80
96
|
* and trailing (right) edge of the row. The `endSlot` flips to the opposite edge.
|
|
81
97
|
*
|
|
98
|
+
* The Figma default composition places `Text` + `Link` in the label slot
|
|
99
|
+
* ("I agree" + "Terms & Conditions"). Pass those as `children` — the slot lays
|
|
100
|
+
* multiple children out in a horizontal row with `slot/gap`.
|
|
101
|
+
*
|
|
82
102
|
* Mirrors the Figma "Checkbox Item" component and uses the `checkboxItem/*`
|
|
83
|
-
* design tokens for
|
|
103
|
+
* design tokens for row spacing; string labels still resolve `checkboxItem/label/*`
|
|
104
|
+
* for backward-compatible typography.
|
|
84
105
|
*
|
|
85
106
|
* @component
|
|
86
107
|
* @param {CheckboxItemProps} props
|
|
@@ -89,13 +110,19 @@ export type CheckboxItemProps = {
|
|
|
89
110
|
* ```tsx
|
|
90
111
|
* const [checked, setChecked] = useState(false)
|
|
91
112
|
*
|
|
92
|
-
* //
|
|
113
|
+
* // Figma default composition — Text + Link in the label slot.
|
|
93
114
|
* <CheckboxItem
|
|
94
115
|
* checked={checked}
|
|
95
116
|
* onValueChange={setChecked}
|
|
96
117
|
* control="leading"
|
|
97
118
|
* modes={{ 'Color Mode': 'Light' }}
|
|
98
119
|
* >
|
|
120
|
+
* <Text text="I agree" autolayout="Hug" />
|
|
121
|
+
* <Link text="Terms & Conditions" autolayout="Hug" onPress={openTerms} />
|
|
122
|
+
* </CheckboxItem>
|
|
123
|
+
*
|
|
124
|
+
* // Plain string (still supported).
|
|
125
|
+
* <CheckboxItem checked={checked} onValueChange={setChecked}>
|
|
99
126
|
* Fixed deposit • 0245
|
|
100
127
|
* </CheckboxItem>
|
|
101
128
|
*
|
|
@@ -124,8 +151,8 @@ const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
|
124
151
|
// 2. otherwise the legacy `label` prop, whenever it was passed at all —
|
|
125
152
|
// including an explicit `null`/`false`, which (as before) hides the
|
|
126
153
|
// label entirely.
|
|
127
|
-
// 3. otherwise the Figma placeholder, so the default story
|
|
128
|
-
// something meaningful.
|
|
154
|
+
// 3. otherwise the Figma plain-string placeholder, so the default story
|
|
155
|
+
// still renders something meaningful when no slot content is supplied.
|
|
129
156
|
const hasChildren = children != null && children !== false
|
|
130
157
|
const slotContent: React.ReactNode = hasChildren
|
|
131
158
|
? children
|
|
@@ -151,6 +178,12 @@ const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
|
151
178
|
(getVariableByName('checkboxItem/padding/horizontal', modes) as number | null) ?? 0
|
|
152
179
|
const paddingVertical =
|
|
153
180
|
(getVariableByName('checkboxItem/padding/vertical', modes) as number | null) ?? 0
|
|
181
|
+
// Label-slot internal gap — Figma uses Slot gap = XS (2). Consumer modes win.
|
|
182
|
+
const labelSlotGap =
|
|
183
|
+
(getVariableByName('slot/gap', {
|
|
184
|
+
...LABEL_SLOT_GAP_DEFAULTS,
|
|
185
|
+
...modes,
|
|
186
|
+
}) as number | null) ?? 2
|
|
154
187
|
|
|
155
188
|
const labelColor =
|
|
156
189
|
(getVariableByName('checkboxItem/foreground', modes) as string | null) ?? '#1a1c1f'
|
|
@@ -168,11 +201,17 @@ const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
|
168
201
|
flexDirection: 'row',
|
|
169
202
|
alignItems: 'center',
|
|
170
203
|
gap,
|
|
204
|
+
// Trailing matches Figma: space-between pins the checkbox to the far edge
|
|
205
|
+
// while the label (and optional endSlot) stay on the leading side.
|
|
206
|
+
justifyContent: isTrailing ? 'space-between' : 'flex-start',
|
|
171
207
|
paddingHorizontal,
|
|
172
208
|
paddingVertical,
|
|
173
209
|
width: '100%',
|
|
174
210
|
}
|
|
175
211
|
|
|
212
|
+
// String labels keep the historical checkboxItem/label/* typography so
|
|
213
|
+
// existing plain-string call-sites do not change visually. `flex: 1` is
|
|
214
|
+
// retained in both control modes so long labels still wrap inside the row.
|
|
176
215
|
const resolvedLabelStyle: TextStyle = {
|
|
177
216
|
flex: 1,
|
|
178
217
|
minWidth: 0,
|
|
@@ -203,17 +242,36 @@ const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
|
203
242
|
// behaviour of the `label` prop).
|
|
204
243
|
const isTextSlot =
|
|
205
244
|
typeof slotContent === 'string' || typeof slotContent === 'number'
|
|
245
|
+
|
|
246
|
+
const labelSlotStyle: ViewStyle = {
|
|
247
|
+
// Grow to fill remaining row width so long content wraps. Trailing rows
|
|
248
|
+
// also set `justifyContent: 'space-between'` so the checkbox stays pinned
|
|
249
|
+
// to the far edge (Figma Control=Trailing).
|
|
250
|
+
flex: 1,
|
|
251
|
+
minWidth: 0,
|
|
252
|
+
flexDirection: 'row',
|
|
253
|
+
flexWrap: 'wrap',
|
|
254
|
+
alignItems: 'flex-start',
|
|
255
|
+
gap: labelSlotGap,
|
|
256
|
+
}
|
|
257
|
+
|
|
206
258
|
const labelNode =
|
|
207
259
|
slotContent == null || slotContent === false ? null : isTextSlot ? (
|
|
208
260
|
<Text style={[resolvedLabelStyle, labelStyle]} selectable={false}>
|
|
209
261
|
{slotContent}
|
|
210
262
|
</Text>
|
|
211
263
|
) : (
|
|
212
|
-
<View style={
|
|
264
|
+
<View style={labelSlotStyle}>
|
|
213
265
|
{cloneChildrenWithModes(slotContent, modes)}
|
|
214
266
|
</View>
|
|
215
267
|
)
|
|
216
268
|
|
|
269
|
+
// Capture the end-slot responder so presses on nested actions (e.g. Button)
|
|
270
|
+
// never bubble to the row Pressable and toggle the checkbox.
|
|
271
|
+
const suppressRowToggle = useCallback((event: GestureResponderEvent) => {
|
|
272
|
+
event.stopPropagation?.()
|
|
273
|
+
}, [])
|
|
274
|
+
|
|
217
275
|
const endSlotNode = endSlot ? (
|
|
218
276
|
<View
|
|
219
277
|
style={{
|
|
@@ -221,6 +279,8 @@ const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
|
221
279
|
flexShrink: 0,
|
|
222
280
|
alignItems: 'stretch',
|
|
223
281
|
}}
|
|
282
|
+
onStartShouldSetResponder={() => true}
|
|
283
|
+
onTouchEnd={suppressRowToggle}
|
|
224
284
|
>
|
|
225
285
|
{cloneChildrenWithModes(endSlot, modes)}
|
|
226
286
|
</View>
|