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.
Files changed (90) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/commonjs/components/ActionFooter/ActionFooter.js +319 -135
  3. package/lib/commonjs/components/Badge/Badge.js +20 -1
  4. package/lib/commonjs/components/Button/Button.js +139 -47
  5. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
  6. package/lib/commonjs/components/Card/Card.js +151 -112
  7. package/lib/commonjs/components/Card/assets/gold-logo-mark.png +0 -0
  8. package/lib/commonjs/components/CategoryCard/CategoryCard.js +97 -63
  9. package/lib/commonjs/components/CategoryCard/assets/sample.png +0 -0
  10. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +59 -10
  11. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +132 -49
  12. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +56 -14
  13. package/lib/commonjs/components/IconCapsule/IconCapsule.js +1 -1
  14. package/lib/commonjs/components/MetricData/MetricData.js +26 -13
  15. package/lib/commonjs/components/MoneyValue/MoneyValue.js +1 -1
  16. package/lib/commonjs/components/Nudge/Nudge.js +1 -1
  17. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +47 -91
  18. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  19. package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +1 -1
  20. package/lib/commonjs/components/Slider/Slider.js +3 -3
  21. package/lib/commonjs/components/TextInput/TextInput.js +26 -8
  22. package/lib/commonjs/components/Tooltip/Tooltip.js +3 -5
  23. package/lib/commonjs/components/index.js +7 -1
  24. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  25. package/lib/commonjs/design-tokens/figma-modes.generated.js +13 -13
  26. package/lib/commonjs/icons/registry.js +1 -1
  27. package/lib/module/components/ActionFooter/ActionFooter.js +322 -138
  28. package/lib/module/components/Badge/Badge.js +20 -1
  29. package/lib/module/components/Button/Button.js +140 -48
  30. package/lib/module/components/ButtonGroup/ButtonGroup.js +1 -1
  31. package/lib/module/components/Card/Card.js +152 -114
  32. package/lib/module/components/Card/assets/gold-logo-mark.png +0 -0
  33. package/lib/module/components/CategoryCard/CategoryCard.js +97 -63
  34. package/lib/module/components/CategoryCard/assets/sample.png +0 -0
  35. package/lib/module/components/CheckboxItem/CheckboxItem.js +59 -10
  36. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +135 -52
  37. package/lib/module/components/HelloJioInput/HelloJioInput.js +56 -14
  38. package/lib/module/components/IconCapsule/IconCapsule.js +1 -1
  39. package/lib/module/components/MetricData/MetricData.js +26 -14
  40. package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
  41. package/lib/module/components/Nudge/Nudge.js +1 -1
  42. package/lib/module/components/PdpCcCard/PdpCcCard.js +48 -92
  43. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  44. package/lib/module/components/PortfolioHero/PortfolioHero.js +1 -1
  45. package/lib/module/components/Slider/Slider.js +3 -3
  46. package/lib/module/components/TextInput/TextInput.js +27 -9
  47. package/lib/module/components/Tooltip/Tooltip.js +3 -5
  48. package/lib/module/components/index.js +1 -1
  49. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  50. package/lib/module/design-tokens/figma-modes.generated.js +13 -13
  51. package/lib/module/icons/registry.js +1 -1
  52. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +50 -21
  53. package/lib/typescript/src/components/Button/Button.d.ts +24 -1
  54. package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +2 -2
  55. package/lib/typescript/src/components/Card/Card.d.ts +54 -25
  56. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +29 -12
  57. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +27 -9
  58. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +37 -12
  59. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +1 -1
  60. package/lib/typescript/src/components/MetricData/MetricData.d.ts +7 -5
  61. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +16 -6
  62. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +9 -2
  63. package/lib/typescript/src/components/index.d.ts +2 -2
  64. package/lib/typescript/src/icons/registry.d.ts +1 -1
  65. package/package.json +1 -1
  66. package/src/components/ActionFooter/ActionFooter.tsx +375 -141
  67. package/src/components/Badge/Badge.tsx +21 -1
  68. package/src/components/Button/Button.tsx +192 -44
  69. package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
  70. package/src/components/Card/Card.tsx +370 -212
  71. package/src/components/Card/assets/gold-logo-mark.png +0 -0
  72. package/src/components/CategoryCard/CategoryCard.tsx +130 -84
  73. package/src/components/CategoryCard/assets/sample.png +0 -0
  74. package/src/components/CheckboxItem/CheckboxItem.tsx +72 -12
  75. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +186 -67
  76. package/src/components/HelloJioInput/HelloJioInput.tsx +58 -17
  77. package/src/components/IconCapsule/IconCapsule.tsx +1 -1
  78. package/src/components/MetricData/MetricData.tsx +39 -22
  79. package/src/components/MoneyValue/MoneyValue.tsx +1 -1
  80. package/src/components/Nudge/Nudge.tsx +1 -1
  81. package/src/components/PdpCcCard/PdpCcCard.tsx +41 -92
  82. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +73 -58
  83. package/src/components/PortfolioHero/PortfolioHero.tsx +1 -1
  84. package/src/components/Slider/Slider.tsx +3 -3
  85. package/src/components/TextInput/TextInput.tsx +26 -9
  86. package/src/components/Tooltip/Tooltip.tsx +3 -5
  87. package/src/components/index.ts +9 -2
  88. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  89. package/src/design-tokens/figma-modes.generated.ts +13 -13
  90. package/src/icons/registry.ts +1 -1
