jfs-components 0.1.23 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/lib/commonjs/components/AutoplayControl/AutoplayControl.js +66 -0
  3. package/lib/commonjs/components/CardCTA/CardCTA.js +32 -25
  4. package/lib/commonjs/components/Carousel/Carousel.js +112 -19
  5. package/lib/commonjs/components/ChipSelect/ChipSelect.js +2 -1
  6. package/lib/commonjs/components/ContentSheet/ContentSheet.js +39 -5
  7. package/lib/commonjs/components/FormUpload/Additem.js +168 -0
  8. package/lib/commonjs/components/FormUpload/FormUpload.js +198 -0
  9. package/lib/commonjs/components/Grid/Grid.js +206 -0
  10. package/lib/commonjs/components/ListItem/ListItem.js +11 -4
  11. package/lib/commonjs/components/MetricData/MetricData.js +132 -0
  12. package/lib/commonjs/components/NumberPagination/NumberPagination.js +144 -0
  13. package/lib/commonjs/components/ProfileCard/ProfileCard.js +208 -0
  14. package/lib/commonjs/components/Rating/Rating.js +137 -0
  15. package/lib/commonjs/components/UpiHandle/UpiHandle.js +23 -11
  16. package/lib/commonjs/components/index.js +42 -0
  17. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  18. package/lib/commonjs/design-tokens/figma-modes.generated.js +23 -10
  19. package/lib/commonjs/icons/components/IconBrokenimage.js +19 -0
  20. package/lib/commonjs/icons/components/IconImage.js +19 -0
  21. package/lib/commonjs/icons/components/IconMic.js +19 -0
  22. package/lib/commonjs/icons/components/IconMicoff.js +19 -0
  23. package/lib/commonjs/icons/components/IconPause.js +19 -0
  24. package/lib/commonjs/icons/components/IconPlay.js +19 -0
  25. package/lib/commonjs/icons/components/index.js +66 -0
  26. package/lib/commonjs/icons/registry.js +2 -2
  27. package/lib/module/components/AutoplayControl/AutoplayControl.js +60 -0
  28. package/lib/module/components/CardCTA/CardCTA.js +32 -25
  29. package/lib/module/components/Carousel/Carousel.js +111 -19
  30. package/lib/module/components/ChipSelect/ChipSelect.js +2 -1
  31. package/lib/module/components/ContentSheet/ContentSheet.js +41 -7
  32. package/lib/module/components/FormUpload/Additem.js +162 -0
  33. package/lib/module/components/FormUpload/FormUpload.js +192 -0
  34. package/lib/module/components/Grid/Grid.js +201 -0
  35. package/lib/module/components/ListItem/ListItem.js +11 -4
  36. package/lib/module/components/MetricData/MetricData.js +127 -0
  37. package/lib/module/components/NumberPagination/NumberPagination.js +138 -0
  38. package/lib/module/components/ProfileCard/ProfileCard.js +202 -0
  39. package/lib/module/components/Rating/Rating.js +132 -0
  40. package/lib/module/components/UpiHandle/UpiHandle.js +23 -11
  41. package/lib/module/components/index.js +7 -1
  42. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  43. package/lib/module/design-tokens/figma-modes.generated.js +23 -10
  44. package/lib/module/icons/components/IconBrokenimage.js +12 -0
  45. package/lib/module/icons/components/IconImage.js +12 -0
  46. package/lib/module/icons/components/IconMic.js +12 -0
  47. package/lib/module/icons/components/IconMicoff.js +12 -0
  48. package/lib/module/icons/components/IconPause.js +12 -0
  49. package/lib/module/icons/components/IconPlay.js +12 -0
  50. package/lib/module/icons/components/index.js +6 -0
  51. package/lib/module/icons/registry.js +2 -2
  52. package/lib/typescript/src/components/AutoplayControl/AutoplayControl.d.ts +15 -0
  53. package/lib/typescript/src/components/Carousel/Carousel.d.ts +8 -1
  54. package/lib/typescript/src/components/ChipSelect/ChipSelect.d.ts +6 -1
  55. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
  56. package/lib/typescript/src/components/FormUpload/Additem.d.ts +96 -0
  57. package/lib/typescript/src/components/FormUpload/FormUpload.d.ts +85 -0
  58. package/lib/typescript/src/components/Grid/Grid.d.ts +87 -0
  59. package/lib/typescript/src/components/MetricData/MetricData.d.ts +53 -0
  60. package/lib/typescript/src/components/NumberPagination/NumberPagination.d.ts +21 -0
  61. package/lib/typescript/src/components/ProfileCard/ProfileCard.d.ts +89 -0
  62. package/lib/typescript/src/components/Rating/Rating.d.ts +30 -0
  63. package/lib/typescript/src/components/UpiHandle/UpiHandle.d.ts +7 -1
  64. package/lib/typescript/src/components/index.d.ts +34 -28
  65. package/lib/typescript/src/design-tokens/figma-modes.generated.d.ts +10 -4
  66. package/lib/typescript/src/icons/components/IconBrokenimage.d.ts +3 -0
  67. package/lib/typescript/src/icons/components/IconImage.d.ts +3 -0
  68. package/lib/typescript/src/icons/components/IconMic.d.ts +3 -0
  69. package/lib/typescript/src/icons/components/IconMicoff.d.ts +3 -0
  70. package/lib/typescript/src/icons/components/IconPause.d.ts +3 -0
  71. package/lib/typescript/src/icons/components/IconPlay.d.ts +3 -0
  72. package/lib/typescript/src/icons/components/index.d.ts +6 -0
  73. package/lib/typescript/src/icons/registry.d.ts +1 -1
  74. package/package.json +1 -1
  75. package/src/components/AutoplayControl/AutoplayControl.tsx +77 -0
  76. package/src/components/CardCTA/CardCTA.tsx +30 -15
  77. package/src/components/Carousel/Carousel.tsx +133 -23
  78. package/src/components/ChipSelect/ChipSelect.tsx +7 -1
  79. package/src/components/ContentSheet/ContentSheet.tsx +50 -0
  80. package/src/components/FormUpload/Additem.tsx +262 -0
  81. package/src/components/FormUpload/FormUpload.tsx +313 -0
  82. package/src/components/Grid/Grid.tsx +368 -0
  83. package/src/components/ListItem/ListItem.tsx +11 -4
  84. package/src/components/MetricData/MetricData.tsx +185 -0
  85. package/src/components/NumberPagination/NumberPagination.tsx +170 -0
  86. package/src/components/ProfileCard/ProfileCard.tsx +268 -0
  87. package/src/components/Rating/Rating.tsx +174 -0
  88. package/src/components/UpiHandle/UpiHandle.tsx +35 -24
  89. package/src/components/index.ts +329 -182
  90. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  91. package/src/design-tokens/figma-modes.generated.ts +23 -10
  92. package/src/icons/components/IconBrokenimage.tsx +11 -0
  93. package/src/icons/components/IconImage.tsx +11 -0
  94. package/src/icons/components/IconMic.tsx +11 -0
  95. package/src/icons/components/IconMicoff.tsx +11 -0
  96. package/src/icons/components/IconPause.tsx +11 -0
  97. package/src/icons/components/IconPlay.tsx +11 -0
  98. package/src/icons/components/index.ts +6 -0
  99. package/src/icons/registry.ts +25 -1
