jfs-components 0.1.51 → 0.1.55

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 (95) hide show
  1. package/.cursor/D2C-SCREEN.md +2 -1
  2. package/.cursor/commands/D2C.md +2 -1
  3. package/CHANGELOG.md +17 -0
  4. package/lib/commonjs/components/ActionFooter/ActionFooter.js +319 -135
  5. package/lib/commonjs/components/Badge/Badge.js +20 -1
  6. package/lib/commonjs/components/Button/Button.js +139 -47
  7. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
  8. package/lib/commonjs/components/Card/Card.js +151 -112
  9. package/lib/commonjs/components/Card/assets/gold-logo-mark.png +0 -0
  10. package/lib/commonjs/components/CategoryCard/CategoryCard.js +97 -63
  11. package/lib/commonjs/components/CategoryCard/assets/sample.png +0 -0
  12. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +59 -10
  13. package/lib/commonjs/components/CircularProgressBarDoted/CircularProgressBarDoted.js +1 -1
  14. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +132 -49
  15. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +56 -14
  16. package/lib/commonjs/components/IconCapsule/IconCapsule.js +1 -1
  17. package/lib/commonjs/components/MetricData/MetricData.js +26 -13
  18. package/lib/commonjs/components/MoneyValue/MoneyValue.js +1 -1
  19. package/lib/commonjs/components/Nudge/Nudge.js +1 -1
  20. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +47 -91
  21. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  22. package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +1 -1
  23. package/lib/commonjs/components/Slider/Slider.js +3 -3
  24. package/lib/commonjs/components/TextInput/TextInput.js +26 -8
  25. package/lib/commonjs/components/Tooltip/Tooltip.js +3 -5
  26. package/lib/commonjs/components/index.js +7 -1
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/design-tokens/figma-modes.generated.js +13 -13
  29. package/lib/commonjs/icons/registry.js +1 -1
  30. package/lib/module/components/ActionFooter/ActionFooter.js +322 -138
  31. package/lib/module/components/Badge/Badge.js +20 -1
  32. package/lib/module/components/Button/Button.js +140 -48
  33. package/lib/module/components/ButtonGroup/ButtonGroup.js +1 -1
  34. package/lib/module/components/Card/Card.js +152 -114
  35. package/lib/module/components/Card/assets/gold-logo-mark.png +0 -0
  36. package/lib/module/components/CategoryCard/CategoryCard.js +97 -63
  37. package/lib/module/components/CategoryCard/assets/sample.png +0 -0
  38. package/lib/module/components/CheckboxItem/CheckboxItem.js +59 -10
  39. package/lib/module/components/CircularProgressBarDoted/CircularProgressBarDoted.js +1 -1
  40. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +135 -52
  41. package/lib/module/components/HelloJioInput/HelloJioInput.js +56 -14
  42. package/lib/module/components/IconCapsule/IconCapsule.js +1 -1
  43. package/lib/module/components/MetricData/MetricData.js +26 -14
  44. package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
  45. package/lib/module/components/Nudge/Nudge.js +1 -1
  46. package/lib/module/components/PdpCcCard/PdpCcCard.js +48 -92
  47. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  48. package/lib/module/components/PortfolioHero/PortfolioHero.js +1 -1
  49. package/lib/module/components/Slider/Slider.js +3 -3
  50. package/lib/module/components/TextInput/TextInput.js +27 -9
  51. package/lib/module/components/Tooltip/Tooltip.js +3 -5
  52. package/lib/module/components/index.js +1 -1
  53. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  54. package/lib/module/design-tokens/figma-modes.generated.js +13 -13
  55. package/lib/module/icons/registry.js +1 -1
  56. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +50 -21
  57. package/lib/typescript/src/components/Button/Button.d.ts +24 -1
  58. package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +2 -2
  59. package/lib/typescript/src/components/Card/Card.d.ts +54 -25
  60. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +29 -12
  61. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +27 -9
  62. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +37 -12
  63. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +1 -1
  64. package/lib/typescript/src/components/MetricData/MetricData.d.ts +7 -5
  65. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +16 -6
  66. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +9 -2
  67. package/lib/typescript/src/components/index.d.ts +2 -2
  68. package/lib/typescript/src/icons/registry.d.ts +1 -1
  69. package/package.json +1 -1
  70. package/src/components/ActionFooter/ActionFooter.tsx +375 -141
  71. package/src/components/Badge/Badge.tsx +21 -1
  72. package/src/components/Button/Button.tsx +192 -44
  73. package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
  74. package/src/components/Card/Card.tsx +370 -212
  75. package/src/components/Card/assets/gold-logo-mark.png +0 -0
  76. package/src/components/CategoryCard/CategoryCard.tsx +130 -84
  77. package/src/components/CategoryCard/assets/sample.png +0 -0
  78. package/src/components/CheckboxItem/CheckboxItem.tsx +72 -12
  79. package/src/components/CircularProgressBarDoted/CircularProgressBarDoted.tsx +1 -1
  80. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +186 -67
  81. package/src/components/HelloJioInput/HelloJioInput.tsx +58 -17
  82. package/src/components/IconCapsule/IconCapsule.tsx +1 -1
  83. package/src/components/MetricData/MetricData.tsx +39 -22
  84. package/src/components/MoneyValue/MoneyValue.tsx +1 -1
  85. package/src/components/Nudge/Nudge.tsx +1 -1
  86. package/src/components/PdpCcCard/PdpCcCard.tsx +41 -92
  87. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +73 -58
  88. package/src/components/PortfolioHero/PortfolioHero.tsx +1 -1
  89. package/src/components/Slider/Slider.tsx +3 -3
  90. package/src/components/TextInput/TextInput.tsx +26 -9
  91. package/src/components/Tooltip/Tooltip.tsx +3 -5
  92. package/src/components/index.ts +9 -2
  93. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  94. package/src/design-tokens/figma-modes.generated.ts +13 -13
  95. package/src/icons/registry.ts +1 -1