@@ -1,179 +1,292 @@
1
- import React, { createContext, useContext, isValidElement, cloneElement, useMemo } from 'react';
2
- import { View, Text, Pressable, type ViewStyle, type TextStyle, type StyleProp } from 'react-native';
3
- import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
4
- import { EMPTY_MODES } from '../../utils/react-utils';
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useMemo,
5
+ } from 'react'
6
+ import {
7
+ View,
8
+ Text,
9
+ Pressable,
10
+ type ViewStyle,
11
+ type TextStyle,
12
+ type StyleProp,
13
+ } from 'react-native'
14
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
15
+ import {
16
+ EMPTY_MODES,
17
+ cloneChildrenWithModes,
18
+ } from '../../utils/react-utils'
5
19
  import type { Modes } from '../../design-tokens'
6
20
 
7
21
  /**
8
- * Context to share 'modes' with child components like Card.Title and Card.SupportText.
9
- * This ensures that nested components can resolve their tokens correctly without
10
- * needing explicit mode prop passing for every child.
22
+ * Context so `Card.Title` / `Card.SupportText` resolve tokens from the parent
23
+ * card's `modes` without every call site having to pass them explicitly.
11
24
  */
12
- const CardContext = createContext<{ modes?: Modes }>({});
25
+ const CardContext = createContext<{ modes: Modes }>({ modes: EMPTY_MODES })
26
+
27
+ /** Canonical variant values. Maps 1:1 to Figma `Property 1`. */
28
+ export type CardVariant = 'default' | 'slim'
29
+
30
+ /**
31
+ * Accepts both the canonical lowercase API and Figma's PascalCase property
32
+ * values so Code Connect / design tooling can pass either.
33
+ */
34
+ export type CardVariantInput = CardVariant | 'Default' | 'Slim'
35
+
36
+ const normalizeVariant = (value: CardVariantInput | undefined): CardVariant => {
37
+ if (value === 'slim' || value === 'Slim') return 'slim'
38
+ return 'default'
39
+ }
40
+
41
+ const toNumber = (value: unknown, fallback: number): number => {
42
+ if (typeof value === 'number') {
43
+ return Number.isFinite(value) ? value : fallback
44
+ }
45
+ if (typeof value === 'string') {
46
+ const parsed = Number(value)
47
+ return Number.isFinite(parsed) ? parsed : fallback
48
+ }
49
+ return fallback
50
+ }
51
+
52
+ const toFontWeight = (
53
+ value: unknown,
54
+ fallback: TextStyle['fontWeight']
55
+ ): TextStyle['fontWeight'] => {
56
+ if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
57
+ if (typeof value === 'string' && value.length > 0) {
58
+ return value as TextStyle['fontWeight']
59
+ }
60
+ return fallback
61
+ }
62
+
63
+ /**
64
+ * Figma `Ratio` default for the media slot (node 766:6258) — 154×116.
65
+ * Kept as a named constant so stories / docs stay aligned with the design.
66
+ */
67
+ export const CARD_MEDIA_ASPECT_RATIO = 154 / 116
13
68
 
