jfs-components 0.1.2 → 0.1.8
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 +29 -0
- package/lib/commonjs/components/AmountInput/AmountInput.js +8 -5
- package/lib/commonjs/components/BenefitCard/BenefitCard.js +231 -0
- package/lib/commonjs/components/CcCard/CcCard.js +470 -0
- package/lib/commonjs/components/Checkbox/Checkbox.js +4 -3
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +4 -3
- package/lib/commonjs/components/CompareTable/CompareTable.js +372 -0
- package/lib/commonjs/components/ComparisonBar/ComparisonBar.js +266 -0
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +35 -3
- package/lib/commonjs/components/FormField/FormField.js +4 -3
- package/lib/commonjs/components/InputSearch/InputSearch.js +6 -4
- package/lib/commonjs/components/NoteInput/NoteInput.js +6 -5
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +273 -0
- package/lib/commonjs/components/ProductMerchandisingCard/GlassFill.js +263 -0
- package/lib/commonjs/components/ProductMerchandisingCard/GlassFill.web.js +116 -0
- package/lib/commonjs/components/ProductMerchandisingCard/ProductMerchandisingCard.js +353 -0
- package/lib/commonjs/components/ProjectionMarker/ProjectionMarker.js +161 -0
- package/lib/commonjs/components/Radio/Radio.js +5 -5
- package/lib/commonjs/components/Slider/Slider.js +473 -0
- package/lib/commonjs/components/TextInput/TextInput.js +13 -8
- package/lib/commonjs/components/TextSegment/TextSegment.js +118 -0
- package/lib/commonjs/components/index.js +63 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +38 -9
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/commonjs/utils/react-utils.js +22 -0
- package/lib/module/components/AmountInput/AmountInput.js +6 -4
- package/lib/module/components/BenefitCard/BenefitCard.js +225 -0
- package/lib/module/components/CcCard/CcCard.js +464 -0
- package/lib/module/components/Checkbox/Checkbox.js +5 -4
- package/lib/module/components/CheckboxItem/CheckboxItem.js +5 -4
- package/lib/module/components/CompareTable/CompareTable.js +367 -0
- package/lib/module/components/ComparisonBar/ComparisonBar.js +260 -0
- package/lib/module/components/DropdownInput/DropdownInput.js +36 -4
- package/lib/module/components/FormField/FormField.js +5 -4
- package/lib/module/components/InputSearch/InputSearch.js +6 -4
- package/lib/module/components/NoteInput/NoteInput.js +7 -6
- package/lib/module/components/PdpCcCard/PdpCcCard.js +267 -0
- package/lib/module/components/ProductMerchandisingCard/GlassFill.js +257 -0
- package/lib/module/components/ProductMerchandisingCard/GlassFill.web.js +111 -0
- package/lib/module/components/ProductMerchandisingCard/ProductMerchandisingCard.js +347 -0
- package/lib/module/components/ProjectionMarker/ProjectionMarker.js +156 -0
- package/lib/module/components/Radio/Radio.js +5 -4
- package/lib/module/components/Slider/Slider.js +468 -0
- package/lib/module/components/TextInput/TextInput.js +15 -10
- package/lib/module/components/TextSegment/TextSegment.js +113 -0
- package/lib/module/components/index.js +9 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/design-tokens/figma-modes.generated.js +38 -9
- package/lib/module/icons/registry.js +1 -1
- package/lib/module/utils/react-utils.js +21 -0
- package/lib/typescript/src/components/AmountInput/AmountInput.d.ts +3 -2
- package/lib/typescript/src/components/BenefitCard/BenefitCard.d.ts +93 -0
- package/lib/typescript/src/components/CcCard/CcCard.d.ts +137 -0
- package/lib/typescript/src/components/Checkbox/Checkbox.d.ts +3 -2
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +2 -2
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +88 -0
- package/lib/typescript/src/components/ComparisonBar/ComparisonBar.d.ts +118 -0
- package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +20 -1
- package/lib/typescript/src/components/FormField/FormField.d.ts +2 -2
- package/lib/typescript/src/components/InputSearch/InputSearch.d.ts +23 -2
- package/lib/typescript/src/components/NoteInput/NoteInput.d.ts +19 -2
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +84 -0
- package/lib/typescript/src/components/ProductMerchandisingCard/GlassFill.d.ts +56 -0
- package/lib/typescript/src/components/ProductMerchandisingCard/GlassFill.web.d.ts +27 -0
- package/lib/typescript/src/components/ProductMerchandisingCard/ProductMerchandisingCard.d.ts +81 -0
- package/lib/typescript/src/components/ProjectionMarker/ProjectionMarker.d.ts +82 -0
- package/lib/typescript/src/components/Radio/Radio.d.ts +3 -2
- package/lib/typescript/src/components/RadioButton/RadioButton.d.ts +2 -2
- package/lib/typescript/src/components/Slider/Slider.d.ts +99 -0
- package/lib/typescript/src/components/TextInput/TextInput.d.ts +9 -29
- package/lib/typescript/src/components/TextSegment/TextSegment.d.ts +100 -0
- package/lib/typescript/src/components/index.d.ts +10 -1
- package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +22 -2
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/lib/typescript/src/utils/react-utils.d.ts +10 -0
- package/package.json +2 -1
- package/src/components/AmountInput/AmountInput.tsx +7 -5
- package/src/components/BenefitCard/BenefitCard.tsx +309 -0
- package/src/components/CcCard/CcCard.tsx +598 -0
- package/src/components/Checkbox/Checkbox.tsx +5 -4
- package/src/components/CheckboxItem/CheckboxItem.tsx +5 -4
- package/src/components/CompareTable/CompareTable.tsx +477 -0
- package/src/components/ComparisonBar/ComparisonBar.tsx +356 -0
- package/src/components/DropdownInput/DropdownInput.tsx +55 -3
- package/src/components/FormField/FormField.tsx +5 -4
- package/src/components/InputSearch/InputSearch.tsx +8 -5
- package/src/components/NoteInput/NoteInput.tsx +8 -6
- package/src/components/PdpCcCard/PdpCcCard.tsx +356 -0
- package/src/components/ProductMerchandisingCard/GlassFill.tsx +276 -0
- package/src/components/ProductMerchandisingCard/GlassFill.web.tsx +127 -0
- package/src/components/ProductMerchandisingCard/ProductMerchandisingCard.tsx +423 -0
- package/src/components/ProjectionMarker/ProjectionMarker.tsx +277 -0
- package/src/components/Radio/Radio.tsx +5 -4
- package/src/components/Slider/Slider.tsx +628 -0
- package/src/components/TextInput/TextInput.tsx +15 -11
- package/src/components/TextSegment/TextSegment.tsx +166 -0
- package/src/components/index.ts +10 -1
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/design-tokens/figma-modes.generated.ts +38 -9
- package/src/icons/registry.ts +1 -1
- package/src/utils/react-utils.ts +23 -0
- package/lib/typescript/scripts/extract-component-tokens.d.ts +0 -9
- package/lib/typescript/scripts/generate-component-docs.d.ts +0 -9
- package/lib/typescript/scripts/generate-icon-registry.d.ts +0 -3
- package/lib/typescript/scripts/generate-mode-types.d.ts +0 -2
- package/lib/typescript/scripts/retype-modes.d.cts +0 -2
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
Pressable,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
type ViewStyle,
|
|
8
|
+
type TextStyle,
|
|
9
|
+
type StyleProp,
|
|
10
|
+
type ImageSourcePropType,
|
|
11
|
+
} from 'react-native'
|
|
12
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
13
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
|
|
14
|
+
import Badge from '../Badge/Badge'
|
|
15
|
+
import Avatar from '../Avatar/Avatar'
|
|
16
|
+
import Image from '../Image/Image'
|
|
17
|
+
import Icon from '../Icon/Icon'
|
|
18
|
+
import Title from '../Title/Title'
|
|
19
|
+
import ListItem from '../ListItem/ListItem'
|
|
20
|
+
import TextSegment, { type TextSegmentRun } from '../TextSegment/TextSegment'
|
|
21
|
+
import Button from '../Button/Button'
|
|
22
|
+
import type { Modes } from '../../design-tokens'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A single coloured chip rendered in the card header. Each badge resolves its
|
|
26
|
+
* own design tokens through `modes`, so different appearances (brand, tonal,
|
|
27
|
+
* etc.) can sit side-by-side in the same group.
|
|
28
|
+
*/
|
|
29
|
+
export interface CcCardBadge {
|
|
30
|
+
/** Visible label text. */
|
|
31
|
+
label: string
|
|
32
|
+
/** Per-badge mode overrides (e.g. `{ AppearanceBrand: 'Tertiary' }`). */
|
|
33
|
+
modes?: Modes
|
|
34
|
+
/** Background color override (wins over the token-resolved value). */
|
|
35
|
+
color?: string
|
|
36
|
+
/** Label color override (wins over the token-resolved value). */
|
|
37
|
+
labelColor?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A single benefit row in the card's list, rendered through the shared
|
|
42
|
+
* {@link ListItem} primitive (leading icon + title).
|
|
43
|
+
*/
|
|
44
|
+
export interface CcCardListItem {
|
|
45
|
+
/**
|
|
46
|
+
* Registry icon name for the leading glyph. Defaults to `'ic_card'`. Pass
|
|
47
|
+
* `null` to omit the leading icon.
|
|
48
|
+
*/
|
|
49
|
+
icon?: string | null
|
|
50
|
+
/** Per-row override for the leading icon color. */
|
|
51
|
+
iconColor?: string
|
|
52
|
+
/** Full override for the leading node. Takes precedence over `icon`. */
|
|
53
|
+
leading?: React.ReactNode
|
|
54
|
+
/** Row title (e.g. `"4 domestic + 2 intl. lounge access yearly"`). */
|
|
55
|
+
title: string
|
|
56
|
+
/** Makes the row pressable. */
|
|
57
|
+
onPress?: () => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CcCardProps {
|
|
61
|
+
/**
|
|
62
|
+
* Compact variant. Renders only the leading media slot (an `Avatar` by
|
|
63
|
+
* default) followed by the footer **without** its CTA button — a condensed
|
|
64
|
+
* representation of the card. Mirrors the Figma `compact` property.
|
|
65
|
+
*/
|
|
66
|
+
compact?: boolean
|
|
67
|
+
|
|
68
|
+
// ---- Header (non-compact only) ----------------------------------------
|
|
69
|
+
/** Toggles the header badge row. Defaults to `true`. */
|
|
70
|
+
showHeader?: boolean
|
|
71
|
+
/** Leading (left) badge group. */
|
|
72
|
+
badges?: CcCardBadge[]
|
|
73
|
+
/** Trailing (right-aligned) badge group. */
|
|
74
|
+
trailingBadges?: CcCardBadge[]
|
|
75
|
+
/** Full override for the header. Takes precedence over `badges`/`trailingBadges`. */
|
|
76
|
+
header?: React.ReactNode
|
|
77
|
+
|
|
78
|
+
// ---- Media slot -------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Image source for the default media slot. Accepts a URL string or any RN
|
|
81
|
+
* `ImageSourcePropType`. Ignored when `media` is provided.
|
|
82
|
+
*/
|
|
83
|
+
imageSource?: ImageSourcePropType | string
|
|
84
|
+
/** Default media image width. Defaults to the Figma spec (`88`). */
|
|
85
|
+
imageWidth?: number
|
|
86
|
+
/** Default media image height. Defaults to the Figma spec (`54`). */
|
|
87
|
+
imageHeight?: number
|
|
88
|
+
/** Product title rendered next to the media slot (non-compact only). */
|
|
89
|
+
title?: string
|
|
90
|
+
/** Product subtitle rendered below the title (non-compact only). */
|
|
91
|
+
subtitle?: string
|
|
92
|
+
/**
|
|
93
|
+
* Full override for the media slot's visual. Whatever you pass here is
|
|
94
|
+
* rendered **in both the compact and non-compact variants** — e.g. pass an
|
|
95
|
+
* `Avatar` and it stays an avatar in both, pass an `Image` and it stays an
|
|
96
|
+
* image in both. `modes` cascade into it automatically. Defaults to an
|
|
97
|
+
* `Image` driven by `imageSource`.
|
|
98
|
+
*/
|
|
99
|
+
media?: React.ReactNode
|
|
100
|
+
|
|
101
|
+
// ---- List (non-compact only) ------------------------------------------
|
|
102
|
+
/** Benefit rows rendered in the list. Defaults to three sample rows. */
|
|
103
|
+
items?: CcCardListItem[]
|
|
104
|
+
|
|
105
|
+
// ---- Nudge (non-compact only) -----------------------------------------
|
|
106
|
+
/** Toggles the upsell nudge row. Defaults to `true`. */
|
|
107
|
+
showNudge?: boolean
|
|
108
|
+
/**
|
|
109
|
+
* Declarative content for the nudge's inline text. Defaults to a sample
|
|
110
|
+
* "Upsell message JioFinance+" string with the last run brand-coloured.
|
|
111
|
+
*/
|
|
112
|
+
nudgeSegments?: TextSegmentRun[]
|
|
113
|
+
/** Avatar image source shown at the start of the nudge row. */
|
|
114
|
+
nudgeAvatarSource?: ImageSourcePropType | string
|
|
115
|
+
/** Full override for the nudge row. Takes precedence over the declarative props. */
|
|
116
|
+
nudge?: React.ReactNode
|
|
117
|
+
|
|
118
|
+
// ---- Footer -----------------------------------------------------------
|
|
119
|
+
/** Small muted label above the footer title (`ccCard/headline/*`). */
|
|
120
|
+
headline?: string
|
|
121
|
+
/** Bold footer value (`ccCard/title/*`). */
|
|
122
|
+
description?: string
|
|
123
|
+
/** Muted caption rendered inline after the description (`ccCard/subtitle/*`). */
|
|
124
|
+
footerSubtitle?: string
|
|
125
|
+
/** Full override for the footer text block. */
|
|
126
|
+
footer?: React.ReactNode
|
|
127
|
+
|
|
128
|
+
// ---- Footer CTA (non-compact only) ------------------------------------
|
|
129
|
+
/** CTA button label. Defaults to `"Button"`. */
|
|
130
|
+
buttonLabel?: string
|
|
131
|
+
/** CTA press handler. */
|
|
132
|
+
onButtonPress?: () => void
|
|
133
|
+
/** Full override for the CTA. Takes precedence over `buttonLabel`. */
|
|
134
|
+
button?: React.ReactNode
|
|
135
|
+
/** Toggles the CTA button (non-compact). Defaults to `true`. */
|
|
136
|
+
showButton?: boolean
|
|
137
|
+
|
|
138
|
+
// ---- Card -------------------------------------------------------------
|
|
139
|
+
/** Press handler for the whole card. When set, the card becomes pressable. */
|
|
140
|
+
onPress?: () => void
|
|
141
|
+
/** Card width. Defaults to the Figma spec (`343`). Pass `'100%'` to fill the parent. */
|
|
142
|
+
width?: number | `${number}%`
|
|
143
|
+
/** Modes object for design-token resolution. Cascaded to all children. */
|
|
144
|
+
modes?: Modes
|
|
145
|
+
/** Style overrides for the card container. */
|
|
146
|
+
style?: StyleProp<ViewStyle>
|
|
147
|
+
/** Accessibility label for the card. */
|
|
148
|
+
accessibilityLabel?: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* CcCard — Figma node 5434:1992 ("Cc Card").
|
|
153
|
+
*
|
|
154
|
+
* A white, rounded credit-card product card composed entirely from the shared
|
|
155
|
+
* design-system primitives so it stays in sync with the rest of the library:
|
|
156
|
+
*
|
|
157
|
+
* - **Header** — two {@link Badge} groups (a leading group + a right-aligned
|
|
158
|
+
* trailing group), e.g. `Pre-qualified` / `Lifetime free`.
|
|
159
|
+
* - **Media** — a product `Image` + a {@link Title} (title + subtitle resolved
|
|
160
|
+
* through the `context7: Card` mode → 14px/12px).
|
|
161
|
+
* - **List** — a column of benefit {@link ListItem}s (`List Item Style:
|
|
162
|
+
* Minimal`) with leading icons.
|
|
163
|
+
* - **Nudge** — an inline upsell row: an {@link Avatar} + a {@link TextSegment}.
|
|
164
|
+
* - **Footer** — a headline + description + subtitle text block alongside a
|
|
165
|
+
* small {@link Button} (`Button / Size: S`, `AppearanceBrand: Secondary`).
|
|
166
|
+
*
|
|
167
|
+
* The {@link CcCardProps.compact} variant collapses the card to just the
|
|
168
|
+
* leading avatar slot + the footer text block (no CTA button). All defaults can
|
|
169
|
+
* be overridden via `modes`.
|
|
170
|
+
*/
|
|
171
|
+
function CcCard({
|
|
172
|
+
compact = false,
|
|
173
|
+
showHeader = true,
|
|
174
|
+
badges = DEFAULT_BADGES,
|
|
175
|
+
trailingBadges,
|
|
176
|
+
header,
|
|
177
|
+
imageSource,
|
|
178
|
+
imageWidth = 88,
|
|
179
|
+
imageHeight = 54,
|
|
180
|
+
title = 'Title',
|
|
181
|
+
subtitle = 'Subtitle',
|
|
182
|
+
media,
|
|
183
|
+
items = DEFAULT_ITEMS,
|
|
184
|
+
showNudge = true,
|
|
185
|
+
nudgeSegments = DEFAULT_NUDGE_SEGMENTS,
|
|
186
|
+
nudgeAvatarSource,
|
|
187
|
+
nudge,
|
|
188
|
+
headline = 'Headline',
|
|
189
|
+
description = 'Description',
|
|
190
|
+
footerSubtitle = 'Subtitle',
|
|
191
|
+
footer,
|
|
192
|
+
buttonLabel = 'Button',
|
|
193
|
+
onButtonPress,
|
|
194
|
+
button,
|
|
195
|
+
showButton = true,
|
|
196
|
+
onPress,
|
|
197
|
+
width = 343,
|
|
198
|
+
modes = EMPTY_MODES,
|
|
199
|
+
style,
|
|
200
|
+
accessibilityLabel,
|
|
201
|
+
}: CcCardProps) {
|
|
202
|
+
const tokens = useMemo(() => resolveTokens(modes), [modes])
|
|
203
|
+
|
|
204
|
+
// The product title uses the card-sized type ramp (14px bold + 12px
|
|
205
|
+
// subtitle) via the `context7: Card` mode. Consumer modes still win.
|
|
206
|
+
const titleModes = useMemo<Modes>(() => ({ context7: 'Card', ...modes }), [modes])
|
|
207
|
+
// List rows use the minimal style (12px / regular, no padding).
|
|
208
|
+
const listItemModes = useMemo<Modes>(() => ({ 'List Item Style': 'Minimal', ...modes }), [modes])
|
|
209
|
+
// The nudge avatar is the smallest size token.
|
|
210
|
+
const nudgeAvatarModes = useMemo<Modes>(() => ({ 'Avatar Size': 'XS', ...modes }), [modes])
|
|
211
|
+
// The CTA is the small, brand-secondary (lilac→purple) button.
|
|
212
|
+
const ctaModes = useMemo<Modes>(
|
|
213
|
+
() => ({ AppearanceBrand: 'Secondary', 'Button / Size': 'S', ...modes }),
|
|
214
|
+
[modes]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
// ---- Header -----------------------------------------------------------
|
|
218
|
+
const headerNode = header
|
|
219
|
+
? cloneChildrenWithModes(header, modes)
|
|
220
|
+
: showHeader && (badges?.length || trailingBadges?.length) ? (
|
|
221
|
+
<View style={styles.header}>
|
|
222
|
+
<BadgeGroup badges={badges} modes={modes} />
|
|
223
|
+
{trailingBadges?.length ? (
|
|
224
|
+
<BadgeGroup badges={trailingBadges} modes={modes} />
|
|
225
|
+
) : null}
|
|
226
|
+
</View>
|
|
227
|
+
) : null
|
|
228
|
+
|
|
229
|
+
// ---- Media slot (shared by both compact + non-compact) ----------------
|
|
230
|
+
// Whatever lives in the slot is rendered identically in both variants; only
|
|
231
|
+
// the surrounding sections differ. The default is an Image driven by
|
|
232
|
+
// `imageSource`, but a consumer can pass an Avatar (or any node) via `media`.
|
|
233
|
+
const mediaSlotContent = media ? (
|
|
234
|
+
cloneChildrenWithModes(media, modes)
|
|
235
|
+
) : (
|
|
236
|
+
<Image
|
|
237
|
+
imageSource={imageSource}
|
|
238
|
+
width={imageWidth}
|
|
239
|
+
height={imageHeight}
|
|
240
|
+
borderRadius={tokens.imageRadius}
|
|
241
|
+
resizeMode="cover"
|
|
242
|
+
accessibilityElementsHidden
|
|
243
|
+
importantForAccessibility="no"
|
|
244
|
+
/>
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
// Non-compact lays the slot out in a row next to the product Title.
|
|
248
|
+
const mediaRow = (
|
|
249
|
+
<View style={styles.mediaSlot}>
|
|
250
|
+
{mediaSlotContent}
|
|
251
|
+
<View style={styles.titleWrap}>
|
|
252
|
+
<Title
|
|
253
|
+
title={title}
|
|
254
|
+
subtitle={subtitle}
|
|
255
|
+
textAlign="Left"
|
|
256
|
+
modes={titleModes}
|
|
257
|
+
style={styles.titleInner}
|
|
258
|
+
/>
|
|
259
|
+
</View>
|
|
260
|
+
</View>
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
// ---- List -------------------------------------------------------------
|
|
264
|
+
const listNode =
|
|
265
|
+
items.length > 0 ? (
|
|
266
|
+
<View style={tokens.listGroup}>
|
|
267
|
+
{items.map((item, index) => {
|
|
268
|
+
const leading =
|
|
269
|
+
item.leading ??
|
|
270
|
+
(item.icon !== null ? (
|
|
271
|
+
<Icon iconName={item.icon ?? 'ic_card'} color={item.iconColor} modes={listItemModes} />
|
|
272
|
+
) : null)
|
|
273
|
+
return (
|
|
274
|
+
<ListItem
|
|
275
|
+
key={index}
|
|
276
|
+
layout="Horizontal"
|
|
277
|
+
navArrow={false}
|
|
278
|
+
showSupportText={false}
|
|
279
|
+
title={item.title}
|
|
280
|
+
leading={leading}
|
|
281
|
+
onPress={item.onPress}
|
|
282
|
+
modes={listItemModes}
|
|
283
|
+
/>
|
|
284
|
+
)
|
|
285
|
+
})}
|
|
286
|
+
</View>
|
|
287
|
+
) : null
|
|
288
|
+
|
|
289
|
+
// ---- Nudge ------------------------------------------------------------
|
|
290
|
+
const nudgeNode = nudge
|
|
291
|
+
? cloneChildrenWithModes(nudge, modes)
|
|
292
|
+
: showNudge ? (
|
|
293
|
+
<View style={tokens.nudge}>
|
|
294
|
+
<Avatar imageSource={nudgeAvatarSource} modes={nudgeAvatarModes} />
|
|
295
|
+
<View style={styles.nudgeContent}>
|
|
296
|
+
<TextSegment segments={nudgeSegments} modes={modes} />
|
|
297
|
+
</View>
|
|
298
|
+
</View>
|
|
299
|
+
) : null
|
|
300
|
+
|
|
301
|
+
// ---- Footer -----------------------------------------------------------
|
|
302
|
+
const ctaNode = button ? (
|
|
303
|
+
cloneChildrenWithModes(button, ctaModes)
|
|
304
|
+
) : (
|
|
305
|
+
<Button label={buttonLabel} modes={ctaModes} onPress={onButtonPress} />
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
const footerTextBlock = footer ? (
|
|
309
|
+
cloneChildrenWithModes(footer, modes)
|
|
310
|
+
) : (
|
|
311
|
+
<View style={tokens.footerTextWrap}>
|
|
312
|
+
{headline != null ? <Text style={tokens.headline}>{headline}</Text> : null}
|
|
313
|
+
<View style={compact ? tokens.titleWrapCompact : tokens.titleWrapDefault}>
|
|
314
|
+
{description != null ? <Text style={tokens.footerTitle}>{description}</Text> : null}
|
|
315
|
+
{footerSubtitle != null ? <Text style={tokens.footerSubtitle}>{footerSubtitle}</Text> : null}
|
|
316
|
+
</View>
|
|
317
|
+
</View>
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
const footerNode = (
|
|
321
|
+
<View style={compact ? tokens.footerRowCompact : tokens.footerRowDefault}>
|
|
322
|
+
{footerTextBlock}
|
|
323
|
+
{!compact && showButton ? ctaNode : null}
|
|
324
|
+
</View>
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
// ---- Compose ----------------------------------------------------------
|
|
328
|
+
const content = compact ? (
|
|
329
|
+
<>
|
|
330
|
+
<View style={styles.compactSlot}>{mediaSlotContent}</View>
|
|
331
|
+
{footerNode}
|
|
332
|
+
</>
|
|
333
|
+
) : (
|
|
334
|
+
<>
|
|
335
|
+
{headerNode}
|
|
336
|
+
{mediaRow}
|
|
337
|
+
{listNode}
|
|
338
|
+
{nudgeNode}
|
|
339
|
+
{footerNode}
|
|
340
|
+
</>
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
const containerStyle = useMemo<ViewStyle>(
|
|
344
|
+
() => ({ ...tokens.container, width }),
|
|
345
|
+
[tokens.container, width]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if (onPress) {
|
|
349
|
+
return (
|
|
350
|
+
<Pressable
|
|
351
|
+
style={({ pressed }) => [containerStyle, pressed ? styles.pressed : null, style]}
|
|
352
|
+
accessibilityRole="button"
|
|
353
|
+
accessibilityLabel={accessibilityLabel ?? title}
|
|
354
|
+
onPress={onPress}
|
|
355
|
+
>
|
|
356
|
+
{content}
|
|
357
|
+
</Pressable>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
|
|
363
|
+
{content}
|
|
364
|
+
</View>
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// BadgeGroup — internal wrapping row of badges
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
function BadgeGroup({ badges, modes }: { badges: CcCardBadge[]; modes: Modes }) {
|
|
373
|
+
return (
|
|
374
|
+
<View style={styles.badgeGroup}>
|
|
375
|
+
{badges.map((badge, index) => {
|
|
376
|
+
const badgeModes = badge.modes ? { ...modes, ...badge.modes } : modes
|
|
377
|
+
return (
|
|
378
|
+
<Badge
|
|
379
|
+
key={index}
|
|
380
|
+
label={badge.label}
|
|
381
|
+
modes={badgeModes}
|
|
382
|
+
style={badge.color ? { backgroundColor: badge.color } : undefined}
|
|
383
|
+
labelStyle={badge.labelColor ? { color: badge.labelColor } : undefined}
|
|
384
|
+
/>
|
|
385
|
+
)
|
|
386
|
+
})}
|
|
387
|
+
</View>
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// Tokens / static styles
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
interface ResolvedTokens {
|
|
396
|
+
container: ViewStyle
|
|
397
|
+
imageRadius: number
|
|
398
|
+
listGroup: ViewStyle
|
|
399
|
+
nudge: ViewStyle
|
|
400
|
+
footerRowDefault: ViewStyle
|
|
401
|
+
footerRowCompact: ViewStyle
|
|
402
|
+
footerTextWrap: ViewStyle
|
|
403
|
+
titleWrapDefault: ViewStyle
|
|
404
|
+
titleWrapCompact: ViewStyle
|
|
405
|
+
headline: TextStyle
|
|
406
|
+
footerTitle: TextStyle
|
|
407
|
+
footerSubtitle: TextStyle
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function asNum(raw: unknown, fallback: number): number {
|
|
411
|
+
const n = typeof raw === 'number' ? raw : parseFloat(raw as string)
|
|
412
|
+
return Number.isFinite(n) ? n : fallback
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function asStr(raw: unknown, fallback: string): string {
|
|
416
|
+
return raw != null ? String(raw) : fallback
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function resolveTokens(modes: Modes): ResolvedTokens {
|
|
420
|
+
// NOTE: token names are passed as string literals DIRECTLY to
|
|
421
|
+
// getVariableByName so the `extract-component-tokens` script can statically
|
|
422
|
+
// collect them for the generated docs. Do not refactor these into a helper
|
|
423
|
+
// that receives the name as a variable.
|
|
424
|
+
const background = asStr(getVariableByName('ccCard/bg', modes), '#ffffff')
|
|
425
|
+
const borderColor = asStr(getVariableByName('ccCard/border/color', modes), '#e3e4e4')
|
|
426
|
+
const borderWidth = asNum(getVariableByName('ccCard/border/size', modes), 1)
|
|
427
|
+
const radius = asNum(getVariableByName('ccCard/radius', modes), 12)
|
|
428
|
+
const paddingHorizontal = asNum(getVariableByName('ccCard/padding/horizontal', modes), 12)
|
|
429
|
+
const paddingVertical = asNum(getVariableByName('ccCard/padding/vertical', modes), 12)
|
|
430
|
+
const gap = asNum(getVariableByName('ccCard/padding/gap', modes), 8)
|
|
431
|
+
|
|
432
|
+
const imageRadius = asNum(getVariableByName('image/radius', modes), 0)
|
|
433
|
+
const listGroupGap = asNum(getVariableByName('listGroup/gap', modes), 4)
|
|
434
|
+
const nudgeGap = asNum(getVariableByName('nudge/gap', modes), 6)
|
|
435
|
+
|
|
436
|
+
const footerGap = asNum(getVariableByName('ccCard/footer/gap', modes), 8)
|
|
437
|
+
const textWrapGap = asNum(getVariableByName('ccCard/footer/textWrap/gap', modes), 4)
|
|
438
|
+
const titleWrapGap = asNum(getVariableByName('ccCard/footer/titleWrap/gap', modes), 8)
|
|
439
|
+
|
|
440
|
+
const headlineColor = asStr(getVariableByName('ccCard/headline/color', modes), '#707275')
|
|
441
|
+
const headlineSize = asNum(getVariableByName('ccCard/headline/fontSize', modes), 10)
|
|
442
|
+
const headlineFamily = asStr(getVariableByName('ccCard/headline/fontFamily', modes), 'JioType Var')
|
|
443
|
+
const headlineLineHeight = asNum(getVariableByName('ccCard/headline/lineHeight', modes), 13)
|
|
444
|
+
const headlineWeight = asStr(getVariableByName('ccCard/headline/fontWeight', modes), '500')
|
|
445
|
+
|
|
446
|
+
const titleColor = asStr(getVariableByName('ccCard/title/color', modes), '#081007')
|
|
447
|
+
const titleSize = asNum(getVariableByName('ccCard/title/fontSize', modes), 14)
|
|
448
|
+
const titleFamily = asStr(getVariableByName('ccCard/title/fontFamily', modes), 'JioType Var')
|
|
449
|
+
const titleLineHeight = asNum(getVariableByName('ccCard/title/lineHeight', modes), 15)
|
|
450
|
+
const titleWeight = asStr(getVariableByName('ccCard/title/fontWeight', modes), '700')
|
|
451
|
+
|
|
452
|
+
const subtitleColor = asStr(getVariableByName('ccCard/subtitle/color', modes), '#707275')
|
|
453
|
+
const subtitleSize = asNum(getVariableByName('ccCard/subtitle/fontSize', modes), 10)
|
|
454
|
+
const subtitleFamily = asStr(getVariableByName('ccCard/subtitle/fontFamily', modes), 'JioType Var')
|
|
455
|
+
const subtitleLineHeight = asNum(getVariableByName('ccCard/subtitle/lineHeight', modes), 13)
|
|
456
|
+
const subtitleWeight = asStr(getVariableByName('ccCard/subtitle/fontWeight', modes), '500')
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
container: {
|
|
460
|
+
backgroundColor: background,
|
|
461
|
+
borderColor,
|
|
462
|
+
borderWidth,
|
|
463
|
+
borderRadius: radius,
|
|
464
|
+
paddingHorizontal,
|
|
465
|
+
paddingVertical,
|
|
466
|
+
gap,
|
|
467
|
+
flexDirection: 'column',
|
|
468
|
+
alignItems: 'flex-start',
|
|
469
|
+
overflow: 'hidden',
|
|
470
|
+
},
|
|
471
|
+
imageRadius,
|
|
472
|
+
listGroup: {
|
|
473
|
+
alignSelf: 'stretch',
|
|
474
|
+
gap: listGroupGap,
|
|
475
|
+
},
|
|
476
|
+
nudge: {
|
|
477
|
+
alignSelf: 'stretch',
|
|
478
|
+
flexDirection: 'row',
|
|
479
|
+
alignItems: 'center',
|
|
480
|
+
gap: nudgeGap,
|
|
481
|
+
},
|
|
482
|
+
footerRowDefault: {
|
|
483
|
+
alignSelf: 'stretch',
|
|
484
|
+
flexDirection: 'row',
|
|
485
|
+
alignItems: 'flex-end',
|
|
486
|
+
gap: footerGap,
|
|
487
|
+
},
|
|
488
|
+
footerRowCompact: {
|
|
489
|
+
alignSelf: 'stretch',
|
|
490
|
+
flexDirection: 'row',
|
|
491
|
+
alignItems: 'center',
|
|
492
|
+
},
|
|
493
|
+
footerTextWrap: {
|
|
494
|
+
flex: 1,
|
|
495
|
+
minWidth: 1,
|
|
496
|
+
alignItems: 'flex-start',
|
|
497
|
+
gap: textWrapGap,
|
|
498
|
+
},
|
|
499
|
+
titleWrapDefault: {
|
|
500
|
+
flexDirection: 'row',
|
|
501
|
+
alignItems: 'flex-end',
|
|
502
|
+
gap: textWrapGap,
|
|
503
|
+
},
|
|
504
|
+
titleWrapCompact: {
|
|
505
|
+
flexDirection: 'row',
|
|
506
|
+
alignItems: 'flex-end',
|
|
507
|
+
gap: titleWrapGap,
|
|
508
|
+
},
|
|
509
|
+
headline: {
|
|
510
|
+
color: headlineColor,
|
|
511
|
+
fontSize: headlineSize,
|
|
512
|
+
fontFamily: headlineFamily,
|
|
513
|
+
fontWeight: headlineWeight as TextStyle['fontWeight'],
|
|
514
|
+
lineHeight: Math.max(headlineLineHeight, Math.ceil(headlineSize * 1.2)),
|
|
515
|
+
includeFontPadding: false as any,
|
|
516
|
+
},
|
|
517
|
+
footerTitle: {
|
|
518
|
+
color: titleColor,
|
|
519
|
+
fontSize: titleSize,
|
|
520
|
+
fontFamily: titleFamily,
|
|
521
|
+
fontWeight: titleWeight as TextStyle['fontWeight'],
|
|
522
|
+
lineHeight: Math.max(titleLineHeight, Math.ceil(titleSize * 1.2)),
|
|
523
|
+
includeFontPadding: false as any,
|
|
524
|
+
},
|
|
525
|
+
footerSubtitle: {
|
|
526
|
+
color: subtitleColor,
|
|
527
|
+
fontSize: subtitleSize,
|
|
528
|
+
fontFamily: subtitleFamily,
|
|
529
|
+
fontWeight: subtitleWeight as TextStyle['fontWeight'],
|
|
530
|
+
lineHeight: Math.max(subtitleLineHeight, Math.ceil(subtitleSize * 1.2)),
|
|
531
|
+
includeFontPadding: false as any,
|
|
532
|
+
},
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const DEFAULT_BADGES: CcCardBadge[] = [
|
|
537
|
+
{ label: 'Pre-qualified', modes: { AppearanceBrand: 'Tertiary' }, labelColor: '#ffffff' },
|
|
538
|
+
{ label: 'Lifetime free', modes: { AppearanceBrand: 'Tertiary' }, labelColor: '#ffffff' },
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
const DEFAULT_ITEMS: CcCardListItem[] = [
|
|
542
|
+
{ icon: 'ic_card', title: '4 domestic + 2 intl. lounge access yearly' },
|
|
543
|
+
{ icon: 'ic_card', title: '5% cashback on dining & travel' },
|
|
544
|
+
{ icon: 'ic_card', title: 'No annual fee for the first year' },
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
const DEFAULT_NUDGE_SEGMENTS: TextSegmentRun[] = [
|
|
548
|
+
{ text: 'Upsell message ' },
|
|
549
|
+
{ text: 'JioFinance+', modes: { 'Text Appearance': 'Primary' } },
|
|
550
|
+
]
|
|
551
|
+
|
|
552
|
+
const styles = StyleSheet.create({
|
|
553
|
+
header: {
|
|
554
|
+
alignSelf: 'stretch',
|
|
555
|
+
flexDirection: 'row',
|
|
556
|
+
alignItems: 'flex-start',
|
|
557
|
+
justifyContent: 'space-between',
|
|
558
|
+
gap: 8,
|
|
559
|
+
},
|
|
560
|
+
badgeGroup: {
|
|
561
|
+
flexDirection: 'row',
|
|
562
|
+
flexWrap: 'wrap',
|
|
563
|
+
alignItems: 'center',
|
|
564
|
+
gap: 4,
|
|
565
|
+
flexShrink: 1,
|
|
566
|
+
},
|
|
567
|
+
mediaSlot: {
|
|
568
|
+
alignSelf: 'stretch',
|
|
569
|
+
flexDirection: 'row',
|
|
570
|
+
alignItems: 'center',
|
|
571
|
+
gap: 8,
|
|
572
|
+
},
|
|
573
|
+
titleWrap: {
|
|
574
|
+
flex: 1,
|
|
575
|
+
minWidth: 1,
|
|
576
|
+
},
|
|
577
|
+
// The shared Title falls back to 16/8 padding when its tokens resolve to 0
|
|
578
|
+
// (0 is falsy). Zero it out so the only horizontal gap to the media slot is
|
|
579
|
+
// the mediaSlot's hard-coded 8px.
|
|
580
|
+
titleInner: {
|
|
581
|
+
paddingHorizontal: 0,
|
|
582
|
+
paddingVertical: 0,
|
|
583
|
+
},
|
|
584
|
+
// Compact media wrapper hugs its child so the slot sizes to the avatar /
|
|
585
|
+
// image's intrinsic dimensions instead of stretching to the card width.
|
|
586
|
+
compactSlot: {
|
|
587
|
+
alignSelf: 'flex-start',
|
|
588
|
+
},
|
|
589
|
+
nudgeContent: {
|
|
590
|
+
flex: 1,
|
|
591
|
+
minWidth: 1,
|
|
592
|
+
},
|
|
593
|
+
pressed: {
|
|
594
|
+
opacity: 0.92,
|
|
595
|
+
},
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
export default CcCard
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
1
|
+
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Pressable,
|
|
4
4
|
Platform,
|
|
@@ -98,7 +98,7 @@ export interface CheckboxProps {
|
|
|
98
98
|
* @component
|
|
99
99
|
* @param {CheckboxProps} props
|
|
100
100
|
*/
|
|
101
|
-
function Checkbox({
|
|
101
|
+
const Checkbox = forwardRef<View, CheckboxProps>(function Checkbox({
|
|
102
102
|
checked: controlledChecked,
|
|
103
103
|
defaultChecked = false,
|
|
104
104
|
onValueChange,
|
|
@@ -106,7 +106,7 @@ function Checkbox({
|
|
|
106
106
|
modes = EMPTY_MODES,
|
|
107
107
|
style,
|
|
108
108
|
accessibilityLabel,
|
|
109
|
-
}: CheckboxProps) {
|
|
109
|
+
}: CheckboxProps, ref: React.Ref<View>) {
|
|
110
110
|
const isControlled = controlledChecked !== undefined
|
|
111
111
|
const [internalChecked, setInternalChecked] = useState(defaultChecked)
|
|
112
112
|
const isChecked = isControlled ? controlledChecked : internalChecked
|
|
@@ -230,6 +230,7 @@ function Checkbox({
|
|
|
230
230
|
|
|
231
231
|
return (
|
|
232
232
|
<Pressable
|
|
233
|
+
ref={ref}
|
|
233
234
|
style={[touchTargetStyle, style]}
|
|
234
235
|
hitSlop={hitSlop}
|
|
235
236
|
onPress={handlePress}
|
|
@@ -253,7 +254,7 @@ function Checkbox({
|
|
|
253
254
|
</View>
|
|
254
255
|
</Pressable>
|
|
255
256
|
)
|
|
256
|
-
}
|
|
257
|
+
})
|
|
257
258
|
|
|
258
259
|
function boxShadow(value: string): ViewStyle {
|
|
259
260
|
if (Platform.OS === 'web') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useState } from 'react'
|
|
1
|
+
import React, { forwardRef, useCallback, useState } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
@@ -80,7 +80,7 @@ export type CheckboxItemProps = {
|
|
|
80
80
|
* />
|
|
81
81
|
* ```
|
|
82
82
|
*/
|
|
83
|
-
function CheckboxItem({
|
|
83
|
+
const CheckboxItem = forwardRef<View, CheckboxItemProps>(function CheckboxItem({
|
|
84
84
|
checked: controlledChecked,
|
|
85
85
|
defaultChecked = false,
|
|
86
86
|
onValueChange,
|
|
@@ -93,7 +93,7 @@ function CheckboxItem({
|
|
|
93
93
|
style,
|
|
94
94
|
labelStyle,
|
|
95
95
|
accessibilityLabel,
|
|
96
|
-
}: CheckboxItemProps) {
|
|
96
|
+
}: CheckboxItemProps, ref: React.Ref<View>) {
|
|
97
97
|
const isTrailing = control === 'trailing'
|
|
98
98
|
const isControlled = controlledChecked !== undefined
|
|
99
99
|
const [internalChecked, setInternalChecked] = useState(defaultChecked)
|
|
@@ -183,6 +183,7 @@ function CheckboxItem({
|
|
|
183
183
|
|
|
184
184
|
return (
|
|
185
185
|
<Pressable
|
|
186
|
+
ref={ref}
|
|
186
187
|
style={[containerStyle, style]}
|
|
187
188
|
onPress={handleToggle}
|
|
188
189
|
disabled={disabled}
|
|
@@ -205,6 +206,6 @@ function CheckboxItem({
|
|
|
205
206
|
)}
|
|
206
207
|
</Pressable>
|
|
207
208
|
)
|
|
208
|
-
}
|
|
209
|
+
})
|
|
209
210
|
|
|
210
211
|
export default CheckboxItem
|