jfs-components 0.1.51 → 0.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/commonjs/components/ActionFooter/ActionFooter.js +319 -135
  3. package/lib/commonjs/components/Badge/Badge.js +20 -1
  4. package/lib/commonjs/components/Button/Button.js +139 -47
  5. package/lib/commonjs/components/ButtonGroup/ButtonGroup.js +1 -1
  6. package/lib/commonjs/components/Card/Card.js +151 -112
  7. package/lib/commonjs/components/Card/assets/gold-logo-mark.png +0 -0
  8. package/lib/commonjs/components/CategoryCard/CategoryCard.js +97 -63
  9. package/lib/commonjs/components/CategoryCard/assets/sample.png +0 -0
  10. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +59 -10
  11. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +132 -49
  12. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +56 -14
  13. package/lib/commonjs/components/IconCapsule/IconCapsule.js +1 -1
  14. package/lib/commonjs/components/MetricData/MetricData.js +26 -13
  15. package/lib/commonjs/components/MoneyValue/MoneyValue.js +1 -1
  16. package/lib/commonjs/components/Nudge/Nudge.js +1 -1
  17. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +47 -91
  18. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  19. package/lib/commonjs/components/PortfolioHero/PortfolioHero.js +1 -1
  20. package/lib/commonjs/components/Slider/Slider.js +3 -3
  21. package/lib/commonjs/components/TextInput/TextInput.js +26 -8
  22. package/lib/commonjs/components/Tooltip/Tooltip.js +3 -5
  23. package/lib/commonjs/components/index.js +7 -1
  24. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  25. package/lib/commonjs/design-tokens/figma-modes.generated.js +13 -13
  26. package/lib/commonjs/icons/registry.js +1 -1
  27. package/lib/module/components/ActionFooter/ActionFooter.js +322 -138
  28. package/lib/module/components/Badge/Badge.js +20 -1
  29. package/lib/module/components/Button/Button.js +140 -48
  30. package/lib/module/components/ButtonGroup/ButtonGroup.js +1 -1
  31. package/lib/module/components/Card/Card.js +152 -114
  32. package/lib/module/components/Card/assets/gold-logo-mark.png +0 -0
  33. package/lib/module/components/CategoryCard/CategoryCard.js +97 -63
  34. package/lib/module/components/CategoryCard/assets/sample.png +0 -0
  35. package/lib/module/components/CheckboxItem/CheckboxItem.js +59 -10
  36. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +135 -52
  37. package/lib/module/components/HelloJioInput/HelloJioInput.js +56 -14
  38. package/lib/module/components/IconCapsule/IconCapsule.js +1 -1
  39. package/lib/module/components/MetricData/MetricData.js +26 -14
  40. package/lib/module/components/MoneyValue/MoneyValue.js +1 -1
  41. package/lib/module/components/Nudge/Nudge.js +1 -1
  42. package/lib/module/components/PdpCcCard/PdpCcCard.js +48 -92
  43. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +52 -45
  44. package/lib/module/components/PortfolioHero/PortfolioHero.js +1 -1
  45. package/lib/module/components/Slider/Slider.js +3 -3
  46. package/lib/module/components/TextInput/TextInput.js +27 -9
  47. package/lib/module/components/Tooltip/Tooltip.js +3 -5
  48. package/lib/module/components/index.js +1 -1
  49. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  50. package/lib/module/design-tokens/figma-modes.generated.js +13 -13
  51. package/lib/module/icons/registry.js +1 -1
  52. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +50 -21
  53. package/lib/typescript/src/components/Button/Button.d.ts +24 -1
  54. package/lib/typescript/src/components/ButtonGroup/ButtonGroup.d.ts +2 -2
  55. package/lib/typescript/src/components/Card/Card.d.ts +54 -25
  56. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +29 -12
  57. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +27 -9
  58. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +37 -12
  59. package/lib/typescript/src/components/IconCapsule/IconCapsule.d.ts +1 -1
  60. package/lib/typescript/src/components/MetricData/MetricData.d.ts +7 -5
  61. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +16 -6
  62. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +9 -2
  63. package/lib/typescript/src/components/index.d.ts +2 -2
  64. package/lib/typescript/src/icons/registry.d.ts +1 -1
  65. package/package.json +1 -1
  66. package/src/components/ActionFooter/ActionFooter.tsx +375 -141
  67. package/src/components/Badge/Badge.tsx +21 -1
  68. package/src/components/Button/Button.tsx +192 -44
  69. package/src/components/ButtonGroup/ButtonGroup.tsx +2 -2
  70. package/src/components/Card/Card.tsx +370 -212
  71. package/src/components/Card/assets/gold-logo-mark.png +0 -0
  72. package/src/components/CategoryCard/CategoryCard.tsx +130 -84
  73. package/src/components/CategoryCard/assets/sample.png +0 -0
  74. package/src/components/CheckboxItem/CheckboxItem.tsx +72 -12
  75. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +186 -67
  76. package/src/components/HelloJioInput/HelloJioInput.tsx +58 -17
  77. package/src/components/IconCapsule/IconCapsule.tsx +1 -1
  78. package/src/components/MetricData/MetricData.tsx +39 -22
  79. package/src/components/MoneyValue/MoneyValue.tsx +1 -1
  80. package/src/components/Nudge/Nudge.tsx +1 -1
  81. package/src/components/PdpCcCard/PdpCcCard.tsx +41 -92
  82. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +73 -58
  83. package/src/components/PortfolioHero/PortfolioHero.tsx +1 -1
  84. package/src/components/Slider/Slider.tsx +3 -3
  85. package/src/components/TextInput/TextInput.tsx +26 -9
  86. package/src/components/Tooltip/Tooltip.tsx +3 -5
  87. package/src/components/index.ts +9 -2
  88. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  89. package/src/design-tokens/figma-modes.generated.ts +13 -13
  90. package/src/icons/registry.ts +1 -1
