jfs-components 0.0.77 → 0.0.79

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 (87) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +55 -55
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
  4. package/lib/commonjs/components/Attached/Attached.js +144 -0
  5. package/lib/commonjs/components/Card/Card.js +25 -2
  6. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  7. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  8. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  9. package/lib/commonjs/components/FormField/FormField.js +14 -1
  10. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +353 -0
  11. package/lib/commonjs/components/ListItem/ListItem.js +46 -24
  12. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  13. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  14. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
  15. package/lib/commonjs/components/Slot/Slot.js +73 -0
  16. package/lib/commonjs/components/Stepper/Step.js +47 -60
  17. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  18. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  19. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  20. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  21. package/lib/commonjs/components/Title/Title.js +10 -2
  22. package/lib/commonjs/components/index.js +49 -0
  23. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  24. package/lib/commonjs/icons/registry.js +1 -1
  25. package/lib/module/components/Accordion/Accordion.js +56 -56
  26. package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
  27. package/lib/module/components/Attached/Attached.js +139 -0
  28. package/lib/module/components/Card/Card.js +25 -2
  29. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  30. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  31. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  32. package/lib/module/components/FormField/FormField.js +16 -3
  33. package/lib/module/components/FullscreenModal/FullscreenModal.js +348 -0
  34. package/lib/module/components/ListItem/ListItem.js +46 -24
  35. package/lib/module/components/MessageField/MessageField.js +313 -0
  36. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  37. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
  38. package/lib/module/components/Slot/Slot.js +68 -0
  39. package/lib/module/components/Stepper/Step.js +48 -61
  40. package/lib/module/components/Stepper/StepLabel.js +40 -10
  41. package/lib/module/components/Stepper/Stepper.js +15 -17
  42. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  43. package/lib/module/components/TextInput/TextInput.js +17 -2
  44. package/lib/module/components/Title/Title.js +10 -2
  45. package/lib/module/components/index.js +7 -0
  46. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  47. package/lib/module/icons/registry.js +1 -1
  48. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  49. package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
  50. package/lib/typescript/src/components/Card/Card.d.ts +9 -2
  51. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  52. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  53. package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
  54. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  55. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  56. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
  57. package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
  58. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  59. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  60. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  61. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  62. package/lib/typescript/src/components/index.d.ts +10 -3
  63. package/lib/typescript/src/icons/registry.d.ts +1 -1
  64. package/package.json +1 -1
  65. package/src/components/Accordion/Accordion.tsx +113 -73
  66. package/src/components/ActionFooter/ActionFooter.tsx +56 -4
  67. package/src/components/Attached/Attached.tsx +181 -0
  68. package/src/components/Card/Card.tsx +28 -1
  69. package/src/components/Checkbox/Checkbox.tsx +22 -9
  70. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  71. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  72. package/src/components/FormField/FormField.tsx +19 -3
  73. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  74. package/src/components/ListItem/ListItem.tsx +55 -25
  75. package/src/components/MessageField/MessageField.tsx +543 -0
  76. package/src/components/NavArrow/NavArrow.tsx +81 -17
  77. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
  78. package/src/components/Slot/Slot.tsx +91 -0
  79. package/src/components/Stepper/Step.tsx +52 -51
  80. package/src/components/Stepper/StepLabel.tsx +46 -9
  81. package/src/components/Stepper/Stepper.tsx +20 -15
  82. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  83. package/src/components/TextInput/TextInput.tsx +14 -1
  84. package/src/components/Title/Title.tsx +13 -2
  85. package/src/components/index.ts +10 -3
  86. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  87. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,426 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ Pressable,
