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.
- package/CHANGELOG.md +28 -0
- package/lib/commonjs/components/Accordion/Accordion.js +55 -55
- package/lib/commonjs/components/ActionFooter/ActionFooter.js +48 -2
- package/lib/commonjs/components/Attached/Attached.js +144 -0
- package/lib/commonjs/components/Card/Card.js +25 -2
- package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
- package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
- package/lib/commonjs/components/FormField/FormField.js +14 -1
- package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +353 -0
- package/lib/commonjs/components/ListItem/ListItem.js +46 -24
- package/lib/commonjs/components/MessageField/MessageField.js +318 -0
- package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
- package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
- package/lib/commonjs/components/Slot/Slot.js +73 -0
- package/lib/commonjs/components/Stepper/Step.js +47 -60
- package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
- package/lib/commonjs/components/Stepper/Stepper.js +15 -17
- package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
- package/lib/commonjs/components/TextInput/TextInput.js +16 -1
- package/lib/commonjs/components/Title/Title.js +10 -2
- package/lib/commonjs/components/index.js +49 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/Accordion/Accordion.js +56 -56
- package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
- package/lib/module/components/Attached/Attached.js +139 -0
- package/lib/module/components/Card/Card.js +25 -2
- package/lib/module/components/Checkbox/Checkbox.js +22 -10
- package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
- package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
- package/lib/module/components/FormField/FormField.js +16 -3
- package/lib/module/components/FullscreenModal/FullscreenModal.js +348 -0
- package/lib/module/components/ListItem/ListItem.js +46 -24
- package/lib/module/components/MessageField/MessageField.js +313 -0
- package/lib/module/components/NavArrow/NavArrow.js +59 -18
- package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
- package/lib/module/components/Slot/Slot.js +68 -0
- package/lib/module/components/Stepper/Step.js +48 -61
- package/lib/module/components/Stepper/StepLabel.js +40 -10
- package/lib/module/components/Stepper/Stepper.js +15 -17
- package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
- package/lib/module/components/TextInput/TextInput.js +17 -2
- package/lib/module/components/Title/Title.js +10 -2
- package/lib/module/components/index.js +7 -0
- package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
- package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
- package/lib/typescript/src/components/Card/Card.d.ts +9 -2
- package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
- package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
- package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
- package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
- package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
- package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
- package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
- package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
- package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
- package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
- package/lib/typescript/src/components/index.d.ts +10 -3
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +113 -73
- package/src/components/ActionFooter/ActionFooter.tsx +56 -4
- package/src/components/Attached/Attached.tsx +181 -0
- package/src/components/Card/Card.tsx +28 -1
- package/src/components/Checkbox/Checkbox.tsx +22 -9
- package/src/components/DropdownInput/DropdownInput.tsx +67 -39
- package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
- package/src/components/FormField/FormField.tsx +19 -3
- package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
- package/src/components/ListItem/ListItem.tsx +55 -25
- package/src/components/MessageField/MessageField.tsx +543 -0
- package/src/components/NavArrow/NavArrow.tsx +81 -17
- package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
- package/src/components/Slot/Slot.tsx +91 -0
- package/src/components/Stepper/Step.tsx +52 -51
- package/src/components/Stepper/StepLabel.tsx +46 -9
- package/src/components/Stepper/Stepper.tsx +20 -15
- package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
- package/src/components/TextInput/TextInput.tsx +14 -1
- package/src/components/Title/Title.tsx +13 -2
- package/src/components/index.ts +10 -3
- package/src/design-tokens/Coin Variables-variables-full.json +1 -1
- 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)
|