@@ -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
- const CATEGORY_CARD_STATE = 'Category Card State';
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
- "Radius": "None"
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
- function resolveCategoryCardTokens(modes, active) {
39
- const idleModes = {
40
- ...modes,
41
- [CATEGORY_CARD_STATE]: 'Idle'
42
- };
43
- const activeModes = {
44
- ...modes,
45
- [CATEGORY_CARD_STATE]: 'Active'
46
- };
47
- const resolvedModes = active ? activeModes : idleModes;
48
- const gap = toNumber(getVariableByName('cardTab/gap', resolvedModes), 6);
49
- const paddingHorizontal = toNumber(getVariableByName('cardTab/padding/horizontal', resolvedModes), 8);
50
- const paddingVertical = toNumber(getVariableByName('cardTab/padding/vertical', resolvedModes), 8);
51
- const radius = toNumber(getVariableByName('cardTab/radius', resolvedModes), 8);
52
- const strokeWidth = toNumber(getVariableByName('cardTab/strokeWidth', resolvedModes), 1);
53
- const strokeColor = getVariableByName('cardTab/stroke/color', resolvedModes) ?? '#f5f5f5';
54
- const idleBackground = getVariableByName('cardTab/background/color', idleModes) ?? '#ffffff';
55
- const activeBackground = getVariableByName('cardTab/background/color', activeModes) ?? '#f5f5f5';
56
- const backgroundColor = active ? activeBackground : idleBackground;
57
- const imageSize = toNumber(getVariableByName('cardTab/image/width', resolvedModes), 36);
58
- const imageRadius = toNumber(getVariableByName('image/radius', resolvedModes), 0);
59
- const fontFamily = getVariableByName('label/fontFamily', resolvedModes) ?? 'JioType Var';
60
- const fontSize = toNumber(getVariableByName('label/fontSize', resolvedModes), 12);
61
- const lineHeight = toNumber(getVariableByName('label/lineHeight', resolvedModes), Math.round(fontSize * 1.3));
62
- const fontWeight = toFontWeight(getVariableByName('label/fontWeight', resolvedModes), '500');
63
- const labelColor = getVariableByName('cardTab/label/color', resolvedModes) ?? '#000000';
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
- imageSize,
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
- * All visual values resolve through the `cardTab/*` design tokens with
105
- * sensible Figma defaults so the card renders correctly out of the box.
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 = false,
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
- }), [globalModes, propModes]);
139
- const resolvedModes = useMemo(() => ({
140
- ...modes,
141
- [CATEGORY_CARD_STATE]: active ? 'Active' : 'Idle'
142
- }), [modes, active]);
143
- const tokens = useMemo(() => resolveCategoryCardTokens(resolvedModes, active), [resolvedModes, active]);
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 (!imageSlot) return null;
155
- const processed = cloneChildrenWithModes(imageSlot, resolvedModes);
196
+ if (slotContent == null) return null;
197
+ const processed = cloneChildrenWithModes(slotContent, modes);
156
198
  return processed.length === 1 ? processed[0] : processed;