6
+ Platform,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ type TextStyle,
10
+ type LayoutChangeEvent,
11
+ } from 'react-native';
12
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
13
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
14
+ import Icon from '../../icons/Icon';
15
+
16
+ /**
17
+ * A single plan column header (the label column has no header of its own).
18
+ */
19
+ export type PlanComparisonColumn = {
20
+ /** Header text for the plan column. */
21
+ label: string;
22
+ /**
23
+ * Render the header in the brand accent colour (gold) — use it to
24
+ * highlight the recommended / upsell plan.
25
+ * @default false
26
+ */
27
+ brand?: boolean;
28
+ };
29
+
30
+ /**
31
+ * Value rendered inside a plan cell.
32
+ * - `string` / `number` → rendered as value text.
33
+ * - `false` → renders the muted "not available" cross icon.
34
+ * - any React node → rendered as-is (e.g. a `Badge`, `MoneyValue`, icon…).
35
+ * - `null` / `undefined` / `true` → empty cell.
36
+ */
37
+ export type PlanComparisonCellValue =
38
+ | string
39
+ | number
40
+ | boolean
41
+ | null
42
+ | undefined
43
+ | React.ReactElement;
44
+
45
+ export type PlanComparisonRow = {
46
+ /** Feature label shown in the first (left) column. */
47
+ label: string;
48
+ /**
49
+ * Show an info icon after the label. When `onInfoPress` is provided the
50
+ * icon becomes tappable; otherwise it is purely decorative.
51
+ */
52
+ showInfo?: boolean;
53
+ /** Handler for the info icon. Implies `showInfo`. */
54
+ onInfoPress?: () => void;
55
+ /**
56
+ * One value per plan column, in the same order as `columns`. See
57
+ * {@link PlanComparisonCellValue} for how each value is rendered.
58
+ */
59
+ values: PlanComparisonCellValue[];
60
+ /** Stable key. Falls back to the label / index. */
61
+ key?: React.Key;
62
+ };
63
+
64
+ export type PlanComparisonCardProps = {
65
+ /**
66
+ * Plan column headers (excludes the leading label column). The order here
67
+ * maps 1:1 to each row's `values` array.
68
+ */
69
+ columns?: PlanComparisonColumn[];
70
+ /** Feature rows compared across the plan columns. */
71
+ rows?: PlanComparisonRow[];
72
+ /**
73
+ * Minimum flex-grow on the label column when the table is given extra
74
+ * horizontal space. Plan columns always size to their content and never
75
+ * shrink below it.
76
+ * @default 0
77
+ */
78
+ labelColumnFlex?: number;
79
+ /** Design token modes for theming (e.g. `{ "Color Mode": "Light" }`). */
80
+ modes?: Record<string, any>;
81
+ /** Override the outer container style. */
82
+ style?: StyleProp<ViewStyle>;
83
+ };
84
+
85
+ const DEFAULT_COLUMNS: PlanComparisonColumn[] = [
86
+ { label: 'Your plan' },
87
+ { label: 'JioFinance+', brand: true },
88
+ ];
89
+
90
+ const DEFAULT_ROWS: PlanComparisonRow[] = [
91
+ { label: 'JioPoints multiplier', values: ['1x', '1.25x'] },
92
+ { label: 'Cashback', showInfo: true, values: [false, 'Upto ₹5000'] },
93
+ { label: 'Bonus JioGold', showInfo: true, values: [false, '1%'] },
94
+ ];
95
+
96
+ /**
97
+ * PlanComparisonCard renders a compact comparison table that pits the user's
98
+ * current plan against one or more alternative plans across a set of feature
99
+ * rows. Implementation of Figma node `4498:2968` (`PlanComparisonCard`).
100
+ *
101
+ * The leading column holds feature labels (with an optional info icon); every
102
+ * other column maps to a plan in `columns`. Each cell value can be plain text,
103
+ * a "not available" cross (`false`), or any custom React node.
104
+ *
105
+ * @component
106
+ * @example
107
+ * ```tsx
108
+ * <PlanComparisonCard
109
+ * columns={[{ label: 'Your plan' }, { label: 'JioFinance+', brand: true }]}
110
+ * rows={[
111
+ * { label: 'JioPoints multiplier', values: ['1x', '1.25x'] },
112
+ * { label: 'Cashback', showInfo: true, values: [false, 'Upto ₹5000'] },
113
+ * ]}
114
+ * />
115
+ * ```
116
+ */
117
+ /** Keeps every text layer on a single line; columns grow to fit content. */
118
+ const NO_WRAP_TEXT: TextStyle = {
119
+ flexShrink: 0,
120
+ ...(Platform.OS === 'web' ? { whiteSpace: 'nowrap' as const } : {}),
121
+ };
122
+
123
+ function PlanComparisonCard({
124
+ columns = DEFAULT_COLUMNS,
125
+ rows = DEFAULT_ROWS,
126
+ labelColumnFlex = 0,
127
+ modes = EMPTY_MODES,
128
+ style,
129
+ }: PlanComparisonCardProps) {
130
+ /** Natural widths from header labels (plan columns only). */
131
+ const [headerWidths, setHeaderWidths] = useState<(number | undefined)[]>([]);
132
+ /** Natural widths from table body columns. */
133
+ const [bodyWidths, setBodyWidths] = useState<(number | undefined)[]>([]);
134
+
135
+ const setMeasuredWidth = useCallback(
136
+ (
137
+ setter: React.Dispatch<React.SetStateAction<(number | undefined)[]>>,
138
+ index: number,
139
+ width: number,
140
+ ) => {
141
+ setter((prev) => {
142
+ if (prev[index] === width) return prev;
143
+ const next = [...prev];
144
+ next[index] = width;
145
+ return next;
146
+ });
147
+ },
148
+ [],
149
+ );
150
+
151
+ const onHeaderColumnLayout = useCallback(
152
+ (index: number, event: LayoutChangeEvent) => {
153
+ setMeasuredWidth(setHeaderWidths, index, event.nativeEvent.layout.width);
154
+ },
155
+ [setMeasuredWidth],
156
+ );
157
+
158
+ const onBodyColumnLayout = useCallback(
159
+ (index: number, event: LayoutChangeEvent) => {
160
+ setMeasuredWidth(setBodyWidths, index, event.nativeEvent.layout.width);
161
+ },
162
+ [setMeasuredWidth],
163
+ );
164
+
165
+ /**
166
+ * Shared width for header + body cells in a column (max of natural header
167
+ * label vs body content). No columnGap between columns — gaps would shift
168
+ * headers relative to the flush table grid below.
169
+ */
170
+ const columnWidthStyle = (index: number): ViewStyle => {
171
+ const width = Math.max(headerWidths[index] ?? 0, bodyWidths[index] ?? 0);
172
+ if (width > 0) {
173
+ return { width, minWidth: width, flexShrink: 0, flexGrow: 0 };
174
+ }
175
+ return { flexShrink: 0, flexGrow: 0 };
176
+ };
177
+
178
+ // Container
179
+ const gap = (getVariableByName('planComparisonCard/gap', modes) as number) ?? 16;
180
+
181
+ // Header
182
+ const headerFg = (getVariableByName('planComparisonCard/header/fg', modes) as string) ?? '#ffffff';
183
+ const headerBrandFg = (getVariableByName('planComparisonCard/header/brand/fg', modes) as string) ?? '#cea15a';
184
+ const headerFontSize = (getVariableByName('planComparisonCard/header/fontSize', modes) as number) ?? 14;
185
+ const headerFontFamily = (getVariableByName('planComparisonCard/header/fontFamily', modes) as string) ?? 'JioType Var';
186
+ const headerLineHeight = (getVariableByName('planComparisonCard/header/lineHeight', modes) as number) ?? 18;
187
+ const headerFontWeight = (getVariableByName('planComparisonCard/header/fontWeight', modes) as number | string) ?? '500';
188
+
189
+ // Table
190
+ const tableBackground = (getVariableByName('planComparisonCard/tableRow/background', modes) as string) ?? '#141414';
191
+ const tableRadius = (getVariableByName('planComparisonCard/tableRow/radius', modes) as number) ?? 16;
192
+ const tableBorderSize = (getVariableByName('planComparisonCard/tableRow/border/size', modes) as number) ?? 1;
193
+ const tableBorderColor = (getVariableByName('planComparisonCard/tableRow/border/color', modes) as string) ?? '#1e1a14';
194
+
195
+ // Cell
196
+ const cellPadding = (getVariableByName('planComparisonCard/tableCell/padding', modes) as number) ?? 12;
197
+ const cellGap = (getVariableByName('planComparisonCard/tableCell/gap', modes) as number) ?? 2;
198
+ const cellMinHeight = (getVariableByName('planComparisonCard/tableCell/height', modes) as number) ?? 46;
199
+ const cellBorderSize = (getVariableByName('planComparisonCard/tableCell/border/size', modes) as number) ?? 1;
200
+ const cellBorderColor = (getVariableByName('planComparisonCard/tableCell/border/color', modes) as string) ?? '#1e1a14';
201
+
202
+ // Cell label
203
+ const labelColor = (getVariableByName('planComparisonCard/tableCell/label/color', modes) as string) ?? '#ffffff';
204
+ const labelDisabledColor = (getVariableByName('planComparisonCard/tableCell/label/disabled/color', modes) as string) ?? '#91949c';
205
+ const labelFontSize = (getVariableByName('planComparisonCard/tableCell/label/fontSize', modes) as number) ?? 12;
206
+ const labelFontFamily = (getVariableByName('planComparisonCard/tableCell/label/fontFamily', modes) as string) ?? 'JioType Var';
207
+ const labelLineHeight = (getVariableByName('planComparisonCard/tableCell/label/lineHeight', modes) as number) ?? 16;
208
+ const labelFontWeight = (getVariableByName('planComparisonCard/tableCell/label/fontWeight', modes) as number | string) ?? '400';
209
+
210
+ // Cell value
211
+ const valueColor = (getVariableByName('planComparisonCard/tableCell/value/color', modes) as string) ?? '#ffffff';
212
+ const valueFontSize = (getVariableByName('planComparisonCard/tableCell/value/fontSize', modes) as number) ?? 12;
213
+ const valueFontFamily = (getVariableByName('planComparisonCard/tableCell/value/fontFamily', modes) as string) ?? 'JioType Var';
214
+ const valueLineHeight = (getVariableByName('planComparisonCard/tableCell/value/lineHeight', modes) as number) ?? 16;
215
+ const valueFontWeight = (getVariableByName('planComparisonCard/tableCell/value/fontWeight', modes) as number | string) ?? '500';
216
+
217
+ // Icon
218
+ const iconColor = (getVariableByName('planComparisonCard/icon/color', modes) as string) ?? '#ffffff';
219
+ const iconSize = (getVariableByName('planComparisonCard/icon/size', modes) as number) ?? 16;
220
+
221
+ const toWeight = (w: number | string) => (typeof w === 'number' ? `${w}` : w) as TextStyle['fontWeight'];
222
+
223
+ const headerTextStyle: TextStyle = {
224
+ ...NO_WRAP_TEXT,
225
+ fontFamily: headerFontFamily,
226
+ fontSize: headerFontSize,
227
+ lineHeight: headerLineHeight,
228
+ fontWeight: toWeight(headerFontWeight),
229
+ textAlign: 'center',
230
+ };
231
+
232
+ const labelTextStyle: TextStyle = {
233
+ ...NO_WRAP_TEXT,
234
+ color: labelColor,
235
+ fontFamily: labelFontFamily,
236
+ fontSize: labelFontSize,
237
+ lineHeight: labelLineHeight,
238
+ fontWeight: toWeight(labelFontWeight),
239
+ };
240
+
241
+ const valueTextStyle: TextStyle = {
242
+ ...NO_WRAP_TEXT,
243
+ color: valueColor,
244
+ fontFamily: valueFontFamily,
245
+ fontSize: valueFontSize,
246
+ lineHeight: valueLineHeight,
247
+ fontWeight: toWeight(valueFontWeight),
248
+ textAlign: 'center',
249
+ };
250
+
251
+ const planHeaderColumnStyle: ViewStyle = {
252
+ alignItems: 'center',
253
+ justifyContent: 'center',
254
+ };
255
+
256
+ const renderValue = (value: PlanComparisonCellValue, cellKey: React.Key) => {
257
+ // "Not available" → muted cross icon.
258
+ if (value === false) {
259
+ return (
260
+ <Icon
261
+ key={cellKey}
262
+ name="ic_close"
263
+ size={iconSize}
264
+ color={labelDisabledColor}
265
+ />
266
+ );
267
+ }
268
+ // Empty cell.
269
+ if (value === null || value === undefined || value === true) {
270
+ return null;
271
+ }
272
+ // Text content.
273
+ if (typeof value === 'string' || typeof value === 'number') {
274
+ return (
275
+ <Text key={cellKey} style={valueTextStyle}>
276
+ {value}
277
+ </Text>
278
+ );
279
+ }
280
+ // Custom node — forward modes so themed children stay in sync.
281
+ return cloneChildrenWithModes(value, modes);
282
+ };
283
+
284
+ const labelCellStyle: ViewStyle = {
285
+ flexDirection: 'row',
286
+ alignItems: 'center',
287
+ gap: cellGap,
288
+ padding: cellPadding,
289
+ minHeight: cellMinHeight,
290
+ flexShrink: 0,
291
+ };
292
+
293
+ const valueCellStyle: ViewStyle = {
294
+ flexDirection: 'row',
295
+ alignItems: 'center',
296
+ justifyContent: 'center',
297
+ padding: cellPadding,
298
+ minHeight: cellMinHeight,
299
+ flexShrink: 0,
300
+ };
301
+
302
+ return (
303
+ <View style={[{ gap, alignSelf: 'flex-start' }, style]}>
304
+ {/* Headers above table — same column grid as body (no columnGap) */}
305
+ <View style={{ flexDirection: 'row', alignItems: 'flex-end' }}>
306
+ <View
307
+ style={[
308
+ columnWidthStyle(0),
309
+ labelColumnFlex > 0 ? { flexGrow: labelColumnFlex } : undefined,
310
+ ]}
311
+ />
312
+ {columns.map((column, index) => {
313
+ const colIndex = index + 1;
314
+ return (
315
+ <View
316
+ key={column.label ?? index}
317
+ onLayout={(e) => onHeaderColumnLayout(colIndex, e)}
318
+ style={[columnWidthStyle(colIndex), planHeaderColumnStyle]}
319
+ >
320
+ <Text
321
+ style={[
322
+ headerTextStyle,
323
+ {
324
+ color: column.brand ? headerBrandFg : headerFg,
325
+ alignSelf: 'center',
326
+ },
327
+ ]}
328
+ >
329
+ {column.label}
330
+ </Text>
331
+ </View>
332
+ );
333
+ })}
334
+ </View>
335
+
336
+ {/* Single rounded table — columns size to their widest cell */}
337
+ <View
338
+ style={{
339
+ flexDirection: 'row',
340
+ alignSelf: 'flex-start',
341
+ backgroundColor: tableBackground,
342
+ borderWidth: tableBorderSize,
343
+ borderColor: tableBorderColor,
344
+ borderRadius: tableRadius,
345
+ overflow: 'hidden',
346
+ }}
347
+ >
348
+ <View
349
+ onLayout={(e) => onBodyColumnLayout(0, e)}
350
+ style={[
351
+ columnWidthStyle(0),
352
+ labelColumnFlex > 0 ? { flexGrow: labelColumnFlex } : undefined,
353
+ ]}
354
+ >
355
+ {rows.map((row, rowIndex) => {
356
+ const isLast = rowIndex === rows.length - 1;
357
+ const showInfo = row.showInfo || row.onInfoPress != null;
358
+ return (
359
+ <View
360
+ key={row.key ?? `${row.label}-${rowIndex}`}
361
+ style={[
362
+ labelCellStyle,
363
+ {
364
+ borderBottomWidth: isLast ? 0 : cellBorderSize,
365
+ borderBottomColor: cellBorderColor,
366
+ },
367
+ ]}
368
+ >
369
+ <Text style={labelTextStyle}>{row.label}</Text>
370
+ {showInfo &&
371
+ (row.onInfoPress ? (
372
+ <Pressable
373
+ onPress={row.onInfoPress}
374
+ accessibilityRole="button"
375
+ accessibilityLabel={`More information about ${row.label}`}
376
+ hitSlop={8}
377
+ >
378
+ <Icon name="ic_info" size={iconSize} color={iconColor} />
379
+ </Pressable>
380
+ ) : (
381
+ <Icon name="ic_info" size={iconSize} color={iconColor} />
382
+ ))}
383
+ </View>
384
+ );
385
+ })}
386
+ </View>
387
+
388
+ {columns.map((column, colIndex) => {
389
+ const colIndexWidth = colIndex + 1;
390
+ return (
391
+ <View
392
+ key={column.label ?? colIndex}
393
+ onLayout={(e) => onBodyColumnLayout(colIndexWidth, e)}
394
+ style={[columnWidthStyle(colIndexWidth), planHeaderColumnStyle]}
395
+ >
396
+ {rows.map((row, rowIndex) => {
397
+ const isLast = rowIndex === rows.length - 1;
398
+ return (
399
+ <View
400
+ key={row.key ?? `${row.label}-${rowIndex}`}
401
+ style={[
402
+ valueCellStyle,
403
+ {
404
+ borderBottomWidth: isLast ? 0 : cellBorderSize,
405
+ borderBottomColor: cellBorderColor,
406
+ },
407
+ ]}
408
+ >
409
+ <View style={{ flexShrink: 0 }}>
410
+ {renderValue(
411
+ row.values?.[colIndex],
412
+ `${rowIndex}-${colIndex}`,
413
+ )}
414
+ </View>
415
+ </View>
416
+ );
417
+ })}
418
+ </View>
419
+ );
420
+ })}
421
+ </View>
422
+ </View>
423
+ );
424
+ }
425
+
426
+ export default PlanComparisonCard;
@@ -0,0 +1,91 @@
1
+ import React, { useMemo } from 'react'
2
+ import { View, type StyleProp, type ViewProps, type ViewStyle } from 'react-native'
3
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
4
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
5
+ import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils'
6
+
7
+ export type SlotLayoutDirection = 'vertical' | 'horizontal'
8
+
9
+ export type SlotProps = ViewProps & {
10
+ /**
11
+ * Content laid out inside the slot. `modes` are cascaded to every child via
12
+ * {@link cloneChildrenWithModes}.
13
+ */
14
+ children?: React.ReactNode
15
+ /**
16
+ * Main-axis direction for slot children. Matches the Figma Slot variant:
17
+ * - `vertical` (default): stacks children in a column
18
+ * - `horizontal`: arranges children in a row
19
+ */
20
+ layoutDirection?: SlotLayoutDirection
21
+ /**
22
+ * Alignment along the cross axis.
23
+ * Defaults to `stretch` for vertical and `flex-start` for horizontal.
24
+ */
25
+ alignCrossAxis?: ViewStyle['alignItems']
26
+ /**
27
+ * Distribution along the main axis (maps to `justifyContent`).
28
+ */
29
+ justifyMainAxis?: ViewStyle['justifyContent']
30
+ /**
31
+ * Mode configuration passed to the token resolver and cascaded to children.
32
+ */
33
+ modes?: Record<string, any>
34
+ style?: StyleProp<ViewStyle>
35
+ }
36
+
37
+ /**
38
+ * Slot — a token-driven layout container for grouped slot content.
39
+ *
40
+ * Use `Slot` instead of a raw `View` when you need a vertical or horizontal
41
+ * stack with design-token gap spacing and automatic `modes` propagation to
42
+ * children. Typical usage is nesting a column of actions inside a
43
+ * direction-locked parent such as `ActionFooter`:
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * <ActionFooter modes={modes}>
48
+ * <Slot layoutDirection="vertical" modes={modes}>
49
+ * <Button label="Continue" modes={primaryModes} />
50
+ * <Disclaimer disclaimer="Terms apply." modes={modes} />
51
+ * </Slot>
52
+ * </ActionFooter>
53
+ * ```
54
+ */
55
+ function Slot({
56
+ children,
57
+ layoutDirection = 'vertical',
58
+ alignCrossAxis,
59
+ justifyMainAxis,
60
+ modes: propModes = EMPTY_MODES,
61
+ style,
62
+ ...rest
63
+ }: SlotProps) {
64
+ const { modes: globalModes } = useTokens()
65
+ const modes = useMemo(() => ({ ...globalModes, ...propModes }), [globalModes, propModes])
66
+
67
+ const { containerStyle, processedChildren } = useMemo(() => {
68
+ const gap = (getVariableByName('slot/gap', modes) ?? 8) as number
69
+ const isHorizontal = layoutDirection === 'horizontal'
70
+
71
+ const container: ViewStyle = {
72
+ flexDirection: isHorizontal ? 'row' : 'column',
73
+ alignItems: alignCrossAxis ?? (isHorizontal ? 'flex-start' : 'stretch'),
74
+ justifyContent: justifyMainAxis ?? (isHorizontal ? 'center' : undefined),
75
+ alignSelf: 'stretch',
76
+ gap,
77
+ }
78
+
79
+ const processed = children ? cloneChildrenWithModes(children, modes) : null
80
+
81
+ return { containerStyle: container, processedChildren: processed }
82
+ }, [children, modes, layoutDirection, alignCrossAxis, justifyMainAxis])
83
+
84
+ return (
85
+ <View style={[containerStyle, style]} {...rest}>
86
+ {processedChildren}
87
+ </View>
88
+ )
89
+ }
90
+
91
+ export default React.memo(Slot)