@@ -5,10 +5,18 @@ import { Platform, Pressable, Text, View } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { usePressableWebSupport } from '../../utils/web-platform-utils';
7
7
  import { EMPTY_MODES } from '../../utils/react-utils';
8
+ import GlassFill from '../../utils/GlassFill/GlassFill';
8
9
  import Icon from '../../icons/Icon';
9
10
  import Skeleton from '../../skeleton/Skeleton';
10
11
  import { useSkeleton } from '../../skeleton/SkeletonGroup';
11
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+
13
+ /**
14
+ * Figma `Type` property on Button.
15
+ * - `'default'` — solid token-colored pill (hugs content)
16
+ * - `'glass'` — frosted translucent pill backed by {@link GlassFill}
17
+ * - `'fixed'` — solid pill constrained by `button/maxWidth`
18
+ */
19
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
12
20
  // ---------------------------------------------------------------------------
13
21
  // Module-scope constants — never re-allocated per render.
14
22
  // ---------------------------------------------------------------------------
@@ -52,28 +60,79 @@ const NO_WRAP_LABEL_STYLE = {
52
60
  // the most common path (no modes prop) is resolved once for the whole app.
53
61
  // ---------------------------------------------------------------------------
54
62
 
55
- function resolveButtonTokens(modes, disabled) {
56
- const backgroundColor = getVariableByName('button/background', modes);
57
- const borderColor = getVariableByName('button/border/color', modes);
58
- const borderWidth = getVariableByName('button/border/size', modes);
59
- const radius = getVariableByName('button/radius', modes);
60
- const paddingHorizontal = getVariableByName('button/padding/horizontal', modes);
61
- const paddingVertical = getVariableByName('button/padding/vertical', modes);
62
- const gap = getVariableByName('button/gap', modes);
63
- const fontFamily = getVariableByName('button/fontFamily', modes);
64
- const fontWeightValue = getVariableByName('button/fontWeight', modes);
65
- const fontWeight = typeof fontWeightValue === 'number' ? fontWeightValue.toString() : fontWeightValue;
66
- const lineHeight = getVariableByName('button/lineHeight', modes);
67
- const fontSize = getVariableByName('button/fontSize', modes);
68
- const textColor = getVariableByName('button/foreground', modes);
69
- const iconSize = getVariableByName('button/icon/size', modes) ?? 18;
63
+ const BUTTON_STATE_VALUES = new Set(['Idle', 'Hover', 'Pressed', 'Disabled']);
64
+ const SOLID_STATE_COLLECTION = 'Button / State';
65
+ const GLASS_STATE_COLLECTION = 'Button Glass State';
66
+ function resolveIsGlass(type, modes) {
67
+ return type === 'glass' || modes['Button type'] === 'glass';
68
+ }
70
69
 
71
- // Pressed tokens are always resolved so the Pressable `style` callback can
72
- // apply the pressed visual without scheduling a React render.
73
- const pressedModes = {
74
- ...modes,
75
- 'Button / State': 'Pressed'
70
+ /**
71
+ * Pick Idle/Hover/Pressed/Disabled from the active state collection, falling
72
+ * back to the sibling collection when callers pass the other key (e.g. glass
73
+ * buttons that still send `Button / State`). Never treats a collection name
74
+ * as a mode value.
75
+ */
76
+ function pickButtonInteractiveState(modes, isGlass) {
77
+ const primary = isGlass ? GLASS_STATE_COLLECTION : SOLID_STATE_COLLECTION;
78
+ const sibling = isGlass ? SOLID_STATE_COLLECTION : GLASS_STATE_COLLECTION;
79
+ const candidate = modes[primary] ?? modes[sibling];
80
+ if (typeof candidate === 'string' && BUTTON_STATE_VALUES.has(candidate)) {
81
+ return candidate;
82
+ }
83
+ return 'Idle';
84
+ }
85
+
86
+ /** Write a valid interactive state onto the correct collection for glass/solid. */
87
+ function withButtonInteractiveState(modes, isGlass, state) {
88
+ const collection = isGlass ? GLASS_STATE_COLLECTION : SOLID_STATE_COLLECTION;
89
+ const sibling = isGlass ? SOLID_STATE_COLLECTION : GLASS_STATE_COLLECTION;
90
+ const {
91
+ [sibling]: _dropSibling,
92
+ ...rest
93
+ } = modes;
94
+ return {
95
+ ...rest,
96
+ [collection]: state
76
97
  };
98
+ }
99
+ function resolveButtonTokens(modes, disabled, type) {
100
+ const isGlass = resolveIsGlass(type, modes);
101
+ const isFixed = type === 'fixed';
102
+
103
+ // Inject the Button type mode so token aliases (background/foreground) resolve
104
+ // to the glass palette. Callers can also drive glass solely via modes.
105
+ const effectiveModes = isGlass ? {
106
+ ...modes,
107
+ 'Button type': 'glass'
108
+ } : modes;
109
+
110
+ // Glass interactive states live on `Button Glass State`; solid on `Button / State`.
111
+ // Map Idle/Hover/Pressed/Disabled values through — never assign a collection name.
112
+ const idleState = disabled && isGlass ? 'Disabled' : pickButtonInteractiveState(effectiveModes, isGlass);
113
+ const idleModes = withButtonInteractiveState(effectiveModes, isGlass, idleState);
114
+ const backgroundColor = getVariableByName('button/background', idleModes);
115
+ const borderColor = getVariableByName('button/border/color', idleModes);
116
+ const borderWidth = getVariableByName('button/border/size', idleModes);
117
+ const radius = getVariableByName('button/radius', idleModes);
118
+ const paddingHorizontal = getVariableByName('button/padding/horizontal', idleModes);
119
+ const paddingVertical = getVariableByName('button/padding/vertical', idleModes);
120
+ const gap = getVariableByName('button/gap', idleModes);
121
+ const maxWidth = getVariableByName('button/maxWidth', idleModes);
122
+ const fontFamily = getVariableByName('button/fontFamily', idleModes);
123
+ const fontWeightValue = getVariableByName('button/fontWeight', idleModes);
124
+ const fontWeight = typeof fontWeightValue === 'number' ? fontWeightValue.toString() : fontWeightValue;
125
+ const lineHeight = getVariableByName('button/lineHeight', idleModes);
126
+ const fontSize = getVariableByName('button/fontSize', idleModes);
127
+ const textColor = getVariableByName('button/foreground', idleModes);
128
+ const iconSize = getVariableByName('button/icon/size', idleModes) ?? 18;
129
+
130
+ // Figma `blur/minimal` (px radius) -> GlassFill's 0–100 intensity scale.
131
+ const glassBlurIntensity = Math.max(0, Math.min(100, Math.round(Number(getVariableByName('blur/minimal', idleModes) ?? 29))));
132
+
133
+ // Pressed tokens are always resolved so the Pressable `style` callback /
134
+ // glass overlay can apply the pressed visual without a modes recompute.
135
+ const pressedModes = withButtonInteractiveState(effectiveModes, isGlass, 'Pressed');
77
136
  const pressedBg = getVariableByName('button/background', pressedModes);
78
137
  const pressedBorderColor = getVariableByName('button/border/color', pressedModes);
79
138
  const pressedTextColor = getVariableByName('button/foreground', pressedModes);
@@ -82,20 +141,22 @@ function resolveButtonTokens(modes, disabled) {
82
141
  // ~3 token lookups + an object spread per render on every Button.
83
142
  let hoverContainer = null;
84
143
  let hoverTextColor = null;
144
+ let glassOverlayHover = null;
85
145
  if (IS_WEB) {
86
- const hoverModes = {
87
- ...modes,
88
- 'Button / State': 'Hover'
89
- };
146
+ const hoverModes = withButtonInteractiveState(effectiveModes, isGlass, 'Hover');
90
147
  const hoverBg = getVariableByName('button/background', hoverModes);
91
148
  const hoverBorderColor = getVariableByName('button/border/color', hoverModes);
92
149
  hoverTextColor = getVariableByName('button/foreground', hoverModes);
93
- hoverContainer = {
150
+ glassOverlayHover = hoverBg;
151
+ // Glass keeps the host transparent; hover only retints the GlassFill overlay.
152
+ hoverContainer = isGlass ? null : {
94
153
  backgroundColor: hoverBg,
95
154
  borderColor: hoverBorderColor
96
155
  };
97
156
  }
98
157
  return {
158
+ isGlass,
159
+ isFixed,
99
160
  container: {
100
161
  ...containerLayoutStyle,
101
162
  paddingHorizontal: paddingHorizontal,
@@ -103,9 +164,18 @@ function resolveButtonTokens(modes, disabled) {
103
164
  borderRadius: radius,
104
165
  borderWidth: borderWidth ?? 1,
105
166
  borderColor: borderColor,
106
- backgroundColor: backgroundColor,
167
+ // Glass: transparent host so GlassFill shows through. Fixed: clamp width.
168
+ backgroundColor: isGlass ? 'transparent' : backgroundColor,
107
169
  gap: gap,
108
- opacity: disabled ? 0.5 : 1
170
+ // Glass disabled uses Button Glass State=Disabled foreground tokens —
171
+ // don't also apply the solid-button opacity fade.
172
+ opacity: disabled && !isGlass ? 0.5 : 1,
173
+ ...(isGlass ? {
174
+ overflow: 'hidden'
175
+ } : null),
176
+ ...(isFixed && maxWidth != null ? {
177
+ maxWidth: maxWidth
178
+ } : null)
109
179
  },
110
180
  baseLabel: {
111
181
  color: textColor,
@@ -115,7 +185,7 @@ function resolveButtonTokens(modes, disabled) {
115
185
  lineHeight: lineHeight
116
186
  },
117
187
  hoverContainer,
118
- pressedContainer: {
188
+ pressedContainer: isGlass ? {} : {
119
189
  backgroundColor: pressedBg,
120
190
  borderColor: pressedBorderColor
121
191
  },
@@ -125,7 +195,11 @@ function resolveButtonTokens(modes, disabled) {
125
195
  hoverIconColor: hoverTextColor,
126
196
  pressedIconColor: pressedTextColor,
127
197
  iconSize,
128
- accessoryOffset: gap / 2
198
+ accessoryOffset: gap / 2,
199
+ glassOverlayIdle: backgroundColor,
200
+ glassOverlayHover,
201
+ glassOverlayPressed: pressedBg,
202
+ glassBlurIntensity
129
203
  };
130
204
  }
131
205
 
@@ -171,6 +245,9 @@ function ButtonImpl({
171
245
  leading,
172
246
  trailing,
173
247
  icon,
248
+ type = 'default',
249
+ glassOverlayColor,
250
+ glassIntensity,
174
251
  modes = EMPTY_MODES,
175
252
  onPress,
176
253
  disabled = false,
@@ -195,7 +272,7 @@ function ButtonImpl({
195
272
  userHandlersRef.current.onPressOut = rest?.onPressOut;
196
273
  userHandlersRef.current.onHoverIn = rest?.onHoverIn;
197
274
  userHandlersRef.current.onHoverOut = rest?.onHoverOut;
198
- const tokens = useMemo(() => resolveButtonTokens(modes, disabled), [modes, disabled]);
275
+ const tokens = useMemo(() => resolveButtonTokens(modes, disabled, type), [modes, disabled, type]);
199
276
 
200
277
  // Skeleton context — read unconditionally so React's hook order stays
201
278
  // stable. The actual short-circuit return happens AFTER all remaining
@@ -309,7 +386,25 @@ function ButtonImpl({
309
386
  modes: modes
310
387
  });
311
388
  }
312
- return /*#__PURE__*/_jsxs(Pressable, {
389
+ const body = /*#__PURE__*/_jsxs(_Fragment, {
390
+ children: [leading ? /*#__PURE__*/_jsx(View, {
391
+ style: leadingAccessoryStyle,
392
+ children: leading
393
+ }) : null, content, icon ? /*#__PURE__*/_jsx(View, {
394
+ style: trailingAccessoryStyle,
395
+ children: /*#__PURE__*/_jsx(Icon, {
396
+ name: icon,
397
+ size: tokens.iconSize,
398
+ color: activeIconColor,
399
+ accessibilityElementsHidden: true,
400
+ importantForAccessibility: "no"
401
+ })
402
+ }) : trailing ? /*#__PURE__*/_jsx(View, {
403
+ style: trailingAccessoryStyle,
404
+ children: trailing
405
+ }) : null]
406
+ });
407
+ return /*#__PURE__*/_jsx(Pressable, {
313
408
  accessibilityRole: "button",
314
409
  accessibilityLabel: undefined,
315
410
  ...(accessibilityHint !== undefined ? {
@@ -330,22 +425,19 @@ function ButtonImpl({
330
425
  unstable_pressDelay: PRESS_DELAY,
331
426
  style: styleCallback,
332
427
  ...webProps,
333
- children: [leading ? /*#__PURE__*/_jsx(View, {
334
- style: leadingAccessoryStyle,
335
- children: leading
336
- }) : null, content, icon ? /*#__PURE__*/_jsx(View, {
337
- style: trailingAccessoryStyle,
338
- children: /*#__PURE__*/_jsx(Icon, {
339
- name: icon,
340
- size: tokens.iconSize,
341
- color: activeIconColor,
342
- accessibilityElementsHidden: true,
343
- importantForAccessibility: "no"
344
- })
345
- }) : trailing ? /*#__PURE__*/_jsx(View, {
346
- style: trailingAccessoryStyle,
347
- children: trailing
348
- }) : null]
428
+ children: tokens.isGlass ? ({
429
+ pressed
430
+ }) => {
431
+ const overlayColor = glassOverlayColor ?? (pressed && !disabled ? tokens.glassOverlayPressed : isHovered && !disabled && tokens.glassOverlayHover ? tokens.glassOverlayHover : tokens.glassOverlayIdle);
432
+ return /*#__PURE__*/_jsxs(_Fragment, {
433
+ children: [/*#__PURE__*/_jsx(GlassFill, {
434
+ tint: "light",
435
+ intensity: glassIntensity ?? tokens.glassBlurIntensity,
436
+ overlayColor: overlayColor,
437
+ androidTintWash: false
438
+ }), body]
439
+ });
440
+ } : body
349
441
  });
350
442
  }
351
443
  const Button = /*#__PURE__*/React.memo(ButtonImpl);
@@ -17,7 +17,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
17
17
  * @component
18
18
  * @example
19
19
  * ```jsx
20
- * <ButtonGroup modes={{"Appearance": "light"}}>
20
+ * <ButtonGroup modes={{"Color Mode": "Light"}}>
21
21
  * <IconButton iconName="ic_qr_code" />
22
22
  * <Button label="Pay" />
23
23
  * </ButtonGroup>
@@ -1,122 +1,165 @@
1
1
  "use strict";
2
2
 
3
- import React, { createContext, useContext, isValidElement, cloneElement } from 'react';
3
+ import React, { createContext, useContext, useMemo } from 'react';
4
4
  import { View, Text, Pressable } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
- import { EMPTY_MODES } from '../../utils/react-utils';
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
7
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  /**
9
- * Context to share 'modes' with child components like Card.Title and Card.SupportText.
10
- * This ensures that nested components can resolve their tokens correctly without
11
- * needing explicit mode prop passing for every child.
9
+ * Context so `Card.Title` / `Card.SupportText` resolve tokens from the parent
10
+ * card's `modes` without every call site having to pass them explicitly.
12
11
  */
13
- const CardContext = /*#__PURE__*/createContext({});
12
+ const CardContext = /*#__PURE__*/createContext({
13
+ modes: EMPTY_MODES
14
+ });
15
+
16
+ /** Canonical variant values. Maps 1:1 to Figma `Property 1`. */
17
+
18
+ /**
19
+ * Accepts both the canonical lowercase API and Figma's PascalCase property
20
+ * values so Code Connect / design tooling can pass either.
21
+ */
22
+
23
+ const normalizeVariant = value => {
24
+ if (value === 'slim' || value === 'Slim') return 'slim';
25
+ return 'default';
26
+ };
27
+ const toNumber = (value, fallback) => {
28
+ if (typeof value === 'number') {
29
+ return Number.isFinite(value) ? value : fallback;
30
+ }
31
+ if (typeof value === 'string') {
32
+ const parsed = Number(value);
33
+ return Number.isFinite(parsed) ? parsed : fallback;
34
+ }
35
+ return fallback;
36
+ };
37
+ const toFontWeight = (value, fallback) => {
38
+ if (typeof value === 'number') return String(value);
39
+ if (typeof value === 'string' && value.length > 0) {
40
+ return value;
41
+ }
42
+ return fallback;
43
+ };
44
+
45
+ /**
46
+ * Figma `Ratio` default for the media slot (node 766:6258) — 154×116.
47
+ * Kept as a named constant so stories / docs stay aligned with the design.
48
+ */
49
+ export const CARD_MEDIA_ASPECT_RATIO = 154 / 116;
50
+ function resolveCardTokens(modes, mediaAspectRatio) {
51
+ const backgroundColor = getVariableByName('card/background/color', modes) ?? '#ffffff';
52
+ const borderColor = getVariableByName('card/border/color', modes) ?? '#ffffff';
53
+ const borderWidth = toNumber(getVariableByName('card/strokeWidth', modes), 0.5);
54
+ const radius = toNumber(getVariableByName('card/radius', modes), 12);
55
+ // Figma root uses itemSpacing=4 (bound conceptually to card/gap). The
56
+ // component-set frames also use SPACE_BETWEEN + fixed heights for gallery
57
+ // layout — we hug content and apply gap only, so consumers aren't stuck
58
+ // with empty distributed space.
59
+ const gap = toNumber(getVariableByName('card/gap', modes), 4);
60
+ const paddingHorizontal = toNumber(getVariableByName('card/padding-horizontal', modes), 0);
61
+ const paddingVertical = toNumber(getVariableByName('card/padding-vertical', modes), 0);
62
+ const contentGap = toNumber(getVariableByName('card/content/gap', modes), 4);
63
+ const contentPaddingHorizontal = toNumber(getVariableByName('card/content/padding/horizontal', modes), 12);
64
+ const contentPaddingVertical = toNumber(getVariableByName('card/content/padding/vertical', modes), 12);
65
+ return {
66
+ containerStyle: {
67
+ backgroundColor,
68
+ borderColor,
69
+ borderWidth,
70
+ borderRadius: radius,
71
+ gap,
72
+ paddingHorizontal,
73
+ paddingVertical,
74
+ overflow: 'hidden'
75
+ },
76
+ // Header wrap binds the same content-padding tokens as the content wrap
77
+ // in Figma (12/12) — not a separate header token set.
78
+ headerWrapperStyle: {
79
+ width: '100%',
80
+ flexDirection: 'row',
81
+ alignItems: 'flex-start',
82
+ paddingHorizontal: contentPaddingHorizontal,
83
+ paddingVertical: contentPaddingVertical
84
+ },
85
+ mediaWrapperStyle: {
86
+ width: '100%',
87
+ aspectRatio: mediaAspectRatio,
88
+ overflow: 'hidden'
89
+ },
90
+ contentWrapperStyle: {
91
+ width: '100%',
92
+ gap: contentGap,
93
+ paddingHorizontal: contentPaddingHorizontal,
94
+ paddingVertical: contentPaddingVertical
95
+ }
96
+ };
97
+ }
98
+
14
99
  /**
15
- * Card component implementation from Figma node 765:6186.
16
- *
17
- * Supports a `media` slot (with aspect ratio) and a content area.
18
- * Supports an optional `header` slot (e.g. a brand logo), a `media` slot
19
- * (with aspect ratio) and a content area.
20
- * Usage:
100
+ * `Card` Figma node 5739:794 (`Property 1=Default` / `Property 1=Slim`).
101
+ *
102
+ * Slots: optional `header`, optional `media` (Default only), and `children`
103
+ * for the content stack. Typography subcomponents (`Card.Title`,
104
+ * `Card.SupportText`) read tokens via context.
105
+ *
106
+ * @example
21
107
  * ```tsx
22
- * <Card header={<GoldLogo />} media={<Image source={...} />} modes={modes}>
108
+ * <Card
109
+ * variant="default"
110
+ * header={<GoldLogo />}
111
+ * media={<Image imageSource={uri} ratio={154 / 116} />}
112
+ * modes={{ AppearanceBrand: 'Neutral' }}
113
+ * >
23
114
  * <Card.SupportText>Support text</Card.SupportText>
24
115
  * <Card.Title>Title</Card.Title>
25
116
  * <Card.SupportText>Support text</Card.SupportText>
26
117
  * </Card>
27
118
  * ```
119
+ *
120
+ * @component
28
121
  */
29
122
  export function Card({
123
+ variant: variantProp = 'default',
30
124
  header,
31
125
  media,
32
126
  children,
33
- modes = EMPTY_MODES,
34
- mediaAspectRatio = 154 / 116,
127
+ modes: propModes = EMPTY_MODES,
128
+ mediaAspectRatio = CARD_MEDIA_ASPECT_RATIO,
35
129
  style,
36
130
  onPress,
37
131
  disabled,
38
132
  accessibilityLabel
39
133
  }) {
40
- // Resolve Card Container Tokens
41
- const backgroundColor = getVariableByName('card/background/color', modes);
42
- const borderColor = getVariableByName('card/border/color', modes);
43
- const borderWidth = parseFloat(getVariableByName('card/strokeWidth', modes)); // Ensure number
44
- const radius = parseFloat(getVariableByName('card/radius', modes));
45
- const gap = parseFloat(getVariableByName('card/gap', modes));
46
- const paddingHorizontal = parseFloat(getVariableByName('card/padding-horizontal', modes));
47
- const paddingVertical = parseFloat(getVariableByName('card/padding-vertical', modes));
134
+ const variant = normalizeVariant(variantProp);
135
+ const modes = propModes ?? EMPTY_MODES;
136
+ const tokens = useMemo(() => resolveCardTokens(modes, mediaAspectRatio), [modes, mediaAspectRatio]);
48
137
 
49
- // Resolve Content Wrapper Tokens
50
- const contentGap = parseFloat(getVariableByName('card/content/gap', modes));
51
- const contentPaddingHorizontal = parseFloat(getVariableByName('card/content/padding/horizontal', modes));
52
- const contentPaddingVertical = parseFloat(getVariableByName('card/content/padding/vertical', modes));
53
-
54
- // Clone media to pass modes if it's a valid element, facilitating nested style resolution
55
- const mediaWithModes = /*#__PURE__*/isValidElement(media) ? /*#__PURE__*/cloneElement(media, {
56
- modes: {
57
- ...media.props.modes,
58
- ...modes
59
- }
60
- }) : media;
61
-
62
- // Clone header to pass modes if it's a valid element
63
- const headerWithModes = /*#__PURE__*/isValidElement(header) ? /*#__PURE__*/cloneElement(header, {
64
- modes: {
65
- ...header.props.modes,
66
- ...modes
67
- }
68
- }) : header;
69
- const containerStyle = {
70
- backgroundColor,
71
- borderColor,
72
- borderWidth,
73
- borderRadius: radius,
74
- gap,
75
- paddingHorizontal,
76
- paddingVertical,
77
- overflow: 'hidden' // Ensure border radius clips content
78
- };
79
-
80
- // Header wrap uses fixed padding from Figma (no dedicated tokens defined).
81
- const headerWrapperStyle = {
82
- width: '100%',
83
- flexDirection: 'row',
84
- alignItems: 'flex-start',
85
- paddingHorizontal: 12,
86
- paddingVertical: 16
87
- };
88
- const mediaWrapperStyle = {
89
- width: '100%',
90
- aspectRatio: mediaAspectRatio,
91
- overflow: 'hidden'
92
- // Figma has 'bg-[#3a46f7]' on the Ratio definition, but that's a placeholder.
93
- // We assume the media content handles its own fill, or we can add a placeholder bg if needed.
94
- // For now, we leave it transparent or rely on media.
95
- };
96
- const contentWrapperStyle = {
97
- width: '100%',
98
- gap: contentGap,
99
- paddingHorizontal: contentPaddingHorizontal,
100
- paddingVertical: contentPaddingVertical
101
- };
138
+ // Slim suppresses media even if a consumer still passes it — matches Figma
139
+ // Property 1=Slim. Default keeps prior behavior: media renders when provided.
140
+ const showMedia = variant === 'default' && media != null;
141
+ const processedHeader = header != null ? cloneChildrenWithModes(React.Children.toArray(header), modes) : null;
142
+ const processedMedia = showMedia ? cloneChildrenWithModes(React.Children.toArray(media), modes) : null;
143
+ const processedChildren = cloneChildrenWithModes(React.Children.toArray(children ?? null), modes);
102
144
  const content = /*#__PURE__*/_jsxs(_Fragment, {
103
- children: [header && /*#__PURE__*/_jsx(View, {
104
- style: headerWrapperStyle,
105
- children: headerWithModes
106
- }), media && /*#__PURE__*/_jsx(View, {
107
- style: mediaWrapperStyle,
108
- children: mediaWithModes
109
- }), /*#__PURE__*/_jsx(View, {
110
- style: contentWrapperStyle,
111
- children: children
145
+ children: [processedHeader ? /*#__PURE__*/_jsx(View, {
146
+ style: tokens.headerWrapperStyle,
147
+ children: processedHeader
148
+ }) : null, processedMedia ? /*#__PURE__*/_jsx(View, {
149
+ style: tokens.mediaWrapperStyle,
150
+ children: processedMedia
151
+ }) : null, /*#__PURE__*/_jsx(View, {
152
+ style: tokens.contentWrapperStyle,
153
+ children: processedChildren
112
154
  })]
113
155
  });
156
+ const rootStyle = [tokens.containerStyle, style];
114
157
  return /*#__PURE__*/_jsx(CardContext.Provider, {
115
158
  value: {
116
159
  modes
117
160
  },
118
161
  children: onPress ? /*#__PURE__*/_jsx(Pressable, {
119
- style: [containerStyle, style],
162
+ style: rootStyle,
120
163
  onPress: onPress,
121
164
  disabled: disabled,
122
165
  accessibilityRole: "button",
@@ -126,7 +169,7 @@ export function Card({
126
169
  },
127
170
  children: content
128
171
  }) : /*#__PURE__*/_jsx(View, {
129
- style: [containerStyle, style],
172
+ style: rootStyle,
130
173
  accessibilityLabel: accessibilityLabel,
131
174
  children: content
132
175
  })
@@ -138,69 +181,64 @@ export function Card({
138
181
  // ----------------------------------------------------------------------------
139
182
 
140
183
  /**
141
- * Title component for Card content.
142
- * Uses tokens: card/title/*
184
+ * Title tokens: `card/title/*`
143
185
  */
144
186
  function Title({
145
187
  children,
146
188
  style,
147
- modes: propModes
189
+ modes: propModes,
190
+ numberOfLines = 3
148
191
  }) {
149
192
  const context = useContext(CardContext);
150
- const modes = propModes || context.modes || {};
151
- const color = getVariableByName('card/title/color', modes);
152
- const fontSize = parseFloat(getVariableByName('card/title/fontSize', modes));
153
- const fontFamily = getVariableByName('card/title/fontFamily', modes);
154
- const lineHeight = parseFloat(getVariableByName('card/title/lineHeight', modes));
155
-
156
- // Handling the potential typo in Figma variables "fonwWeight" vs "fontWeight"
157
- // We check both keys to be safe.
158
- const rawFontWeight = getVariableByName('card/title/fontWeight', modes);
159
- const fontWeight = typeof rawFontWeight === 'number' ? rawFontWeight.toString() : rawFontWeight;
193
+ const modes = propModes ?? context.modes ?? EMPTY_MODES;
194
+ const color = getVariableByName('card/title/color', modes) ?? '#000000';
195
+ const fontSize = toNumber(getVariableByName('card/title/fontSize', modes), 16);
196
+ const fontFamily = getVariableByName('card/title/fontFamily', modes) ?? 'JioType Var';
197
+ const lineHeight = toNumber(getVariableByName('card/title/lineHeight', modes), 18);
198
+ const fontWeight = toFontWeight(getVariableByName('card/title/fontWeight', modes), '900');
160
199
  const textStyle = {
161
200
  color,
162
201
  fontSize,
163
202
  fontFamily,
164
203
  lineHeight,
165
- fontWeight: fontWeight
204
+ fontWeight
166
205
  };
167
206
  return /*#__PURE__*/_jsx(Text, {
168
207
  style: [textStyle, style],
208
+ numberOfLines: numberOfLines > 0 ? numberOfLines : undefined,
169
209
  children: children
170
210
  });
171
211
  }
172
212
 
173
213
  /**
174
- * SupportiveText component for Card content.
175
- * Uses tokens: card/supportive-text/*
214
+ * Supportive text tokens: `card/supportive-text/*`
176
215
  */
177
216
  function SupportText({
178
217
  children,
179
218
  style,
180
- modes: propModes
219
+ modes: propModes,
220
+ numberOfLines
181
221
  }) {
182
222
  const context = useContext(CardContext);
183
- const modes = propModes || context.modes || {};
184
- const color = getVariableByName('card/supportive-text/color', modes);
185
- const fontSize = parseFloat(getVariableByName('card/supportive-text/fontSize', modes));
186
- const fontFamily = getVariableByName('card/supportive-text/fontFamily', modes);
187
- const lineHeight = parseFloat(getVariableByName('card/supportive-text/lineHeight', modes));
188
- const rawFontWeight = getVariableByName('card/supportive-text/fontWeight', modes);
189
- const fontWeight = typeof rawFontWeight === 'number' ? rawFontWeight.toString() : rawFontWeight;
223
+ const modes = propModes ?? context.modes ?? EMPTY_MODES;
224
+ const color = getVariableByName('card/supportive-text/color', modes) ?? '#1a1c1f';
225
+ const fontSize = toNumber(getVariableByName('card/supportive-text/fontSize', modes), 12);
226
+ const fontFamily = getVariableByName('card/supportive-text/fontFamily', modes) ?? 'JioType Var';
227
+ const lineHeight = toNumber(getVariableByName('card/supportive-text/lineHeight', modes), 16);
228
+ const fontWeight = toFontWeight(getVariableByName('card/supportive-text/fontWeight', modes), '400');
190
229
  const textStyle = {
191
230
  color,
192
231
  fontSize,
193
232
  fontFamily,
194
233
  lineHeight,
195
- fontWeight: fontWeight
234
+ fontWeight
196
235
  };
197
236
  return /*#__PURE__*/_jsx(Text, {
198
237
  style: [textStyle, style],
238
+ numberOfLines: numberOfLines,
199
239
  children: children
200
240
  });
201
241
  }
202
-
203
- // Attach sub-components to Card
204
242
  Card.Title = Title;
205
243
  Card.SupportText = SupportText;
206
244
  export default Card;