157
- }, [imageSlot, resolvedModes]);
158
- const imageNode = processedImageSlot ?? (imageSource !== undefined ? /*#__PURE__*/_jsx(Image, {
159
- imageSource: imageSource,
160
- width: tokens.imageSize,
161
- height: tokens.imageSize,
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: resolvedModes,
191
- style: BADGE_CENTER_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, labelLayoutStyle, labelStyle],
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 BADGE_CENTER_STYLE = {
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. */
@@ -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
- * CheckboxItem composes a `Checkbox`, a label and an optional `endSlot` into a
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`) toggles the checkbox, mirroring the typical native form pattern.
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 typography and spacing.
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
- * // Recommended: pass the label via the children slot.
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 still renders
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, {
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useCallback, useMemo, useState } from 'react';
4
- import { View, Text } from 'react-native';
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 Checkbox from '../Checkbox/Checkbox';
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
+ import CheckboxItem from '../CheckboxItem/CheckboxItem';
8
8
  import Button from '../Button/Button';
9
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 `Checkbox`, a long-form label and a
24
- * "Read more" / "Read less" toggle. Mirrors the Figma "Expandable Checkbox"
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)** — checkbox + truncated label + toggle button arranged
28
- * in a horizontal row (cross-axis centered).
29
- * - **Open (expanded)** — checkbox + full multi-line label, with the toggle
30
- * button right-aligned beneath the row.
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="By checking this box, I (a) acknowledge and (b) agree to the full terms…"
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 rowGap = getVariableByName('checkboxItem/gap', modes) ?? 8;
86
- const labelColor = getVariableByName('checkboxItem/foreground', modes) ?? '#1a1c1f';
87
- const labelFontFamily = getVariableByName('checkboxItem/label/fontFamily', modes) ?? 'JioType Var';
88
- const labelFontSize = getVariableByName('checkboxItem/label/fontSize', modes) ?? 14;
89
- const labelLineHeight = getVariableByName('checkboxItem/label/lineHeight', modes) ?? 19;
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 rowStyle = useMemo(() => ({
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: rowGap
108
- }), [isExpanded, rowGap]);
109
- const resolvedLabelStyle = useMemo(() => ({
138
+ gap: isExpanded ? OPEN_SLOT_GAP : IDLE_SLOT_GAP,
110
139
  flex: 1,
111
- minWidth: 0,
112
- color: labelColor,
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 : undefined);
149
+ const a11yLabel = accessibilityLabel ?? (typeof label === 'string' && label.length > 0 ? linkLabel ? `${label} ${linkLabel}` : label : linkLabel);
133
150
  const buttonLabel = isExpanded ? readLessLabel : readMoreLabel;
134
- const labelNumberOfLinesProps = !disableTruncation && !isExpanded && collapsedLines > 0 ? {
135
- numberOfLines: collapsedLines,
136
- ellipsizeMode: 'tail'
137
- } : null;
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__*/_jsxs(View, {
141
- style: rowStyle,
142
- children: [/*#__PURE__*/_jsx(Checkbox, {
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
- }), /*#__PURE__*/_jsx(Text, {
151
- style: [resolvedLabelStyle, labelStyle],
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,