14
69
  export interface CardProps {
15
- /**
16
- * Content rendered in the header slot at the top of the card (e.g. a brand logo).
17
- * Sits above the media slot, with its own padding.
18
- */
19
- header?: React.ReactNode;
20
- /**
21
- * The content to be rendered in the media slot (e.g. an Image).
22
- * This content is wrapped in a container that respects the `aspectRatio`.
23
- */
24
- media?: React.ReactNode;
25
- /**
26
- * Content to render inside the card (e.g. Card.Title, Card.SupportText).
27
- */
28
- children?: React.ReactNode;
29
- /**
30
- * Modes object for token resolution (e.g. { "Mode": "Dark" }).
31
- */
32
- modes?: Modes;
33
- /**
34
- * Aspect ratio for the media slot container.
35
- * Default is based on Figma design 154/116 (~1.328).
36
- */
37
- mediaAspectRatio?: number;
38
- /**
39
- * Style overrides for the card container.
40
- */
41
- style?: StyleProp<ViewStyle>;
42
- /**
43
- * Press handler for the whole card. When set, the card becomes pressable
44
- * (rendered through a `Pressable` with `accessibilityRole="button"`).
45
- */
46
- onPress?: () => void;
47
- /** Disable interaction when `onPress` is set. */
48
- disabled?: boolean;
49
- /** Accessibility label forwarded to the pressable wrapper. */
50
- accessibilityLabel?: string;
70
+ /**
71
+ * Visual layout variant matching Figma `Property 1`.
72
+ * - `'default'` header + optional media + content (Figma Default)
73
+ * - `'slim'` — header + content only; `media` is ignored (Figma Slim)
74
+ *
75
+ * Defaults to `'default'`. Existing call sites that omit `media` continue
76
+ * to render a media-less default card (backward compatible). Prefer
77
+ * `variant="slim"` when you want the Slim contract explicitly.
78
+ */
79
+ variant?: CardVariantInput
80
+ /**
81
+ * Content rendered in the header slot at the top of the card (e.g. a brand
82
+ * logo). Sits above the media slot, padded with `card/content/padding/*`.
83
+ */
84
+ header?: React.ReactNode
85
+ /**
86
+ * Media slot content (e.g. `<Image />`). Wrapped in a container that
87
+ * respects `mediaAspectRatio`. Ignored when `variant="slim"`.
88
+ */
89
+ media?: React.ReactNode
90
+ /** Content slot (e.g. `Card.Title`, `Card.SupportText`). */
91
+ children?: React.ReactNode
92
+ /** Modes object for token resolution. Cascades into all slots. */
93
+ modes?: Modes
94
+ /**
95
+ * Aspect ratio for the media slot container.
96
+ * Defaults to the Figma Ratio 154/116 (~1.328).
97
+ */
98
+ mediaAspectRatio?: number
99
+ /** Style overrides for the card container. */
100
+ style?: StyleProp<ViewStyle>
101
+ /**
102
+ * Press handler for the whole card. When set, the card becomes pressable
103
+ * (`Pressable` + `accessibilityRole="button"`).
104
+ */
105
+ onPress?: () => void
106
+ /** Disable interaction when `onPress` is set. */
107
+ disabled?: boolean
108
+ /** Accessibility label forwarded to the root. */
109
+ accessibilityLabel?: string
110
+ }
111
+
112
+ interface CardTokens {
113
+ containerStyle: ViewStyle
114
+ headerWrapperStyle: ViewStyle
115
+ mediaWrapperStyle: ViewStyle
116
+ contentWrapperStyle: ViewStyle
117
+ }
118
+
119
+ function resolveCardTokens(
120
+ modes: Modes,
121
+ mediaAspectRatio: number
122
+ ): CardTokens {
123
+ const backgroundColor =
124
+ (getVariableByName('card/background/color', modes) as string | null) ??
125
+ '#ffffff'
126
+ const borderColor =
127
+ (getVariableByName('card/border/color', modes) as string | null) ??
128
+ '#ffffff'
129
+ const borderWidth = toNumber(
130
+ getVariableByName('card/strokeWidth', modes),
131
+ 0.5
132
+ )
133
+ const radius = toNumber(getVariableByName('card/radius', modes), 12)
134
+ // Figma root uses itemSpacing=4 (bound conceptually to card/gap). The
135
+ // component-set frames also use SPACE_BETWEEN + fixed heights for gallery
136
+ // layout — we hug content and apply gap only, so consumers aren't stuck
137
+ // with empty distributed space.
138
+ const gap = toNumber(getVariableByName('card/gap', modes), 4)
139
+ const paddingHorizontal = toNumber(
140
+ getVariableByName('card/padding-horizontal', modes),
141
+ 0
142
+ )
143
+ const paddingVertical = toNumber(
144
+ getVariableByName('card/padding-vertical', modes),
145
+ 0
146
+ )
147
+
148
+ const contentGap = toNumber(
149
+ getVariableByName('card/content/gap', modes),
150
+ 4
151
+ )
152
+ const contentPaddingHorizontal = toNumber(
153
+ getVariableByName('card/content/padding/horizontal', modes),
154
+ 12
155
+ )
156
+ const contentPaddingVertical = toNumber(
157
+ getVariableByName('card/content/padding/vertical', modes),
158
+ 12
159
+ )
160
+
161
+ return {
162
+ containerStyle: {
163
+ backgroundColor,
164
+ borderColor,
165
+ borderWidth,
166
+ borderRadius: radius,
167
+ gap,
168
+ paddingHorizontal,
169
+ paddingVertical,
170
+ overflow: 'hidden',
171
+ },
172
+ // Header wrap binds the same content-padding tokens as the content wrap
173
+ // in Figma (12/12) — not a separate header token set.
174
+ headerWrapperStyle: {
175
+ width: '100%',
176
+ flexDirection: 'row',
177
+ alignItems: 'flex-start',
178
+ paddingHorizontal: contentPaddingHorizontal,
179
+ paddingVertical: contentPaddingVertical,
180
+ },
181
+ mediaWrapperStyle: {
182
+ width: '100%',
183
+ aspectRatio: mediaAspectRatio,
184
+ overflow: 'hidden',
185
+ },
186
+ contentWrapperStyle: {
187
+ width: '100%',
188
+ gap: contentGap,
189
+ paddingHorizontal: contentPaddingHorizontal,
190
+ paddingVertical: contentPaddingVertical,
191
+ },
192
+ }
51
193
  }
