jfs-components 0.1.51 → 0.1.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +319 -135
- package/lib/commonjs/components/Badge/Badge.js +20 -1
- package/lib/commonjs/components/Button/Button.js +139 -47
- package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
- package/lib/commonjs/components/Card/Card.js +151 -112
- package/lib/commonjs/components/Card/assets/gold-logo-mark.png +0 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +97 -63
- package/lib/commonjs/components/CategoryCard/assets/sample.png +0 -0
- package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +59 -10
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +132 -49
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +56 -14
- package/lib/commonjs/components/IconCapsule/IconCapsule.js +1 -1
- package/lib/commonjs/components/MetricData/MetricData.js +26 -13
- package/lib/commonjs/components/MoneyValue/MoneyValue.js +1 -1
- package/lib/commonjs/components/Nudge/Nudge.js +1 -1
- package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +47 -91
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
- package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +1 -1
- package/lib/commonjs/components/Slider/Slider.js +3 -3
- package/lib/commonjs/components/TextInput/TextInput.js +26 -8
- package/lib/commonjs/components/Tooltip/Tooltip.js +3 -5
- package/lib/commonjs/components/index.js +7 -1
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +13 -13
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/ActionFooter/ActionFooter.js +322 -138
- package/lib/module/components/Badge/Badge.js +20 -1
- package/lib/module/components/Button/Button.js +140 -48
- package/lib/module/components/ButtonGroup/ButtonGroup.js +1 -1
- package/lib/module/components/Card/Card.js +152 -114
- package/lib/module/components/Card/assets/gold-logo-mark.png +0 -0
- package/lib/module/components/CategoryCard/CategoryCard.js +97 -63
- package/lib/module/components/CategoryCard/assets/sample.png +0 -0
- package/lib/module/components/CheckboxItem/CheckboxItem.js +59 -10
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +135 -52
- package/lib/module/components/HelloJioInput/HelloJioInput.js +56 -14
- package/lib/module/components/IconCapsule/IconCapsule.js +1 -1
- package/lib/module/components/MetricData/MetricData.js +26 -14
- package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
- package/lib/module/components/Nudge/Nudge.js +1 -1
- package/lib/module/components/PdpCcCard/PdpCcCard.js +48 -92
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
- package/lib/module/components/PortfolioHero/PortfolioHero.js +1 -1
- package/lib/module/components/Slider/Slider.js +3 -3
- package/lib/module/components/TextInput/TextInput.js +27 -9
- package/lib/module/components/Tooltip/Tooltip.js +3 -5
- package/lib/module/components/index.js +1 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/design-tokens/figma-modes.generated.js +13 -13
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +50 -21
- package/lib/typescript/src/components/Button/Button.d.ts +24 -1
- package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +2 -2
- package/lib/typescript/src/components/Card/Card.d.ts +54 -25
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +29 -12
- package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +27 -9
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +37 -12
- package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +1 -1
- package/lib/typescript/src/components/MetricData/MetricData.d.ts +7 -5
- package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +16 -6
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +9 -2
- package/lib/typescript/src/components/index.d.ts +2 -2
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/ActionFooter/ActionFooter.tsx +375 -141
- package/src/components/Badge/Badge.tsx +21 -1
- package/src/components/Button/Button.tsx +192 -44
- package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
- package/src/components/Card/Card.tsx +370 -212
- package/src/components/Card/assets/gold-logo-mark.png +0 -0
- package/src/components/CategoryCard/CategoryCard.tsx +130 -84
- package/src/components/CategoryCard/assets/sample.png +0 -0
- package/src/components/CheckboxItem/CheckboxItem.tsx +72 -12
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +186 -67
- package/src/components/HelloJioInput/HelloJioInput.tsx +58 -17
- package/src/components/IconCapsule/IconCapsule.tsx +1 -1
- package/src/components/MetricData/MetricData.tsx +39 -22
- package/src/components/MoneyValue/MoneyValue.tsx +1 -1
- package/src/components/Nudge/Nudge.tsx +1 -1
- package/src/components/PdpCcCard/PdpCcCard.tsx +41 -92
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +73 -58
- package/src/components/PortfolioHero/PortfolioHero.tsx +1 -1
- package/src/components/Slider/Slider.tsx +3 -3
- package/src/components/TextInput/TextInput.tsx +26 -9
- package/src/components/Tooltip/Tooltip.tsx +3 -5
- package/src/components/index.ts +9 -2
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- package/src/design-tokens/figma-modes.generated.ts +13 -13
- package/src/icons/registry.ts +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
+
// Glass: transparent host so GlassFill shows through. Fixed: clamp width.
|
|
168
|
+
backgroundColor: isGlass ? 'transparent' : backgroundColor,
|
|
107
169
|
gap: gap,
|
|
108
|
-
|
|
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
|
-
|
|
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:
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
})
|
|
345
|
-
}
|
|
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={{"
|
|
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,
|
|
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
|
|
10
|
-
*
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
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 =
|
|
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
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
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
|
-
//
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
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: [
|
|
104
|
-
style: headerWrapperStyle,
|
|
105
|
-
children:
|
|
106
|
-
}),
|
|
107
|
-
style: mediaWrapperStyle,
|
|
108
|
-
children:
|
|
109
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
110
|
-
style: contentWrapperStyle,
|
|
111
|
-
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:
|
|
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:
|
|
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
|
|
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
|
|
151
|
-
const color = getVariableByName('card/title/color', modes);
|
|
152
|
-
const fontSize =
|
|
153
|
-
const fontFamily = getVariableByName('card/title/fontFamily', modes);
|
|
154
|
-
const lineHeight =
|
|
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
|
|
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
|
-
*
|
|
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
|
|
184
|
-
const color = getVariableByName('card/supportive-text/color', modes);
|
|
185
|
-
const fontSize =
|
|
186
|
-
const fontFamily = getVariableByName('card/supportive-text/fontFamily', modes);
|
|
187
|
-
const lineHeight =
|
|
188
|
-
const
|
|
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
|
|
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;
|
|
Binary file
|