@@ -0,0 +1,185 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ type TextStyle,
8
+ } from 'react-native'
9
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
10
+ import { EMPTY_MODES } from '../../utils/react-utils'
11
+ import Icon from '../Icon/Icon'
12
+ import type { Modes } from '../../design-tokens'
13
+
14
+ const LINE_HEIGHT_RATIO = 1.2
15
+ const ICON_SIZE = 18
16
+ // Figma renders the value-row icon with no surrounding padding; the shared Icon
17
+ // component pulls padding from `icon/padding/*` tokens, so we zero it here.
18
+ const ICON_PADDING_RESET: ViewStyle = {
19
+ paddingLeft: 0,
20
+ paddingRight: 0,
21
+ paddingTop: 0,
22
+ paddingBottom: 0,
23
+ }
24
+
25
+ export type MetricDataProps = {
26
+ /** Top label (small, 12px). Hidden when omitted. */
27
+ title?: string
28
+ /**
29
+ * The prominent value (20px bold). Pass a string, or any node (e.g. a
30
+ * `MoneyValue`) to compose a richer value.
31
+ */
32
+ value?: string | React.ReactNode
33
+ /** Sub-label below the value (12px, muted). Hidden when omitted. */
34
+ caption?: string
35
+ /**
36
+ * Optional glyph shown to the left of the value. Pass a registry icon name
37
+ * (e.g. `'ic_card'`) or a custom node. Rendered at 18px, tinted to the value
38
+ * colour. Hidden when omitted.
39
+ */
40
+ icon?: string | React.ReactNode
41
+ /** Design-token modes for theming. */
42
+ modes?: Modes
43
+ /** Override the container styles. */
44
+ style?: StyleProp<ViewStyle>
45
+ /** Override the title text styles. */
46
+ titleStyle?: StyleProp<TextStyle>
47
+ /** Override the value text styles. */
48
+ valueStyle?: StyleProp<TextStyle>
49
+ /** Override the caption text styles. */
50
+ captionStyle?: StyleProp<TextStyle>
51
+ /**
52
+ * Accessibility label. Defaults to the resolved `title`, `value` and
53
+ * `caption` text joined together.
54
+ */
55
+ accessibilityLabel?: string
56
+ }
57
+
58
+ /**
59
+ * MetricData — a compact, centered metric block.
60
+ *
61
+ * Stacks a small `title`, a prominent `value` (with an optional leading
62
+ * `icon`), and a muted `caption`. Typography, colours, gaps and padding all
63
+ * resolve from the `metricdata/*` design tokens. `title`, `caption` and `icon`
64
+ * are optional; only `value` is shown by default.
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * <MetricData title="Balance" value="₹1,20,000" caption="as of today" />
69
+ * <MetricData title="Cards" value="12" icon="ic_card" caption="active" />
70
+ * ```
71
+ */
72
+ function MetricData({
73
+ title,
74
+ value,
75
+ caption,
76
+ icon,
77
+ modes = EMPTY_MODES,
78
+ style,
79
+ titleStyle,
80
+ valueStyle,
81
+ captionStyle,
82
+ accessibilityLabel,
83
+ }: MetricDataProps) {
84
+ const titleColor = (getVariableByName('metricdata/title/color', modes) as string | null) ?? '#000000'
85
+ const titleFontFamily = (getVariableByName('metricdata/title/fontfamily', modes) as string | null) ?? 'JioType Var'
86
+ const titleFontSize = (getVariableByName('metricdata/title/fontsize', modes) as number | null) ?? 12
87
+ const titleFontWeight = String(getVariableByName('metricdata/title/fontweight', modes) ?? 400) as TextStyle['fontWeight']
88
+
89
+ const valueColor = (getVariableByName('metricdata/value/color', modes) as string | null) ?? '#000000'
90
+ const valueFontFamily = (getVariableByName('metricdata/value/fontfamily', modes) as string | null) ?? 'JioType Var'
91
+ const valueFontSize = (getVariableByName('metricdata/value/fontsize', modes) as number | null) ?? 20
92
+ const valueFontWeight = String(getVariableByName('metricdata/value/fontweight', modes) ?? 700) as TextStyle['fontWeight']
93
+
94
+ const captionColor = (getVariableByName('metricdata/caption/color', modes) as string | null) ?? '#777777'
95
+ const captionFontFamily = (getVariableByName('metricdata/caption/fontfamily', modes) as string | null) ?? 'JioType Var'
96
+ const captionFontSize = (getVariableByName('metricdata/caption/fontsize', modes) as number | null) ?? 12
97
+ const captionFontWeight = String(getVariableByName('metricdata/caption/fontweight', modes) ?? 500) as TextStyle['fontWeight']
98
+
99
+ const gap = (getVariableByName('metricdata/gap', modes) as number | null) ?? 4
100
+ const valueWrapGap = (getVariableByName('metricdata/valueWrap/gap', modes) as number | null) ?? 4
101
+ const paddingHorizontal = (getVariableByName('metricdata/padding/horizontal', modes) as number | null) ?? 10
102
+ const paddingVertical = (getVariableByName('metricdata/padding/vertical', modes) as number | null) ?? 10
103
+
104
+ const containerStyle: ViewStyle = {
105
+ flexDirection: 'column',
106
+ alignItems: 'center',
107
+ justifyContent: 'center',
108
+ gap,
109
+ paddingHorizontal,
110
+ paddingVertical,
111
+ }
112
+
113
+ const titleTextStyle: TextStyle = {
114
+ color: titleColor,
115
+ fontFamily: titleFontFamily,
116
+ fontSize: titleFontSize,
117
+ fontWeight: titleFontWeight,
118
+ lineHeight: titleFontSize * LINE_HEIGHT_RATIO,
119
+ textAlign: 'center',
120
+ }
121
+
122
+ const valueTextStyle: TextStyle = {
123
+ color: valueColor,
124
+ fontFamily: valueFontFamily,
125
+ fontSize: valueFontSize,
126
+ fontWeight: valueFontWeight,
127
+ lineHeight: valueFontSize * LINE_HEIGHT_RATIO,
128
+ textAlign: 'center',
129
+ }
130
+
131
+ const captionTextStyle: TextStyle = {
132
+ color: captionColor,
133
+ fontFamily: captionFontFamily,
134
+ fontSize: captionFontSize,
135
+ fontWeight: captionFontWeight,
136
+ lineHeight: captionFontSize * LINE_HEIGHT_RATIO,
137
+ textAlign: 'center',
138
+ }
139
+
140
+ const iconNode =
141
+ icon == null ? null : typeof icon === 'string' ? (
142
+ <Icon
143
+ iconName={icon}
144
+ size={ICON_SIZE}
145
+ color={valueColor}
146
+ modes={modes}
147
+ style={ICON_PADDING_RESET}
148
+ />
149
+ ) : (
150
+ icon
151
+ )
152
+
153
+ const resolvedLabel =
154
+ accessibilityLabel ??
155
+ ([title, typeof value === 'string' ? value : undefined, caption]
156
+ .filter(Boolean)
157
+ .join(', ') || undefined)
158
+
159
+ return (
160
+ <View
161
+ style={[containerStyle, style]}
162
+ accessible
163
+ {...(resolvedLabel !== undefined ? { accessibilityLabel: resolvedLabel } : null)}
164
+ >
165
+ {title !== undefined && title !== '' ? (
166
+ <Text style={[titleTextStyle, titleStyle]}>{title}</Text>
167
+ ) : null}
168
+
169
+ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: valueWrapGap }}>
170
+ {iconNode}
171
+ {typeof value === 'string' ? (
172
+ <Text style={[valueTextStyle, valueStyle]}>{value}</Text>
173
+ ) : (
174
+ value
175
+ )}
176
+ </View>
177
+
178
+ {caption !== undefined && caption !== '' ? (
179
+ <Text style={[captionTextStyle, captionStyle]}>{caption}</Text>
180
+ ) : null}
181
+ </View>
182
+ )
183
+ }
184
+
185
+ export default React.memo(MetricData)
@@ -0,0 +1,170 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ Pressable,
4
+ Text,
5
+ View,
6
+ type StyleProp,
7
+ type TextStyle,
8
+ type ViewStyle,
9
+ } from 'react-native'
10
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
+ import GlassFill from '../../utils/GlassFill/GlassFill'
12
+ import type { Modes } from '../../design-tokens'
13
+
14
+ export type NumberPaginationProps = {
15
+ /** The 1-based active page index. Ignored if `children` is supplied. Defaults to 1. */
16
+ activePage?: number;
17
+ /** The total number of pages. Ignored if `children` is supplied. Defaults to 1. */
18
+ totalPages?: number;
19
+ /** Callback fired when a page button is pressed. Ignored if `children` is supplied. */
20
+ onPageChange?: (page: number) => void;
21
+ /** Custom children to override the default page buttons. */
22
+ children?: React.ReactNode;
23
+ modes?: Modes;
24
+ style?: StyleProp<ViewStyle>;
25
+ }
26
+
27
+ interface NumberPaginationTokens {
28
+ containerStyle: ViewStyle;
29
+ activeItemStyle: ViewStyle;
30
+ inactiveItemStyle: ViewStyle;
31
+ activeTextStyle: TextStyle;
32
+ inactiveTextStyle: TextStyle;
33
+ blurIntensity: number;
34
+ backgroundColor: string;
35
+ }
36
+
37
+ function resolveNumberPaginationTokens(modes: Modes): NumberPaginationTokens {
38
+ const horizontalPadding = (getVariableByName('numberPagination/padding/horizontal', modes) ?? 4) as number
39
+ const verticalPadding = (getVariableByName('numberPagination/padding/vertical', modes) ?? 4) as number
40
+ const radiusRaw = (getVariableByName('numberPagination/radius', modes) ?? 9999) as number
41
+ const backgroundColor = (getVariableByName('numberPagination/background/color', modes) ?? 'rgba(141, 141, 141, 0.4)') as string
42
+ const borderColor = (getVariableByName('numberPagination/border/color', modes) ?? 'rgba(255, 255, 255, 0.15)') as string
43
+
44
+ // control number variables
45
+ const itemWidth = (getVariableByName('controlNumber/width', modes) ?? 32) as number
46
+ const itemHeight = (getVariableByName('controlNumber/height', modes) ?? 32) as number
47
+ const itemRadiusRaw = (getVariableByName('controlNumber/radius', modes) ?? 9999) as number
48
+ const activeBgColor = (getVariableByName('control/active/background/color', modes) ?? '#ffffff') as string
49
+ const activeTextColor = (getVariableByName('control/active/text/color', modes) ?? '#000000') as string
50
+ const inactiveTextColor = (getVariableByName('control/text/default', modes) ?? '#ffffff') as string
51
+
52
+ // typography variables
53
+ const fontFamily = (getVariableByName('numberPagination/fontFamily', modes) ?? 'JioType') as string
54
+ const fontSize = (getVariableByName('numberPagination/fontSize', modes) ?? 16) as number
55
+ const fontWeight = String(getVariableByName('numberPagination/fontWeight', modes) ?? '500') as TextStyle['fontWeight']
56
+ const lineHeight = (getVariableByName('numberPagination/lineHeight', modes) ?? 18) as number
57
+
58
+ const blurMinimal = (getVariableByName('blur/minimal', modes) ?? 29) as number
59
+ const blurIntensity = Math.max(0, Math.min(100, Math.round(Number(blurMinimal))))
60
+
61
+ const borderRadius = radiusRaw === 9999 ? 39 / 2 : radiusRaw
62
+ const itemRadius = itemRadiusRaw === 9999 ? itemWidth / 2 : itemRadiusRaw
63
+
64
+ return {
65
+ containerStyle: {
66
+ height: 39,
67
+ borderRadius,
68
+ borderWidth: 1,
69
+ borderColor,
70
+ backgroundColor: 'transparent',
71
+ overflow: 'hidden',
72
+ flexDirection: 'row',
73
+ alignItems: 'center',
74
+ paddingHorizontal: horizontalPadding,
75
+ paddingVertical: verticalPadding,
76
+ },
77
+ activeItemStyle: {
78
+ width: itemWidth,
79
+ height: itemHeight,
80
+ borderRadius: itemRadius,
81
+ backgroundColor: activeBgColor,
82
+ alignItems: 'center',
83
+ justifyContent: 'center',
84
+ },
85
+ inactiveItemStyle: {
86
+ width: itemWidth,
87
+ height: itemHeight,
88
+ borderRadius: itemRadius,
89
+ backgroundColor: 'transparent',
90
+ alignItems: 'center',
91
+ justifyContent: 'center',
92
+ },
93
+ activeTextStyle: {
94
+ fontFamily,
95
+ fontSize,
96
+ fontWeight,
97
+ lineHeight,
98
+ color: activeTextColor,
99
+ textAlign: 'center',
100
+ },
101
+ inactiveTextStyle: {
102
+ fontFamily,
103
+ fontSize,
104
+ fontWeight,
105
+ lineHeight,
106
+ color: inactiveTextColor,
107
+ textAlign: 'center',
108
+ },
109
+ blurIntensity,
110
+ backgroundColor,
111
+ }
112
+ }
113
+
114
+ /**
115
+ * NumberPagination component displays a horizontal list of page numbers on a frosted glass surface.
116
+ * All styling is driven by Figma design tokens with support for custom children slot.
117
+ */
118
+ export default function NumberPagination({
119
+ activePage = 1,
120
+ totalPages = 1,
121
+ onPageChange,
122
+ children,
123
+ modes,
124
+ style,
125
+ }: NumberPaginationProps) {
126
+ const tokens = useMemo(() => resolveNumberPaginationTokens(modes ?? {}), [modes])
127
+
128
+ const renderPages = () => {
129
+ if (children) {
130
+ return children
131
+ }
132
+
133
+ const pages = []
134
+ for (let i = 1; i <= totalPages; i++) {
135
+ const active = i === activePage
136
+ pages.push(
137
+ <Pressable
138
+ key={i}
139
+ onPress={() => onPageChange?.(i)}
140
+ style={({ pressed }) => [
141
+ active ? tokens.activeItemStyle : tokens.inactiveItemStyle,
142
+ pressed && { opacity: 0.7 },
143
+ ]}
144
+ accessibilityRole="button"
145
+ accessibilityState={{ selected: active }}
146
+ accessibilityLabel={`Page ${i}`}
147
+ >
148
+ <Text style={active ? tokens.activeTextStyle : tokens.inactiveTextStyle}>
149
+ {i}
150
+ </Text>
151
+ </Pressable>
152
+ )
153
+ }
154
+ return pages
155
+ }
156
+
157
+ return (
158
+ <View style={[tokens.containerStyle, style]}>
159
+ <GlassFill
160
+ tint="light"
161
+ intensity={tokens.blurIntensity}
162
+ overlayColor={tokens.backgroundColor}
163
+ androidTintWash={false}
164
+ />
165
+ <View style={{ flexDirection: 'row', alignItems: 'center', height: '100%' }}>
166
+ {renderPages()}
167
+ </View>
168
+ </View>
169
+ )
170
+ }
@@ -0,0 +1,268 @@
1
+ import React, { useMemo } from 'react'
2
+ import {
3
+ View,
4
+ type StyleProp,
5
+ type ViewStyle,
6
+ type ImageSourcePropType,
7
+ } from 'react-native'
8
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
9
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
10
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils'
11
+ import Avatar from '../Avatar/Avatar'
12
+ import Attached from '../Attached/Attached'
13
+ import IconCapsule from '../IconCapsule/IconCapsule'
14
+ import ListItem from '../ListItem/ListItem'
15
+ import UpiHandle from '../UpiHandle/UpiHandle'
16
+ import Divider from '../Divider/Divider'
17
+ import type { Modes } from '../../design-tokens'
18
+
19
+ /**
20
+ * A single UPI handle pill rendered under the profile name.
21
+ */
22
+ export interface ProfileCardUpiHandle {
23
+ /** Handle text, e.g. `"username@jio"` or a phone number. */
24
+ label: string
25
+ /** Trailing registry icon (e.g. `'ic_copy'`, `'ic_confirm'`). */
26
+ iconName?: string
27
+ /** Toggles the trailing icon. Defaults to `true`. */
28
+ showIcon?: boolean
29
+ /** Makes the pill pressable (e.g. copy-to-clipboard). */
30
+ onPress?: () => void
31
+ }
32
+
33
+ export interface ProfileCardProps {
34
+ /** User's display name shown in the header. */
35
+ name?: string
36
+ /** Monogram initials for the avatar (used when `avatarSource` is not set). */
37
+ monogram?: string
38
+ /** Avatar image source. When set, the avatar renders the image instead of the monogram. */
39
+ avatarSource?: ImageSourcePropType | string
40
+ /**
41
+ * Registry icon for the small badge attached to the avatar's bottom-right
42
+ * corner. Pass `null` to hide the badge. Defaults to `'ic_qr_code'`.
43
+ */
44
+ badgeIcon?: string | null
45
+ /** UPI handle pills rendered under the name. Pass `[]` to render none. */
46
+ upiHandles?: ProfileCardUpiHandle[]
47
+ /**
48
+ * Full override for the header row (avatar + name + handles). Takes
49
+ * precedence over the declarative header props. `modes` cascade into it.
50
+ */
51
+ header?: React.ReactNode
52
+ /**
53
+ * Card body (the Figma "slot"). Rendered below the header divider; `modes`
54
+ * (including the profile-card context) cascade into every child, so nested
55
+ * `ListItem`s, `Divider`s, `Text`s and `Button`s resolve the on-dark
56
+ * profile-card token values automatically.
57
+ */
58
+ children?: React.ReactNode
59
+ /** Toggles the divider between the header and the body. Defaults to `true`. */
60
+ showDivider?: boolean
61
+ /** Card width. Defaults to the Figma spec (`344`). Pass `'100%'` to fill the parent. */
62
+ width?: number | `${number}%`
63
+ /** Modes object for design-token resolution, cascaded to all children. */
64
+ modes?: Modes
65
+ /** Style overrides for the card container. */
66
+ style?: StyleProp<ViewStyle>
67
+ /** Accessibility label for the card. */
68
+ accessibilityLabel?: string
69
+ }
70
+
71
+ // Forced modes for everything rendered inside the card — the profile card is
72
+ // an on-dark (jioPlus) surface, so child components (Avatar, UpiHandle,
73
+ // ListItem, Divider, Text, Button, …) must resolve their tokens through the
74
+ // `context 10: jioPlus` mode. Consumer modes are spread first so appearance
75
+ // modes (e.g. `Profile Card Appearance: Premium`) still cascade through.
76
+ const CARD_CONTEXT: Modes = Object.freeze({ 'context 10': 'jioPlus' })
77
+
78
+ const DEFAULT_UPI_HANDLES: ProfileCardUpiHandle[] = [
79
+ { label: 'username@jio', iconName: 'ic_copy' },
80
+ { label: '9999999999', iconName: 'ic_confirm' },
81
+ ]
82
+
83
+ interface ProfileCardTokens {
84
+ containerStyle: ViewStyle
85
+ handlesRowStyle: ViewStyle
86
+ badgeBackground: string
87
+ childModes: Modes
88
+ badgeModes: Modes
89
+ }
90
+
91
+ function resolveProfileCardTokens(modes: Modes): ProfileCardTokens {
92
+ const backgroundColor = (getVariableByName('profileCard/bg', modes) ?? '#00065a') as string
93
+ const gap = (getVariableByName('profileCard/gap', modes) ?? 12) as number
94
+ const borderRadius = (getVariableByName('profileCard/radius', modes) ?? 12) as number
95
+ const paddingHorizontal = (getVariableByName('profileCard/padding/horizontal', modes) ?? 20) as number
96
+ const paddingVertical = (getVariableByName('profileCard/padding/vertical', modes) ?? 20) as number
97
+
98
+ const childModes: Modes = { ...modes, ...CARD_CONTEXT }
99
+
100
+ // The UPI handle pills sit in the header ListItem's support slot, whose row
101
+ // gap is the `slot/gap` token in the ListItem context.
102
+ const handlesGap = (getVariableByName('slot/gap', { ...childModes, Context: 'ListItem' }) ?? 2) as number
103
+
104
+ // The avatar badge is a white circle: the fill comes from the Attached
105
+ // slot's background token (the IconCapsule itself resolves transparent).
106
+ const badgeBackground = (getVariableByName('attached/slot/background', childModes) ?? '#ffffff') as string
107
+
108
+ return {
109
+ containerStyle: {
110
+ backgroundColor,
111
+ borderRadius,
112
+ paddingHorizontal,
113
+ paddingVertical,
114
+ gap,
115
+ flexDirection: 'column',
116
+ alignItems: 'stretch',
117
+ },
118
+ handlesRowStyle: {
119
+ flexDirection: 'row',
120
+ flexWrap: 'wrap',
121
+ alignItems: 'center',
122
+ alignSelf: 'flex-start',
123
+ gap: handlesGap,
124
+ },
125
+ badgeBackground,
126
+ childModes,
127
+ // XS capsule per the Figma spec; consumers can still override the size
128
+ // through their own modes.
129
+ badgeModes: { 'Icon Capsule Size': 'XS', ...modes, ...CARD_CONTEXT },
130
+ }
131
+ }
132
+
133
+ /**
134
+ * ProfileCard — Figma node 6914:38498 ("Profile Card").
135
+ *
136
+ * A dark, rounded profile summary card composed from the shared primitives:
137
+ *
138
+ * - **Header** — a {@link ListItem} whose leading slot is an {@link Attached}
139
+ * monogram {@link Avatar} with a small {@link IconCapsule} badge at its
140
+ * bottom-right, the user's name as the title, and a row of
141
+ * {@link UpiHandle} pills in the support slot.
142
+ * - **Divider** — separates the header from the body.
143
+ * - **Body slot** — arbitrary children (typically `ListItem`s separated by
144
+ * `Divider`s). All profile-card context modes cascade into the slot, so
145
+ * nested components automatically resolve their on-dark token values.
146
+ *
147
+ * The card supports the `Profile Card Appearance` collection
148
+ * (`Default` | `Premium`) via the `modes` prop, which recolors the container,
149
+ * avatar, pills and divider in one go.
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * <ProfileCard
154
+ * name="Shruti Chandarshekhar Rai"
155
+ * monogram="SR"
156
+ * upiHandles={[
157
+ * { label: 'shrutirai-1@jio', iconName: 'ic_copy', onPress: copyHandle },
158
+ * { label: '9184844184', iconName: 'ic_confirm' },
159
+ * ]}
160
+ * >
161
+ * <ListItem layout="Horizontal" title="JioFinance+" navArrow={false} />
162
+ * </ProfileCard>
163
+ * ```
164
+ */
165
+ function ProfileCard({
166
+ name = 'Name',
167
+ monogram = 'MS',
168
+ avatarSource,
169
+ badgeIcon = 'ic_qr_code',
170
+ upiHandles = DEFAULT_UPI_HANDLES,
171
+ header,
172
+ children,
173
+ showDivider = true,
174
+ width = 344,
175
+ modes: propModes = EMPTY_MODES,
176
+ style,
177
+ accessibilityLabel,
178
+ }: ProfileCardProps) {
179
+ const { modes: globalModes } = useTokens()
180
+
181
+ const modes = useMemo(
182
+ () => (globalModes === EMPTY_MODES && propModes === EMPTY_MODES
183
+ ? EMPTY_MODES
184
+ : { ...globalModes, ...propModes }),
185
+ [globalModes, propModes]
186
+ )
187
+
188
+ const tokens = useMemo(() => resolveProfileCardTokens(modes), [modes])
189
+
190
+ // ---- Header -------------------------------------------------------------
191
+ const headerNode = useMemo(() => {
192
+ if (header) return cloneChildrenWithModes(header, tokens.childModes)
193
+
194
+ const avatar = (
195
+ <Attached
196
+ position="bottom-right"
197
+ modes={tokens.childModes}
198
+ badge={
199
+ badgeIcon != null ? (
200
+ <IconCapsule
201
+ iconName={badgeIcon}
202
+ modes={tokens.badgeModes}
203
+ style={{ backgroundColor: tokens.badgeBackground }}
204
+ />
205
+ ) : null
206
+ }
207
+ >
208
+ <Avatar
209
+ monogram={monogram}
210
+ style={avatarSource ? 'Image' : 'Monogram'}
211
+ {...(avatarSource != null ? { imageSource: avatarSource } : {})}
212
+ modes={tokens.childModes}
213
+ />
214
+ </Attached>
215
+ )
216
+
217
+ const handles = upiHandles.length > 0 ? (
218
+ <View style={tokens.handlesRowStyle}>
219
+ {upiHandles.map((handle, index) => (
220
+ <UpiHandle
221
+ key={`${handle.label}-${index}`}
222
+ label={handle.label}
223
+ {...(handle.iconName != null ? { iconName: handle.iconName } : {})}
224
+ showIcon={handle.showIcon ?? true}
225
+ {...(handle.onPress != null ? { onPress: handle.onPress } : {})}
226
+ modes={tokens.childModes}
227
+ />
228
+ ))}
229
+ </View>
230
+ ) : null
231
+
232
+ return (
233
+ <ListItem
234
+ layout="Horizontal"
235
+ title={name}
236
+ navArrow={false}
237
+ showSupportText={handles != null}
238
+ supportSlot={handles}
239
+ leading={avatar}
240
+ modes={tokens.childModes}
241
+ />
242
+ )
243
+ }, [header, name, monogram, avatarSource, badgeIcon, upiHandles, tokens])
244
+
245
+ // ---- Body slot ----------------------------------------------------------
246
+ const bodyChildren = useMemo(
247
+ () => (children != null ? cloneChildrenWithModes(children, tokens.childModes) : null),
248
+ [children, tokens.childModes]
249
+ )
250
+ const hasBody = bodyChildren != null && bodyChildren.length > 0
251
+
252
+ const containerStyle = useMemo<ViewStyle>(
253
+ () => ({ ...tokens.containerStyle, width }),
254
+ [tokens.containerStyle, width]
255
+ )
256
+
257
+ return (
258
+ <View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
259
+ {headerNode}
260
+ {hasBody && showDivider ? (
261
+ <Divider direction="horizontal" modes={tokens.childModes} />
262
+ ) : null}
263
+ {bodyChildren}
264
+ </View>
265
+ )
266
+ }
267
+
268
+ export default React.memo(ProfileCard)