52
194
 
53
195
  /**
54
- * Card component implementation from Figma node 765:6186.
55
- *
56
- * Supports a `media` slot (with aspect ratio) and a content area.
57
- * Supports an optional `header` slot (e.g. a brand logo), a `media` slot
58
- * (with aspect ratio) and a content area.
59
- * Usage:
196
+ * `Card` Figma node 5739:794 (`Property 1=Default` / `Property 1=Slim`).
197
+ *
198
+ * Slots: optional `header`, optional `media` (Default only), and `children`
199
+ * for the content stack. Typography subcomponents (`Card.Title`,
200
+ * `Card.SupportText`) read tokens via context.
201
+ *
202
+ * @example
60
203
  * ```tsx
61
- * <Card header={<GoldLogo />} media={<Image source={...} />} modes={modes}>
204
+ * <Card
205
+ * variant="default"
206
+ * header={<GoldLogo />}
207
+ * media={<Image imageSource={uri} ratio={154 / 116} />}
208
+ * modes={{ AppearanceBrand: 'Neutral' }}
209
+ * >
62
210
  * <Card.SupportText>Support text</Card.SupportText>
63
211
  * <Card.Title>Title</Card.Title>
64
212
  * <Card.SupportText>Support text</Card.SupportText>
65
213
  * </Card>
66
214
  * ```
215
+ *
216
+ * @component
67
217
  */
