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.
- package/.cursor/D2C-SCREEN.md +2 -1
- package/.cursor/commands/D2C.md +2 -1
- package/CHANGELOG.md +17 -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/CircularProgressBarDoted/CircularProgressBarDoted.js +1 -1
- 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/CircularProgressBarDoted/CircularProgressBarDoted.js +1 -1
- 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/CircularProgressBarDoted/CircularProgressBarDoted.tsx +1 -1
- 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
|
@@ -9,16 +9,23 @@ import { usePressableWebSupport } from '../../utils/web-platform-utils';
|
|
|
9
9
|
import Badge from '../Badge/Badge';
|
|
10
10
|
import Image from '../Image/Image';
|
|
11
11
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
|
-
|
|
12
|
+
/** Canonical Figma collection that drives idle vs active chrome. */
|
|
13
|
+
const CARD_TAB_STATE = 'Card Tab State';
|
|
14
|
+
/**
|
|
15
|
+
* Legacy collection name used by earlier CategoryCard builds. Still accepted on
|
|
16
|
+
* `modes` so existing callers keep resolving the correct Active/Default tokens.
|
|
17
|
+
*/
|
|
18
|
+
const LEGACY_CARD_TAB_STATE = 'Category Card State';
|
|
13
19
|
const BADGE_OVERLAP = 11;
|
|
14
20
|
const DEFAULT_CARD_WIDTH = 60;
|
|
21
|
+
const DEFAULT_IMAGE_ASSET = require('./assets/sample.png');
|
|
15
22
|
|
|
16
23
|
// Default modes from the Figma CategoryCard component. Overridable via `modes`.
|
|
17
24
|
const CATEGORY_CARD_DEFAULT_MODES = Object.freeze({
|
|
18
25
|
AppearanceBrand: 'Tertiary',
|
|
19
26
|
'Badge Size': 'Small',
|
|
20
27
|
Context4: 'Badge',
|
|
21
|
-
|
|
28
|
+
Radius: 'None'
|
|
22
29
|
});
|
|
23
30
|
const toNumber = (value, fallback) => {
|
|
24
31
|
if (typeof value === 'number') {
|
|
@@ -35,32 +42,54 @@ const toFontWeight = (value, fallback) => {
|
|
|
35
42
|
if (typeof value === 'string') return value;
|
|
36
43
|
return fallback;
|
|
37
44
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Maps any accepted state token (`Default` / `Active`, plus legacy `Idle`) onto
|
|
48
|
+
* the canonical `Card Tab State` mode names used by the variable resolver.
|
|
49
|
+
*/
|
|
50
|
+
const normalizeCardTabState = value => {
|
|
51
|
+
if (value === 'Active') return 'Active';
|
|
52
|
+
if (value === 'Default' || value === 'Idle') return 'Default';
|
|
53
|
+
return undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolves whether the card should render in the Active (gold stroke) state.
|
|
58
|
+
*
|
|
59
|
+
* Precedence (backward compatible):
|
|
60
|
+
* 1. Explicit `active` boolean prop (existing public API)
|
|
61
|
+
* 2. `modes['Card Tab State']` / legacy `modes['Category Card State']`
|
|
62
|
+
* 3. Default → idle (`Default`)
|
|
63
|
+
*/
|
|
64
|
+
const resolveIsActive = (active, propModes) => {
|
|
65
|
+
if (typeof active === 'boolean') return active;
|
|
66
|
+
const fromModes = normalizeCardTabState(propModes[CARD_TAB_STATE] ?? propModes[LEGACY_CARD_TAB_STATE]);
|
|
67
|
+
return fromModes === 'Active';
|
|
68
|
+
};
|
|
69
|
+
function resolveCategoryCardTokens(modes) {
|
|
70
|
+
const gap = toNumber(getVariableByName('cardTab/gap', modes), 6);
|
|
71
|
+
const paddingHorizontal = toNumber(getVariableByName('cardTab/padding/horizontal', modes), 8);
|
|
72
|
+
const paddingVertical = toNumber(getVariableByName('cardTab/padding/vertical', modes), 8);
|
|
73
|
+
const radius = toNumber(getVariableByName('cardTab/radius', modes), 8);
|
|
74
|
+
const strokeWidth = toNumber(getVariableByName('cardTab/strokeWidth', modes), 1);
|
|
75
|
+
// Active → gold (`#cea15a`); Default → grey (`#f5f5f5`). Both resolve through
|
|
76
|
+
// `Card Tab State` on `cardTab/stroke/color`.
|
|
77
|
+
const strokeColor = getVariableByName('cardTab/stroke/color', modes) ?? '#f5f5f5';
|
|
78
|
+
|
|
79
|
+
// Both Idle and Active use a white surface — selection is stroke-only.
|
|
80
|
+
const backgroundColor = getVariableByName('cardTab/background/color', modes) ?? '#ffffff';
|
|
81
|
+
const imageWidth = toNumber(getVariableByName('cardTab/image/width', modes), 36);
|
|
82
|
+
const imageHeight = toNumber(getVariableByName('cardTab/image/height', modes), imageWidth);
|
|
83
|
+
const imageRadius = toNumber(getVariableByName('image/radius', modes), 0);
|
|
84
|
+
|
|
85
|
+
// Prefer component-scoped `cardTab/label/*` (Figma bindings). Do not fall
|
|
86
|
+
// back to bare `label/fontWeight` / `label/lineHeight` — those currently
|
|
87
|
+
// resolve from an unrelated Donut Chart collection (400 / 15.6).
|
|
88
|
+
const fontFamily = getVariableByName('cardTab/label/fontFamily', modes) ?? getVariableByName('label/fontFamily', modes) ?? 'JioType Var';
|
|
89
|
+
const fontSize = toNumber(getVariableByName('cardTab/label/fontSize', modes) ?? getVariableByName('label/fontSize', modes), 12);
|
|
90
|
+
const lineHeight = toNumber(getVariableByName('cardTab/label/lineHeight', modes), 16);
|
|
91
|
+
const fontWeight = toFontWeight(getVariableByName('cardTab/label/fontWeight', modes), '500');
|
|
92
|
+
const labelColor = getVariableByName('cardTab/label/color', modes) ?? '#000000';
|
|
64
93
|
return {
|
|
65
94
|
containerStyle: {
|
|
66
95
|
position: 'relative',
|
|
@@ -84,7 +113,8 @@ function resolveCategoryCardTokens(modes, active) {
|
|
|
84
113
|
lineHeight,
|
|
85
114
|
textAlign: 'center'
|
|
86
115
|
},
|
|
87
|
-
|
|
116
|
+
imageWidth,
|
|
117
|
+
imageHeight,
|
|
88
118
|
imageRadius
|
|
89
119
|
};
|
|
90
120
|
}
|
|
@@ -101,8 +131,13 @@ const disabledOverlayStyle = {
|
|
|
101
131
|
* optionally show an overlapping badge at the top edge. Cards are typically
|
|
102
132
|
* laid out in a horizontal `ScrollView` or `HStack`.
|
|
103
133
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
134
|
+
* Selection is communicated with a gold stroke (`Card Tab State = Active`);
|
|
135
|
+
* the surface stays white in both Idle and Active. All visual values resolve
|
|
136
|
+
* through the `cardTab/*` design tokens with sensible Figma defaults so the
|
|
137
|
+
* card renders correctly out of the box.
|
|
138
|
+
*
|
|
139
|
+
* The image area is a real slot (`imageSlot` or `children`) matching the Figma
|
|
140
|
+
* Image instance — omit both to keep the default `Image`.
|
|
106
141
|
*
|
|
107
142
|
* @component
|
|
108
143
|
* @param {CategoryCardProps} props
|
|
@@ -111,11 +146,12 @@ function CategoryCard({
|
|
|
111
146
|
label = 'Popular',
|
|
112
147
|
imageSource,
|
|
113
148
|
imageSlot,
|
|
149
|
+
children,
|
|
114
150
|
badge,
|
|
115
|
-
active
|
|
151
|
+
active: activeProp,
|
|
116
152
|
width = DEFAULT_CARD_WIDTH,
|
|
117
|
-
disableTruncation,
|
|
118
|
-
singleLine,
|
|
153
|
+
disableTruncation = true,
|
|
154
|
+
singleLine = true,
|
|
119
155
|
onPress,
|
|
120
156
|
disabled = false,
|
|
121
157
|
modes: propModes = EMPTY_MODES,
|
|
@@ -131,16 +167,21 @@ function CategoryCard({
|
|
|
131
167
|
const {
|
|
132
168
|
modes: globalModes
|
|
133
169
|
} = useTokens();
|
|
170
|
+
const active = resolveIsActive(activeProp, propModes);
|
|
171
|
+
const cardTabState = active ? 'Active' : 'Default';
|
|
134
172
|
const modes = useMemo(() => ({
|
|
135
173
|
...globalModes,
|
|
136
174
|
...CATEGORY_CARD_DEFAULT_MODES,
|
|
137
|
-
...propModes
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
[
|
|
142
|
-
}), [
|
|
143
|
-
const tokens = useMemo(() => resolveCategoryCardTokens(
|
|
175
|
+
...propModes,
|
|
176
|
+
// Canonical collection always wins so stroke/background tokens resolve.
|
|
177
|
+
[CARD_TAB_STATE]: cardTabState,
|
|
178
|
+
// Keep the legacy key in sync for any nested consumers still reading it.
|
|
179
|
+
[LEGACY_CARD_TAB_STATE]: active ? 'Active' : 'Idle'
|
|
180
|
+
}), [globalModes, propModes, cardTabState, active]);
|
|
181
|
+
const tokens = useMemo(() => resolveCategoryCardTokens(modes), [modes]);
|
|
182
|
+
|
|
183
|
+
// Same layout merge order as `Text`: consumer `style` first, then layout so
|
|
184
|
+
// single-line / no-truncation overflow always wins.
|
|
144
185
|
const {
|
|
145
186
|
style: labelLayoutStyle,
|
|
146
187
|
...labelTruncation
|
|
@@ -150,30 +191,22 @@ function CategoryCard({
|
|
|
150
191
|
numberOfLines: 1,
|
|
151
192
|
ellipsizeMode: 'tail'
|
|
152
193
|
}), [disableTruncation, singleLine]);
|
|
194
|
+
const slotContent = imageSlot ?? children;
|
|
153
195
|
const processedImageSlot = useMemo(() => {
|
|
154
|
-
if (
|
|
155
|
-
const processed = cloneChildrenWithModes(
|
|
196
|
+
if (slotContent == null) return null;
|
|
197
|
+
const processed = cloneChildrenWithModes(slotContent, modes);
|
|
156
198
|
return processed.length === 1 ? processed[0] : processed;
|
|
157
|
-
}, [
|
|
158
|
-
const imageNode = processedImageSlot ??
|
|
159
|
-
imageSource: imageSource,
|
|
160
|
-
width: tokens.
|
|
161
|
-
height: tokens.
|
|
162
|
-
ratio: 1,
|
|
163
|
-
resizeMode: "cover",
|
|
164
|
-
borderRadius: tokens.imageRadius,
|
|
165
|
-
accessibilityElementsHidden: true,
|
|
166
|
-
importantForAccessibility: "no"
|
|
167
|
-
}) : /*#__PURE__*/_jsx(Image, {
|
|
168
|
-
imageSource: "https://www.figma.com/api/mcp/asset/0ee34871-646f-4736-be06-22fb6c0a4eed",
|
|
169
|
-
width: tokens.imageSize,
|
|
170
|
-
height: tokens.imageSize,
|
|
199
|
+
}, [slotContent, modes]);
|
|
200
|
+
const imageNode = processedImageSlot ?? /*#__PURE__*/_jsx(Image, {
|
|
201
|
+
imageSource: imageSource ?? DEFAULT_IMAGE_ASSET,
|
|
202
|
+
width: tokens.imageWidth,
|
|
203
|
+
height: tokens.imageHeight,
|
|
171
204
|
ratio: 1,
|
|
172
205
|
resizeMode: "cover",
|
|
173
206
|
borderRadius: tokens.imageRadius,
|
|
174
207
|
accessibilityElementsHidden: true,
|
|
175
208
|
importantForAccessibility: "no"
|
|
176
|
-
})
|
|
209
|
+
});
|
|
177
210
|
const a11yLabel = accessibilityLabel ?? (badge ? `${label}, ${badge}` : label);
|
|
178
211
|
const webProps = usePressableWebSupport({
|
|
179
212
|
restProps: rest,
|
|
@@ -187,8 +220,8 @@ function CategoryCard({
|
|
|
187
220
|
pointerEvents: "none",
|
|
188
221
|
children: /*#__PURE__*/_jsx(Badge, {
|
|
189
222
|
label: badge,
|
|
190
|
-
modes:
|
|
191
|
-
style:
|
|
223
|
+
modes: modes,
|
|
224
|
+
style: BADGE_STYLE
|
|
192
225
|
})
|
|
193
226
|
}) : null;
|
|
194
227
|
|
|
@@ -219,7 +252,7 @@ function CategoryCard({
|
|
|
219
252
|
testID: testID,
|
|
220
253
|
...webProps,
|
|
221
254
|
children: [badgeNode, imageNode, /*#__PURE__*/_jsx(Text, {
|
|
222
|
-
style: [tokens.labelStyle, labelWidthStyle,
|
|
255
|
+
style: [tokens.labelStyle, labelWidthStyle, labelStyle, labelLayoutStyle],
|
|
223
256
|
...labelTruncation,
|
|
224
257
|
accessibilityElementsHidden: true,
|
|
225
258
|
importantForAccessibility: "no",
|
|
@@ -236,8 +269,9 @@ const BADGE_ANCHOR = {
|
|
|
236
269
|
justifyContent: 'center',
|
|
237
270
|
zIndex: 1
|
|
238
271
|
};
|
|
239
|
-
const
|
|
240
|
-
alignSelf: 'center'
|
|
272
|
+
const BADGE_STYLE = {
|
|
273
|
+
alignSelf: 'center',
|
|
274
|
+
maxWidth: 38
|
|
241
275
|
};
|
|
242
276
|
|
|
243
277
|
/** Lets the label fill the card width so the one-line ellipsis has a bound. */
|
|
Binary file
|
|
@@ -7,15 +7,30 @@ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
|
7
7
|
import Checkbox from '../Checkbox/Checkbox';
|
|
8
8
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Figma's Checkbox Item label slot lays out `Text` + `Link` (etc.) in a
|
|
11
|
+
* horizontal auto-layout frame with `Slot gap = XS` (2px). Consumers may still
|
|
12
|
+
* override via `modes['Slot gap']`. Applied only when resolving the slot gap —
|
|
13
|
+
* not merged into child `modes` — so nested components are unaffected.
|
|
14
|
+
*/
|
|
15
|
+
const LABEL_SLOT_GAP_DEFAULTS = {
|
|
16
|
+
'Slot gap': 'XS'
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* CheckboxItem composes a `Checkbox`, a label slot and an optional `endSlot` into a
|
|
11
20
|
* single horizontal pressable row. Pressing anywhere on the row (outside of the
|
|
12
|
-
* `endSlot`
|
|
21
|
+
* `endSlot` and nested interactive children such as `Link`) toggles the checkbox,
|
|
22
|
+
* mirroring the typical native form pattern.
|
|
13
23
|
*
|
|
14
24
|
* Use the `control` prop to swap the checkbox between the leading (left, default)
|
|
15
25
|
* and trailing (right) edge of the row. The `endSlot` flips to the opposite edge.
|
|
16
26
|
*
|
|
27
|
+
* The Figma default composition places `Text` + `Link` in the label slot
|
|
28
|
+
* ("I agree" + "Terms & Conditions"). Pass those as `children` — the slot lays
|
|
29
|
+
* multiple children out in a horizontal row with `slot/gap`.
|
|
30
|
+
*
|
|
17
31
|
* Mirrors the Figma "Checkbox Item" component and uses the `checkboxItem/*`
|
|
18
|
-
* design tokens for
|
|
32
|
+
* design tokens for row spacing; string labels still resolve `checkboxItem/label/*`
|
|
33
|
+
* for backward-compatible typography.
|
|
19
34
|
*
|
|
20
35
|
* @component
|
|
21
36
|
* @param {CheckboxItemProps} props
|
|
@@ -24,13 +39,19 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
24
39
|
* ```tsx
|
|
25
40
|
* const [checked, setChecked] = useState(false)
|
|
26
41
|
*
|
|
27
|
-
* //
|
|
42
|
+
* // Figma default composition — Text + Link in the label slot.
|
|
28
43
|
* <CheckboxItem
|
|
29
44
|
* checked={checked}
|
|
30
45
|
* onValueChange={setChecked}
|
|
31
46
|
* control="leading"
|
|
32
47
|
* modes={{ 'Color Mode': 'Light' }}
|
|
33
48
|
* >
|
|
49
|
+
* <Text text="I agree" autolayout="Hug" />
|
|
50
|
+
* <Link text="Terms & Conditions" autolayout="Hug" onPress={openTerms} />
|
|
51
|
+
* </CheckboxItem>
|
|
52
|
+
*
|
|
53
|
+
* // Plain string (still supported).
|
|
54
|
+
* <CheckboxItem checked={checked} onValueChange={setChecked}>
|
|
34
55
|
* Fixed deposit • 0245
|
|
35
56
|
* </CheckboxItem>
|
|
36
57
|
*
|
|
@@ -59,8 +80,8 @@ const CheckboxItem = /*#__PURE__*/forwardRef(function CheckboxItem({
|
|
|
59
80
|
// 2. otherwise the legacy `label` prop, whenever it was passed at all —
|
|
60
81
|
// including an explicit `null`/`false`, which (as before) hides the
|
|
61
82
|
// label entirely.
|
|
62
|
-
// 3. otherwise the Figma placeholder, so the default story
|
|
63
|
-
// something meaningful.
|
|
83
|
+
// 3. otherwise the Figma plain-string placeholder, so the default story
|
|
84
|
+
// still renders something meaningful when no slot content is supplied.
|
|
64
85
|
const hasChildren = children != null && children !== false;
|
|
65
86
|
const slotContent = hasChildren ? children : label !== undefined ? label : 'Fixed deposit • 0245';
|
|
66
87
|
const isTrailing = control === 'trailing';
|
|
@@ -78,6 +99,11 @@ const CheckboxItem = /*#__PURE__*/forwardRef(function CheckboxItem({
|
|
|
78
99
|
const gap = getVariableByName('checkboxItem/gap', modes) ?? 8;
|
|
79
100
|
const paddingHorizontal = getVariableByName('checkboxItem/padding/horizontal', modes) ?? 0;
|
|
80
101
|
const paddingVertical = getVariableByName('checkboxItem/padding/vertical', modes) ?? 0;
|
|
102
|
+
// Label-slot internal gap — Figma uses Slot gap = XS (2). Consumer modes win.
|
|
103
|
+
const labelSlotGap = getVariableByName('slot/gap', {
|
|
104
|
+
...LABEL_SLOT_GAP_DEFAULTS,
|
|
105
|
+
...modes
|
|
106
|
+
}) ?? 2;
|
|
81
107
|
const labelColor = getVariableByName('checkboxItem/foreground', modes) ?? '#1a1c1f';
|
|
82
108
|
const labelFontFamily = getVariableByName('checkboxItem/label/fontFamily', modes) ?? 'JioType Var';
|
|
83
109
|
const labelFontSize = getVariableByName('checkboxItem/label/fontSize', modes) ?? 14;
|
|
@@ -88,10 +114,17 @@ const CheckboxItem = /*#__PURE__*/forwardRef(function CheckboxItem({
|
|
|
88
114
|
flexDirection: 'row',
|
|
89
115
|
alignItems: 'center',
|
|
90
116
|
gap,
|
|
117
|
+
// Trailing matches Figma: space-between pins the checkbox to the far edge
|
|
118
|
+
// while the label (and optional endSlot) stay on the leading side.
|
|
119
|
+
justifyContent: isTrailing ? 'space-between' : 'flex-start',
|
|
91
120
|
paddingHorizontal,
|
|
92
121
|
paddingVertical,
|
|
93
122
|
width: '100%'
|
|
94
123
|
};
|
|
124
|
+
|
|
125
|
+
// String labels keep the historical checkboxItem/label/* typography so
|
|
126
|
+
// existing plain-string call-sites do not change visually. `flex: 1` is
|
|
127
|
+
// retained in both control modes so long labels still wrap inside the row.
|
|
95
128
|
const resolvedLabelStyle = {
|
|
96
129
|
flex: 1,
|
|
97
130
|
minWidth: 0,
|
|
@@ -118,23 +151,39 @@ const CheckboxItem = /*#__PURE__*/forwardRef(function CheckboxItem({
|
|
|
118
151
|
// correctly). An explicit null/false hides the label entirely (legacy
|
|
119
152
|
// behaviour of the `label` prop).
|
|
120
153
|
const isTextSlot = typeof slotContent === 'string' || typeof slotContent === 'number';
|
|
154
|
+
const labelSlotStyle = {
|
|
155
|
+
// Grow to fill remaining row width so long content wraps. Trailing rows
|
|
156
|
+
// also set `justifyContent: 'space-between'` so the checkbox stays pinned
|
|
157
|
+
// to the far edge (Figma Control=Trailing).
|
|
158
|
+
flex: 1,
|
|
159
|
+
minWidth: 0,
|
|
160
|
+
flexDirection: 'row',
|
|
161
|
+
flexWrap: 'wrap',
|
|
162
|
+
alignItems: 'flex-start',
|
|
163
|
+
gap: labelSlotGap
|
|
164
|
+
};
|
|
121
165
|
const labelNode = slotContent == null || slotContent === false ? null : isTextSlot ? /*#__PURE__*/_jsx(Text, {
|
|
122
166
|
style: [resolvedLabelStyle, labelStyle],
|
|
123
167
|
selectable: false,
|
|
124
168
|
children: slotContent
|
|
125
169
|
}) : /*#__PURE__*/_jsx(View, {
|
|
126
|
-
style:
|
|
127
|
-
flex: 1,
|
|
128
|
-
minWidth: 0
|
|
129
|
-
},
|
|
170
|
+
style: labelSlotStyle,
|
|
130
171
|
children: cloneChildrenWithModes(slotContent, modes)
|
|
131
172
|
});
|
|
173
|
+
|
|
174
|
+
// Capture the end-slot responder so presses on nested actions (e.g. Button)
|
|
175
|
+
// never bubble to the row Pressable and toggle the checkbox.
|
|
176
|
+
const suppressRowToggle = useCallback(event => {
|
|
177
|
+
event.stopPropagation?.();
|
|
178
|
+
}, []);
|
|
132
179
|
const endSlotNode = endSlot ? /*#__PURE__*/_jsx(View, {
|
|
133
180
|
style: {
|
|
134
181
|
width: endSlotWidth,
|
|
135
182
|
flexShrink: 0,
|
|
136
183
|
alignItems: 'stretch'
|
|
137
184
|
},
|
|
185
|
+
onStartShouldSetResponder: () => true,
|
|
186
|
+
onTouchEnd: suppressRowToggle,
|
|
138
187
|
children: cloneChildrenWithModes(endSlot, modes)
|
|
139
188
|
}) : null;
|
|
140
189
|
return /*#__PURE__*/_jsx(Pressable, {
|
|
@@ -71,7 +71,7 @@ function CircularProgressBarDoted({
|
|
|
71
71
|
const activeDots = normalizedValue <= 0 ? 0 : Math.ceil(normalizedValue / 100 * resolvedDotCount);
|
|
72
72
|
const dotShadowSize = toNumber(getVariableByName('circularProgressBarDoted/dot/shadow/size', modes), 6);
|
|
73
73
|
const baseDotSize = toNumber(getVariableByName('circularProgressBarDoted/dot/size', modes), 6);
|
|
74
|
-
const dotSize = baseDotSize + dotShadowSize;
|
|
74
|
+
const dotSize = baseDotSize + dotShadowSize * 2;
|
|
75
75
|
const outerDotSize = dotSize;
|
|
76
76
|
const ringSize = layoutSize;
|
|
77
77
|
const ringRadius = Math.max(0, (ringSize - outerDotSize) / 2);
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
-
import { View
|
|
4
|
+
import { View } from 'react-native';
|
|
5
5
|
import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
|
|
6
|
-
import { EMPTY_MODES } from '../../utils/react-utils';
|
|
7
|
-
import
|
|
6
|
+
import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
|
|
7
|
+
import CheckboxItem from '../CheckboxItem/CheckboxItem';
|
|
8
8
|
import Button from '../Button/Button';
|
|
9
|
-
import
|
|
9
|
+
import Text from '../Text/Text';
|
|
10
|
+
import Link from '../Link/Link';
|
|
11
|
+
import ScrollArea from '../ScrollArea/ScrollArea';
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
10
13
|
/**
|
|
11
14
|
* Default modes applied to the inner toggle `Button`. These resolve the
|
|
12
15
|
* tertiary-style pill in the Figma reference (small, transparent background,
|
|
@@ -19,15 +22,21 @@ const BUTTON_DEFAULT_MODES = {
|
|
|
19
22
|
Emphasis: 'Low'
|
|
20
23
|
};
|
|
21
24
|
|
|
25
|
+
/** Figma Idle label-slot itemSpacing. */
|
|
26
|
+
const IDLE_SLOT_GAP = 4;
|
|
27
|
+
/** Figma Open scroll-slot itemSpacing. */
|
|
28
|
+
const OPEN_SLOT_GAP = 2;
|
|
29
|
+
|
|
22
30
|
/**
|
|
23
|
-
* ExpandableCheckbox composes a `
|
|
24
|
-
* "Read more" / "Read less" toggle. Mirrors
|
|
25
|
-
* component with two states:
|
|
31
|
+
* ExpandableCheckbox composes a `CheckboxItem` (checkbox + label slot), optional
|
|
32
|
+
* `Text`/`Link` label content, and a "Read more" / "Read less" toggle. Mirrors
|
|
33
|
+
* the Figma "Expandable Checkbox" component with two states:
|
|
26
34
|
*
|
|
27
|
-
* - **Idle (collapsed)** —
|
|
28
|
-
*
|
|
29
|
-
* - **Open (expanded)** —
|
|
30
|
-
*
|
|
35
|
+
* - **Idle (collapsed)** — horizontal row: CheckboxItem (fill) + toggle button
|
|
36
|
+
* (hug), cross-axis centered.
|
|
37
|
+
* - **Open (expanded)** — vertical stack aligned to the end: CheckboxItem with
|
|
38
|
+
* the label slot wrapped in a `ScrollArea` (capped by `checkbox/maxHeight`),
|
|
39
|
+
* then the toggle button right-aligned beneath.
|
|
31
40
|
*
|
|
32
41
|
* The checkbox and the toggle button have independent press handlers — pressing
|
|
33
42
|
* the toggle does not affect the checked state, and toggling the checkbox does
|
|
@@ -39,7 +48,9 @@ const BUTTON_DEFAULT_MODES = {
|
|
|
39
48
|
* @example
|
|
40
49
|
* ```tsx
|
|
41
50
|
* <ExpandableCheckbox
|
|
42
|
-
* label="
|
|
51
|
+
* label="I agree"
|
|
52
|
+
* linkLabel="Terms & Conditions"
|
|
53
|
+
* onLinkPress={openTerms}
|
|
43
54
|
* defaultChecked
|
|
44
55
|
* onValueChange={setAccepted}
|
|
45
56
|
* modes={{ 'Color Mode': 'Light' }}
|
|
@@ -48,6 +59,9 @@ const BUTTON_DEFAULT_MODES = {
|
|
|
48
59
|
*/
|
|
49
60
|
function ExpandableCheckbox({
|
|
50
61
|
label = '',
|
|
62
|
+
linkLabel,
|
|
63
|
+
onLinkPress,
|
|
64
|
+
children,
|
|
51
65
|
checked: controlledChecked,
|
|
52
66
|
defaultChecked = false,
|
|
53
67
|
onValueChange,
|
|
@@ -81,14 +95,21 @@ function ExpandableCheckbox({
|
|
|
81
95
|
if (!isExpandedControlled) setInternalExpanded(next);
|
|
82
96
|
onExpandedChange?.(next);
|
|
83
97
|
}, [disabled, isExpanded, isExpandedControlled, onExpandedChange]);
|
|
98
|
+
|
|
99
|
+
// Isolate link presses from the CheckboxItem row Pressable so activating the
|
|
100
|
+
// Terms link never toggles the checkbox.
|
|
101
|
+
const handleLinkPress = useCallback(event => {
|
|
102
|
+
if (disabled) return;
|
|
103
|
+
// RN web: stop the parent Pressable from also receiving the click.
|
|
104
|
+
event.stopPropagation?.();
|
|
105
|
+
onLinkPress?.(event);
|
|
106
|
+
}, [disabled, onLinkPress]);
|
|
84
107
|
const gap = getVariableByName('expandableCheckbox/gap', modes) ?? 8;
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const labelFontWeightRaw = getVariableByName('checkboxItem/label/fontWeight', modes) ?? 400;
|
|
91
|
-
const labelFontWeight = String(labelFontWeightRaw);
|
|
108
|
+
const maxHeight = getVariableByName('checkbox/maxHeight', modes) ?? 450;
|
|
109
|
+
const scrollPaddingLeft = getVariableByName('scrollArea/padding/left', modes) ?? 0;
|
|
110
|
+
const scrollPaddingTop = getVariableByName('scrollArea/padding/top', modes) ?? 0;
|
|
111
|
+
const scrollPaddingRight = getVariableByName('scrollArea/padding/right', modes) ?? 0;
|
|
112
|
+
const scrollPaddingBottom = getVariableByName('scrollArea/padding/bottom', modes) ?? 0;
|
|
92
113
|
const containerStyle = useMemo(() => ({
|
|
93
114
|
flexDirection: isExpanded ? 'column' : 'row',
|
|
94
115
|
alignItems: isExpanded ? 'flex-end' : 'center',
|
|
@@ -98,30 +119,26 @@ function ExpandableCheckbox({
|
|
|
98
119
|
opacity: 0.6
|
|
99
120
|
} : null)
|
|
100
121
|
}), [isExpanded, gap, disabled]);
|
|
101
|
-
const
|
|
122
|
+
const itemWrapperStyle = useMemo(() => ({
|
|
102
123
|
flex: isExpanded ? undefined : 1,
|
|
103
124
|
alignSelf: isExpanded ? 'stretch' : 'auto',
|
|
104
125
|
minWidth: 0,
|
|
126
|
+
width: isExpanded ? '100%' : undefined
|
|
127
|
+
}), [isExpanded]);
|
|
128
|
+
const checkboxItemStyle = useMemo(() => ({
|
|
129
|
+
// CheckboxItem hard-codes width: '100%'; clear it in the Idle row so the
|
|
130
|
+
// Read more button can sit beside a flex-growing item.
|
|
131
|
+
width: isExpanded ? '100%' : undefined,
|
|
132
|
+
alignItems: isExpanded ? 'flex-start' : 'center'
|
|
133
|
+
}), [isExpanded]);
|
|
134
|
+
const slotRowStyle = useMemo(() => ({
|
|
105
135
|
flexDirection: 'row',
|
|
136
|
+
flexWrap: 'wrap',
|
|
106
137
|
alignItems: isExpanded ? 'flex-start' : 'center',
|
|
107
|
-
gap:
|
|
108
|
-
}), [isExpanded, rowGap]);
|
|
109
|
-
const resolvedLabelStyle = useMemo(() => ({
|
|
138
|
+
gap: isExpanded ? OPEN_SLOT_GAP : IDLE_SLOT_GAP,
|
|
110
139
|
flex: 1,
|
|
111
|
-
minWidth: 0
|
|
112
|
-
|
|
113
|
-
fontFamily: labelFontFamily,
|
|
114
|
-
fontSize: labelFontSize,
|
|
115
|
-
lineHeight: labelLineHeight,
|
|
116
|
-
fontWeight: labelFontWeight,
|
|
117
|
-
// Android adds asymmetric font padding and top-aligns the glyph inside
|
|
118
|
-
// an inflated line box when `lineHeight` is set. That makes the centered
|
|
119
|
-
// checkbox look like it drops below the text. Disabling the extra
|
|
120
|
-
// padding + centering the glyph keeps the single-line label optically
|
|
121
|
-
// aligned with the checkbox. No-op on iOS / web.
|
|
122
|
-
includeFontPadding: false,
|
|
123
|
-
textAlignVertical: isExpanded ? 'top' : 'center'
|
|
124
|
-
}), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight, isExpanded]);
|
|
140
|
+
minWidth: 0
|
|
141
|
+
}), [isExpanded]);
|
|
125
142
|
|
|
126
143
|
// Layer component modes first (e.g. Color Mode), then button defaults so
|
|
127
144
|
// Secondary / XS / Low always win unless a dedicated override prop is added.
|
|
@@ -129,30 +146,96 @@ function ExpandableCheckbox({
|
|
|
129
146
|
...modes,
|
|
130
147
|
...BUTTON_DEFAULT_MODES
|
|
131
148
|
}), [modes]);
|
|
132
|
-
const a11yLabel = accessibilityLabel ?? (typeof label === 'string' ? label :
|
|
149
|
+
const a11yLabel = accessibilityLabel ?? (typeof label === 'string' && label.length > 0 ? linkLabel ? `${label} ${linkLabel}` : label : linkLabel);
|
|
133
150
|
const buttonLabel = isExpanded ? readLessLabel : readMoreLabel;
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
const shouldTruncate = !disableTruncation && !isExpanded && collapsedLines > 0;
|
|
152
|
+
const hasChildren = children != null && children !== false;
|
|
153
|
+
const composedLabel = hasChildren ? cloneChildrenWithModes(children, modes) : /*#__PURE__*/_jsxs(View, {
|
|
154
|
+
style: slotRowStyle,
|
|
155
|
+
children: [label ? /*#__PURE__*/_jsx(Text, {
|
|
156
|
+
text: label,
|
|
157
|
+
modes: modes,
|
|
158
|
+
textAlign: "Left",
|
|
159
|
+
style: labelStyle,
|
|
160
|
+
...(shouldTruncate ? {
|
|
161
|
+
numberOfLines: collapsedLines
|
|
162
|
+
} : {
|
|
163
|
+
disableTruncation: true
|
|
164
|
+
})
|
|
165
|
+
}) : null, linkLabel ?
|
|
166
|
+
/*#__PURE__*/
|
|
167
|
+
// Capture the touch so the parent CheckboxItem Pressable does not also
|
|
168
|
+
// toggle the checkbox when the Terms link is activated.
|
|
169
|
+
_jsx(View, {
|
|
170
|
+
onStartShouldSetResponder: () => true,
|
|
171
|
+
children: /*#__PURE__*/_jsx(Link, {
|
|
172
|
+
text: linkLabel,
|
|
173
|
+
onPress: handleLinkPress,
|
|
174
|
+
disabled: disabled,
|
|
175
|
+
modes: modes,
|
|
176
|
+
textAlign: "Left"
|
|
177
|
+
// Idle: Fill (Figma). Open: Hug (Figma).
|
|
178
|
+
,
|
|
179
|
+
autolayout: isExpanded || !label ? 'Hug' : 'Fill'
|
|
180
|
+
})
|
|
181
|
+
}) : null]
|
|
182
|
+
});
|
|
183
|
+
const labelSlot = isExpanded ? /*#__PURE__*/_jsx(ScrollArea, {
|
|
184
|
+
direction: "vertical",
|
|
185
|
+
modes: modes,
|
|
186
|
+
showScrollIndicator: false,
|
|
187
|
+
paddingLeft: scrollPaddingLeft,
|
|
188
|
+
paddingTop: scrollPaddingTop,
|
|
189
|
+
paddingRight: scrollPaddingRight,
|
|
190
|
+
paddingBottom: scrollPaddingBottom,
|
|
191
|
+
style: {
|
|
192
|
+
maxHeight,
|
|
193
|
+
width: '100%',
|
|
194
|
+
flexGrow: 1,
|
|
195
|
+
flexShrink: 1
|
|
196
|
+
},
|
|
197
|
+
contentContainerStyle: {
|
|
198
|
+
flexDirection: 'row',
|
|
199
|
+
flexWrap: 'wrap',
|
|
200
|
+
alignItems: 'flex-start',
|
|
201
|
+
gap: OPEN_SLOT_GAP
|
|
202
|
+
},
|
|
203
|
+
children: hasChildren ? composedLabel : /*#__PURE__*/_jsxs(_Fragment, {
|
|
204
|
+
children: [label ? /*#__PURE__*/_jsx(Text, {
|
|
205
|
+
text: label,
|
|
206
|
+
modes: modes,
|
|
207
|
+
textAlign: "Left",
|
|
208
|
+
style: labelStyle,
|
|
209
|
+
disableTruncation: true
|
|
210
|
+
}) : null, linkLabel ? /*#__PURE__*/_jsx(View, {
|
|
211
|
+
onStartShouldSetResponder: () => true,
|
|
212
|
+
children: /*#__PURE__*/_jsx(Link, {
|
|
213
|
+
text: linkLabel,
|
|
214
|
+
onPress: handleLinkPress,
|
|
215
|
+
disabled: disabled,
|
|
216
|
+
modes: modes,
|
|
217
|
+
textAlign: "Left",
|
|
218
|
+
autolayout: "Hug"
|
|
219
|
+
})
|
|
220
|
+
}) : null]
|
|
221
|
+
})
|
|
222
|
+
}) : composedLabel;
|
|
138
223
|
return /*#__PURE__*/_jsxs(View, {
|
|
139
224
|
style: [containerStyle, style],
|
|
140
|
-
children: [/*#__PURE__*/
|
|
141
|
-
style:
|
|
142
|
-
children:
|
|
225
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
226
|
+
style: itemWrapperStyle,
|
|
227
|
+
children: /*#__PURE__*/_jsx(CheckboxItem, {
|
|
143
228
|
checked: isChecked,
|
|
144
229
|
disabled: disabled,
|
|
145
230
|
onValueChange: handleToggleChecked,
|
|
146
231
|
modes: modes,
|
|
232
|
+
control: "leading",
|
|
233
|
+
style: checkboxItemStyle,
|
|
147
234
|
...(a11yLabel !== undefined ? {
|
|
148
235
|
accessibilityLabel: a11yLabel
|
|
149
|
-
} : {})
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
selectable: false,
|
|
153
|
-
...(labelNumberOfLinesProps ?? {}),
|
|
154
|
-
children: label
|
|
155
|
-
})]
|
|
236
|
+
} : {}),
|
|
237
|
+
children: labelSlot
|
|
238
|
+
})
|
|
156
239
|
}), /*#__PURE__*/_jsx(Button, {
|
|
157
240
|
label: buttonLabel,
|
|
158
241
|
onPress: handleToggleExpanded,
|