68
218
  export function Card({
69
- header,
70
- media,
71
- children,
72
- modes = EMPTY_MODES,
73
- mediaAspectRatio = 154 / 116,
74
- style,
75
- onPress,
76
- disabled,
77
- accessibilityLabel,
219
+ variant: variantProp = 'default',
220
+ header,
221
+ media,
222
+ children,
223
+ modes: propModes = EMPTY_MODES,
224
+ mediaAspectRatio = CARD_MEDIA_ASPECT_RATIO,
225
+ style,
226
+ onPress,
227
+ disabled,
228
+ accessibilityLabel,
78
229
  }: CardProps) {
79
- // Resolve Card Container Tokens
80
- const backgroundColor = getVariableByName('card/background/color', modes);
81
- const borderColor = getVariableByName('card/border/color', modes);
82
- const borderWidth = parseFloat(getVariableByName('card/strokeWidth', modes)); // Ensure number
83
- const radius = parseFloat(getVariableByName('card/radius', modes));
84
- const gap = parseFloat(getVariableByName('card/gap', modes));
85
- const paddingHorizontal = parseFloat(getVariableByName('card/padding-horizontal', modes));
86
- const paddingVertical = parseFloat(getVariableByName('card/padding-vertical', modes));
87
-
88
- // Resolve Content Wrapper Tokens
89
- const contentGap = parseFloat(getVariableByName('card/content/gap', modes));
90
- const contentPaddingHorizontal = parseFloat(getVariableByName('card/content/padding/horizontal', modes));
91
- const contentPaddingVertical = parseFloat(getVariableByName('card/content/padding/vertical', modes));
92
-
93
- // Clone media to pass modes if it's a valid element, facilitating nested style resolution
94
- const mediaWithModes = isValidElement(media)
95
- ? cloneElement(media as any, { modes: { ...(media.props as any).modes, ...modes } })
96
- : media;
97
-
98
- // Clone header to pass modes if it's a valid element
99
- const headerWithModes = isValidElement(header)
100
- ? cloneElement(header as any, { modes: { ...(header.props as any).modes, ...modes } })
101
- : header;
102
-
103
- const containerStyle: ViewStyle = {
104
- backgroundColor,
105
- borderColor,
106
- borderWidth,
107
- borderRadius: radius,
108
- gap,
109
- paddingHorizontal,
110
- paddingVertical,
111
- overflow: 'hidden', // Ensure border radius clips content
112
- };
113
-
114
- // Header wrap uses fixed padding from Figma (no dedicated tokens defined).
115
- const headerWrapperStyle: ViewStyle = {
116
- width: '100%',
117
- flexDirection: 'row',
118
- alignItems: 'flex-start',
119
- paddingHorizontal: 12,
120
- paddingVertical: 16,
121
- };
122
-
123
- const mediaWrapperStyle: ViewStyle = {
124
- width: '100%',
125
- aspectRatio: mediaAspectRatio,
126
- overflow: 'hidden',
127
- // Figma has 'bg-[#3a46f7]' on the Ratio definition, but that's a placeholder.
128
- // We assume the media content handles its own fill, or we can add a placeholder bg if needed.
129
- // For now, we leave it transparent or rely on media.
130
- };
131
-
132
- const contentWrapperStyle: ViewStyle = {
133
- width: '100%',
134
- gap: contentGap,
135
- paddingHorizontal: contentPaddingHorizontal,
136
- paddingVertical: contentPaddingVertical,
137
- };
138
-
139
- const content = (
140
- <>
141
- {header && (
142
- <View style={headerWrapperStyle}>
143
- {headerWithModes}
144
- </View>
145
- )}
146
- {media && (
147
- <View style={mediaWrapperStyle}>
148
- {mediaWithModes}
149
- </View>
150
- )}
151
- <View style={contentWrapperStyle}>
152
- {children}
153
- </View>
154
- </>
155
- );
156
-
157
- return (
158
- <CardContext.Provider value={{ modes }}>
159
- {onPress ? (
160
- <Pressable
161
- style={[containerStyle, style]}
162
- onPress={onPress}
163
- disabled={disabled}
164
- accessibilityRole="button"
165
- accessibilityLabel={accessibilityLabel}
166
- accessibilityState={{ disabled: !!disabled }}
167
- >
168
- {content}
169
- </Pressable>
170
- ) : (
171
- <View style={[containerStyle, style]} accessibilityLabel={accessibilityLabel}>
172
- {content}
173
- </View>
174
- )}
175
- </CardContext.Provider>
176
- );
230
+ const variant = normalizeVariant(variantProp)
231
+ const modes = propModes ?? EMPTY_MODES
232
+
233
+ const tokens = useMemo(
234
+ () => resolveCardTokens(modes, mediaAspectRatio),
235
+ [modes, mediaAspectRatio]
236
+ )
237
+
238
+ // Slim suppresses media even if a consumer still passes it — matches Figma
239
+ // Property 1=Slim. Default keeps prior behavior: media renders when provided.
240
+ const showMedia = variant === 'default' && media != null
241
+
242
+ const processedHeader =
243
+ header != null
244
+ ? cloneChildrenWithModes(React.Children.toArray(header), modes)
245
+ : null
246
+
247
+ const processedMedia = showMedia
248
+ ? cloneChildrenWithModes(React.Children.toArray(media), modes)
249
+ : null
250
+
251
+ const processedChildren = cloneChildrenWithModes(
252
+ React.Children.toArray(children ?? null),
253
+ modes
254
+ )
255
+
256
+ const content = (
257
+ <>
258
+ {processedHeader ? (
259
+ <View style={tokens.headerWrapperStyle}>{processedHeader}</View>
260
+ ) : null}
261
+ {processedMedia ? (
262
+ <View style={tokens.mediaWrapperStyle}>{processedMedia}</View>
263
+ ) : null}
264
+ <View style={tokens.contentWrapperStyle}>{processedChildren}</View>
265
+ </>
266
+ )
267
+
268
+ const rootStyle: StyleProp<ViewStyle> = [tokens.containerStyle, style]
269
+
270
+ return (
271
+ <CardContext.Provider value={{ modes }}>
272
+ {onPress ? (
273
+ <Pressable
274
+ style={rootStyle}
275
+ onPress={onPress}
276
+ disabled={disabled}
277
+ accessibilityRole="button"
278
+ accessibilityLabel={accessibilityLabel}
279
+ accessibilityState={{ disabled: !!disabled }}
280
+ >
281
+ {content}
282
+ </Pressable>
283
+ ) : (
284
+ <View style={rootStyle} accessibilityLabel={accessibilityLabel}>
285
+ {content}
286
+ </View>
287
+ )}
288
+ </CardContext.Provider>
289
+ )
177
290
  }
178
291
 
179
292
  // ----------------------------------------------------------------------------
@@ -181,68 +294,113 @@ export function Card({
181
294
  // ----------------------------------------------------------------------------
182
295
 
183
296
  export interface CardTextProps {
184
- children?: React.ReactNode;
185
- style?: StyleProp<TextStyle>;
186
- modes?: Modes;
297
+ children?: React.ReactNode
298
+ style?: StyleProp<TextStyle>
299
+ modes?: Modes
300
+ /**
301
+ * Line clamp before ellipsis. `Card.Title` defaults to `3` (Figma).
302
+ * Pass a different number to override, or `0` for no clamp.
303
+ */
304
+ numberOfLines?: number
187
305
  }
188
306
 
189
307
  /**
190
- * Title component for Card content.
191
- * Uses tokens: card/title/*
308
+ * Title tokens: `card/title/*`
192
309
  */
193
- function Title({ children, style, modes: propModes }: CardTextProps) {
194
- const context = useContext(CardContext);
195
- const modes = propModes || context.modes || {};
196
-
197
- const color = getVariableByName('card/title/color', modes);
198
- const fontSize = parseFloat(getVariableByName('card/title/fontSize', modes));
199
- const fontFamily = getVariableByName('card/title/fontFamily', modes);
200
- const lineHeight = parseFloat(getVariableByName('card/title/lineHeight', modes));
201
-
202
- // Handling the potential typo in Figma variables "fonwWeight" vs "fontWeight"
203
- // We check both keys to be safe.
204
- const rawFontWeight = getVariableByName('card/title/fontWeight', modes);
205
- const fontWeight = typeof rawFontWeight === 'number' ? rawFontWeight.toString() : rawFontWeight;
206
-
207
- const textStyle: TextStyle = {
208
- color,
209
- fontSize,
210
- fontFamily,
211
- lineHeight,
212
- fontWeight: fontWeight as TextStyle['fontWeight'],
213
- };
214
-
215
- return <Text style={[textStyle, style]}>{children}</Text>;
310
+ function Title({
311
+ children,
312
+ style,
313
+ modes: propModes,
314
+ numberOfLines = 3,
315
+ }: CardTextProps) {
316
+ const context = useContext(CardContext)
317
+ const modes = propModes ?? context.modes ?? EMPTY_MODES
318
+
319
+ const color =
320
+ (getVariableByName('card/title/color', modes) as string | null) ?? '#000000'
321
+ const fontSize = toNumber(
322
+ getVariableByName('card/title/fontSize', modes),
323
+ 16
324
+ )
325
+ const fontFamily =
326
+ (getVariableByName('card/title/fontFamily', modes) as string | null) ??
327
+ 'JioType Var'
328
+ const lineHeight = toNumber(
329
+ getVariableByName('card/title/lineHeight', modes),
330
+ 18
331
+ )
332
+ const fontWeight = toFontWeight(
333
+ getVariableByName('card/title/fontWeight', modes),
334
+ '900'
335
+ )
336
+
337
+ const textStyle: TextStyle = {
338
+ color,
339
+ fontSize,
340
+ fontFamily,
341
+ lineHeight,
342
+ fontWeight,
343
+ }
344
+
345
+ return (
346
+ <Text
347
+ style={[textStyle, style]}
348
+ numberOfLines={numberOfLines > 0 ? numberOfLines : undefined}
349
+ >
350
+ {children}
351
+ </Text>
352
+ )
216
353
  }
217
354
 
218
355
  /**
219
- * SupportiveText component for Card content.
220
- * Uses tokens: card/supportive-text/*
356
+ * Supportive text tokens: `card/supportive-text/*`
221
357
  */
222
- function SupportText({ children, style, modes: propModes }: CardTextProps) {
223
- const context = useContext(CardContext);
224
- const modes = propModes || context.modes || {};
225
-
226
- const color = getVariableByName('card/supportive-text/color', modes);
227
- const fontSize = parseFloat(getVariableByName('card/supportive-text/fontSize', modes));
228
- const fontFamily = getVariableByName('card/supportive-text/fontFamily', modes);
229
- const lineHeight = parseFloat(getVariableByName('card/supportive-text/lineHeight', modes));
230
- const rawFontWeight = getVariableByName('card/supportive-text/fontWeight', modes);
231
- const fontWeight = typeof rawFontWeight === 'number' ? rawFontWeight.toString() : rawFontWeight;
232
-
233
- const textStyle: TextStyle = {
234
- color,
235
- fontSize,
236
- fontFamily,
237
- lineHeight,
238
- fontWeight: fontWeight as TextStyle['fontWeight'],
239
- };
240
-
241
- return <Text style={[textStyle, style]}>{children}</Text>;
358
+ function SupportText({
359
+ children,
360
+ style,
361
+ modes: propModes,
362
+ numberOfLines,
363
+ }: CardTextProps) {
364
+ const context = useContext(CardContext)
365
+ const modes = propModes ?? context.modes ?? EMPTY_MODES
366
+
367
+ const color =
368
+ (getVariableByName('card/supportive-text/color', modes) as string | null) ??
369
+ '#1a1c1f'
370
+ const fontSize = toNumber(
371
+ getVariableByName('card/supportive-text/fontSize', modes),
372
+ 12
373
+ )
374
+ const fontFamily =
375
+ (getVariableByName(
376
+ 'card/supportive-text/fontFamily',
377
+ modes
378
+ ) as string | null) ?? 'JioType Var'
379
+ const lineHeight = toNumber(
380
+ getVariableByName('card/supportive-text/lineHeight', modes),
381
+ 16
382
+ )
383
+ const fontWeight = toFontWeight(
384
+ getVariableByName('card/supportive-text/fontWeight', modes),
385
+ '400'
386
+ )
387
+
388
+ const textStyle: TextStyle = {
389
+ color,
390
+ fontSize,
391
+ fontFamily,
392
+ lineHeight,
393
+ fontWeight,
394
+ }
395
+
396
+ return (
397
+ <Text style={[textStyle, style]} numberOfLines={numberOfLines}>
398
+ {children}
399
+ </Text>
400
+ )
242
401
  }
243
402
 
244
- // Attach sub-components to Card
245
- Card.Title = Title;
246
- Card.SupportText = SupportText;
403
+ Card.Title = Title
404
+ Card.SupportText = SupportText
247
405
 
248
- export default Card;
406
+ export default Card