jfs-components 0.0.78 → 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 (33) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/commonjs/components/Attached/Attached.js +144 -0
  3. package/lib/commonjs/components/Card/Card.js +25 -2
  4. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +4 -6
  5. package/lib/commonjs/components/ListItem/ListItem.js +22 -15
  6. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
  7. package/lib/commonjs/components/Slot/Slot.js +73 -0
  8. package/lib/commonjs/components/index.js +21 -0
  9. package/lib/commonjs/icons/registry.js +1 -1
  10. package/lib/module/components/Attached/Attached.js +139 -0
  11. package/lib/module/components/Card/Card.js +25 -2
  12. package/lib/module/components/FullscreenModal/FullscreenModal.js +4 -6
  13. package/lib/module/components/ListItem/ListItem.js +22 -15
  14. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
  15. package/lib/module/components/Slot/Slot.js +68 -0
  16. package/lib/module/components/index.js +3 -0
  17. package/lib/module/icons/registry.js +1 -1
  18. package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
  19. package/lib/typescript/src/components/Card/Card.d.ts +9 -2
  20. package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
  21. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
  22. package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
  23. package/lib/typescript/src/components/index.d.ts +3 -0
  24. package/lib/typescript/src/icons/registry.d.ts +1 -1
  25. package/package.json +1 -1
  26. package/src/components/Attached/Attached.tsx +181 -0
  27. package/src/components/Card/Card.tsx +28 -1
  28. package/src/components/FullscreenModal/FullscreenModal.tsx +3 -3
  29. package/src/components/ListItem/ListItem.tsx +35 -16
  30. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
  31. package/src/components/Slot/Slot.tsx +91 -0
  32. package/src/components/index.ts +3 -0
  33. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { View } from 'react-native';
5
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
6
+ import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils';
7
+
8
+ /**
9
+ * Anchor point on the main content where the attached `badge` is centered.
10
+ * Mirrors the nine Figma `position` variants (corners, edge midpoints, center).
11
+ */
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ const ZERO_SIZE = {
14
+ width: 0,
15
+ height: 0
16
+ };
17
+
18
+ /**
19
+ * Fraction (0 | 0.5 | 1) of the main content's width/height at which the badge
20
+ * center should sit, derived from the `position` anchor.
21
+ */
22
+ function resolveAnchorFractions(position) {
23
+ const fx = position.includes('left') ? 0 : position.includes('right') ? 1 : 0.5;
24
+ const fy = position.startsWith('top') ? 0 : position.startsWith('bottom') ? 1 : 0.5;
25
+ return {
26
+ fx,
27
+ fy
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Attached — overlays a small `badge` on top of arbitrary main content,
33
+ * centered on one of nine anchor points (corners, edge midpoints, or center).
34
+ *
35
+ * The badge straddles the chosen anchor regardless of either element's size:
36
+ * both the main content and the badge are measured via `onLayout`, then the
37
+ * badge is absolutely positioned so its center lands exactly on the anchor.
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <Attached position="bottom-right" badge={<InstitutionBadge modes={modes} />} modes={modes}>
42
+ * <IconCapsule iconName="ic_card" modes={modes} />
43
+ * </Attached>
44
+ * ```
45
+ */
46
+ function Attached({
47
+ children,
48
+ badge,
49
+ position = 'bottom-right',
50
+ circular = true,
51
+ modes: propModes = EMPTY_MODES,
52
+ style,
53
+ ...rest
54
+ }) {
55
+ const {
56
+ modes: globalModes
57
+ } = useTokens();
58
+ const modes = useMemo(() => globalModes === EMPTY_MODES && propModes === EMPTY_MODES ? EMPTY_MODES : {
59
+ ...globalModes,
60
+ ...propModes
61
+ }, [globalModes, propModes]);
62
+ const [mainSize, setMainSize] = useState(ZERO_SIZE);
63
+ const [badgeSize, setBadgeSize] = useState(ZERO_SIZE);
64
+ const onMainLayout = useCallback(e => {
65
+ const {
66
+ width,
67
+ height
68
+ } = e.nativeEvent.layout;
69
+ setMainSize(prev => prev.width === width && prev.height === height ? prev : {
70
+ width,
71
+ height
72
+ });
73
+ }, []);
74
+ const onBadgeLayout = useCallback(e => {
75
+ const {
76
+ width,
77
+ height
78
+ } = e.nativeEvent.layout;
79
+ setBadgeSize(prev => prev.width === width && prev.height === height ? prev : {
80
+ width,
81
+ height
82
+ });
83
+ }, []);
84
+ const mainChildren = useMemo(() => children != null ? cloneChildrenWithModes(children, modes) : null, [children, modes]);
85
+ const badgeChildren = useMemo(() => badge != null ? cloneChildrenWithModes(badge, modes) : null, [badge, modes]);
86
+ const badgePlacement = useMemo(() => {
87
+ const {
88
+ fx,
89
+ fy
90
+ } = resolveAnchorFractions(position);
91
+ const measured = mainSize.width > 0 && badgeSize.width > 0;
92
+ let anchorX;
93
+ let anchorY;
94
+ if (circular) {
95
+ // Project the anchor onto the circle inscribed in the bounding box, so
96
+ // corner badges land on the circumference (45°) instead of the box corner.
97
+ const cx = mainSize.width / 2;
98
+ const cy = mainSize.height / 2;
99
+ const radius = Math.min(mainSize.width, mainSize.height) / 2;
100
+ const dx = (fx - 0.5) * 2; // -1 | 0 | 1
101
+ const dy = (fy - 0.5) * 2; // -1 | 0 | 1
102
+ const len = Math.hypot(dx, dy) || 1; // 'center' → 0, guard against /0
103
+ anchorX = cx + dx / len * radius;
104
+ anchorY = cy + dy / len * radius;
105
+ } else {
106
+ anchorX = mainSize.width * fx;
107
+ anchorY = mainSize.height * fy;
108
+ }
109
+ return {
110
+ position: 'absolute',
111
+ left: anchorX - badgeSize.width / 2,
112
+ top: anchorY - badgeSize.height / 2,
113
+ // Hide until both elements are measured to avoid a one-frame flash at (0,0).
114
+ opacity: measured ? 1 : 0
115
+ };
116
+ }, [position, circular, mainSize, badgeSize]);
117
+ return /*#__PURE__*/_jsxs(View, {
118
+ style: [styles.container, style],
119
+ ...rest,
120
+ children: [/*#__PURE__*/_jsx(View, {
121
+ onLayout: onMainLayout,
122
+ children: mainChildren
123
+ }), badgeChildren != null && /*#__PURE__*/_jsx(View, {
124
+ style: badgePlacement,
125
+ onLayout: onBadgeLayout,
126
+ pointerEvents: "box-none",
127
+ children: badgeChildren
128
+ })]
129
+ });
130
+ }
131
+ const styles = {
132
+ // alignSelf flex-start so the wrapper hugs the main content; anchors are then
133
+ // computed relative to the content size rather than a stretched parent.
134
+ container: {
135
+ position: 'relative',
136
+ alignSelf: 'flex-start'
137
+ }
138
+ };
139
+ export default /*#__PURE__*/React.memo(Attached);
@@ -16,9 +16,11 @@ const CardContext = /*#__PURE__*/createContext({});
16
16
  * Card component implementation from Figma node 765:6186.
17
17
  *
18
18
  * Supports a `media` slot (with aspect ratio) and a content area.
19
+ * Supports an optional `header` slot (e.g. a brand logo), a `media` slot
20
+ * (with aspect ratio) and a content area.
19
21
  * Usage:
20
22
  * ```tsx
21
- * <Card media={<Image source={...} />} modes={modes}>
23
+ * <Card header={<GoldLogo />} media={<Image source={...} />} modes={modes}>
22
24
  * <Card.SupportText>Support text</Card.SupportText>
23
25
  * <Card.Title>Title</Card.Title>
24
26
  * <Card.SupportText>Support text</Card.SupportText>
@@ -26,6 +28,7 @@ const CardContext = /*#__PURE__*/createContext({});
26
28
  * ```
27
29
  */
28
30
  export function Card({
31
+ header,
29
32
  media,
30
33
  children,
31
34
  modes = EMPTY_MODES,
@@ -53,6 +56,14 @@ export function Card({
53
56
  ...modes
54
57
  }
55
58
  }) : media;
59
+
60
+ // Clone header to pass modes if it's a valid element
61
+ const headerWithModes = /*#__PURE__*/isValidElement(header) ? /*#__PURE__*/cloneElement(header, {
62
+ modes: {
63
+ ...header.props.modes,
64
+ ...modes
65
+ }
66
+ }) : header;
56
67
  const containerStyle = {
57
68
  backgroundColor,
58
69
  borderColor,
@@ -63,6 +74,15 @@ export function Card({
63
74
  paddingVertical,
64
75
  overflow: 'hidden' // Ensure border radius clips content
65
76
  };
77
+
78
+ // Header wrap uses fixed padding from Figma (no dedicated tokens defined).
79
+ const headerWrapperStyle = {
80
+ width: '100%',
81
+ flexDirection: 'row',
82
+ alignItems: 'flex-start',
83
+ paddingHorizontal: 12,
84
+ paddingVertical: 16
85
+ };
66
86
  const mediaWrapperStyle = {
67
87
  width: '100%',
68
88
  aspectRatio: mediaAspectRatio,
@@ -83,7 +103,10 @@ export function Card({
83
103
  },
84
104
  children: /*#__PURE__*/_jsxs(View, {
85
105
  style: [containerStyle, style],
86
- children: [media && /*#__PURE__*/_jsx(View, {
106
+ children: [header && /*#__PURE__*/_jsx(View, {
107
+ style: headerWrapperStyle,
108
+ children: headerWithModes
109
+ }), media && /*#__PURE__*/_jsx(View, {
87
110
  style: mediaWrapperStyle,
88
111
  children: mediaWithModes
89
112
  }), /*#__PURE__*/_jsx(View, {
@@ -10,6 +10,7 @@ import Button from '../Button/Button';
10
10
  import Disclaimer from '../Disclaimer/Disclaimer';
11
11
  import IconButton from '../IconButton/IconButton';
12
12
  import ActionFooter from '../ActionFooter/ActionFooter';
13
+ import Slot from '../Slot/Slot';
13
14
 
14
15
  // ---------------------------------------------------------------------------
15
16
  // Forced modes
@@ -269,8 +270,9 @@ function FullscreenModal({
269
270
  if (footer) {
270
271
  footerContent = footer;
271
272
  } else if (primaryActionLabel) {
272
- footerContent = /*#__PURE__*/_jsxs(View, {
273
- style: footerColumnStyle,
273
+ footerContent = /*#__PURE__*/_jsxs(Slot, {
274
+ layoutDirection: "vertical",
275
+ modes: modes,
274
276
  children: [/*#__PURE__*/_jsx(Button, {
275
277
  label: primaryActionLabel,
276
278
  modes: modes,
@@ -335,10 +337,6 @@ const scrollViewStyle = {
335
337
  const scrollContentStyle = {
336
338
  flexGrow: 1
337
339
  };
338
- const footerColumnStyle = {
339
- width: '100%',
340
- gap: 8
341
- };
342
340
  const fullWidthStyle = {
343
341
  width: '100%'
344
342
  };
@@ -15,9 +15,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  const IS_IOS = Platform.OS === 'ios';
16
16
  const PRESS_DELAY = IS_IOS ? 130 : 0;
17
17
 
18
- // Forced modes for the endSlot — `Context: 'ListItem'` can never be
19
- // overridden by external modes. Frozen so identity is stable across renders.
20
- const END_SLOT_FORCED_MODES = Object.freeze({
18
+ // Forced modes for the leading/trailing slots — `Context: 'ListItem'` can
19
+ // never be overridden by external modes. Frozen so identity is stable across
20
+ // renders. Applied to both slots so they cascade modes identically.
21
+ const SLOT_FORCED_MODES = Object.freeze({
21
22
  Context: 'ListItem'
22
23
  });
23
24
 
@@ -32,7 +33,7 @@ const pressedOverlayStyle = {
32
33
  // ---------------------------------------------------------------------------
33
34
 
34
35
  function resolveListItemTokens(modes) {
35
- // Modes used to cascade into slot children (leading / supportSlot / endSlot).
36
+ // Modes used to cascade into slot children (leading / supportSlot / trailing).
36
37
  // We do NOT inject an `AppearanceBrand` default here: slot content such as
37
38
  // Buttons or Badges carry their own intended appearance, so forcing one onto
38
39
  // them would be surprising.
@@ -131,9 +132,11 @@ const verticalSupportTextOverride = {
131
132
  * - **design-token driven styling** via `getVariableByName` and `modes`
132
133
  *
133
134
  * Wherever the Figma layer name contains "Slot", this component exposes a
134
- * dedicated React "slot" prop:
135
+ * dedicated React "slot" prop. The leading and trailing edges share a
136
+ * symmetric `leading` / `trailing` slot API:
137
+ * - Slot "leading" → `leading`
135
138
  * - Slot "support text" → `supportSlot`
136
- * - Slot "end" → `endSlot`
139
+ * - Slot "trailing" → `trailing`
137
140
  *
138
141
  * @component
139
142
  * @param {Object} props
@@ -141,9 +144,9 @@ const verticalSupportTextOverride = {
141
144
  * @param {string} [props.title='Title'] - Primary title used in the horizontal layout.
142
145
  * @param {string} [props.supportText='Support Text'] - Support text used in both layouts when `supportSlot` is not provided.
143
146
  * @param {boolean} [props.showSupportText=true] - Toggles rendering of the support text in Horizontal layout.
144
- * @param {React.ReactNode} [props.leading] - Optional leading element. Defaults to `IconCapsule`.
147
+ * @param {React.ReactNode} [props.leading] - Optional leading slot. Defaults to `IconCapsule`.
145
148
  * @param {React.ReactNode} [props.supportSlot] - Optional custom slot used instead of the default support text block.
146
- * @param {React.ReactNode} [props.endSlot] - Optional custom trailing slot (Figma Slot "end").
149
+ * @param {React.ReactNode} [props.trailing] - Optional trailing slot (Figma Slot "trailing"). Horizontal layout only.
147
150
  * @param {boolean} [props.navArrow=true] - Whether to show NavArrow on the far right (Horizontal layout only).
148
151
  * @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens.
149
152
  * @param {Function} [props.onPress] - When provided, the entire item becomes pressable (navigation variant).
@@ -172,6 +175,7 @@ function ListItemImpl({
172
175
  showSupportText = true,
173
176
  leading,
174
177
  supportSlot,
178
+ trailing,
175
179
  endSlot,
176
180
  navArrow = true,
177
181
  modes = EMPTY_MODES,
@@ -209,7 +213,7 @@ function ListItemImpl({
209
213
  // Process leading slot to pass modes to children. Memoized on
210
214
  // (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
211
215
  const leadingElement = useMemo(() => {
212
- const processed = leading ? cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes) : [];
216
+ const processed = leading ? cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes, SLOT_FORCED_MODES) : [];
213
217
  if (processed.length === 0) {
214
218
  return /*#__PURE__*/_jsx(IconCapsule, {
215
219
  modes: tokens.resolvedModes,
@@ -223,11 +227,14 @@ function ListItemImpl({
223
227
  const processed = cloneChildrenWithModes(React.Children.toArray(supportSlot), tokens.resolvedModes);
224
228
  return processed.length === 1 ? processed[0] : processed;
225
229
  }, [supportSlot, tokens.resolvedModes]);
226
- const processedEndSlot = useMemo(() => {
227
- if (!endSlot) return null;
228
- const processed = cloneChildrenWithModes(React.Children.toArray(endSlot), tokens.resolvedModes, END_SLOT_FORCED_MODES);
230
+
231
+ // `trailing` wins; `endSlot` is the deprecated alias kept for back-compat.
232
+ const trailingContent = trailing ?? endSlot;
233
+ const processedTrailing = useMemo(() => {
234
+ if (!trailingContent) return null;
235
+ const processed = cloneChildrenWithModes(React.Children.toArray(trailingContent), tokens.resolvedModes, SLOT_FORCED_MODES);
229
236
  return processed.length === 1 ? processed[0] : processed;
230
- }, [endSlot, tokens.resolvedModes]);
237
+ }, [trailingContent, tokens.resolvedModes]);
231
238
  const renderSupportContent = () => {
232
239
  if (processedSupportSlot) return processedSupportSlot;
233
240
 
@@ -273,9 +280,9 @@ function ListItemImpl({
273
280
  numberOfLines: 1,
274
281
  children: title
275
282
  }), showSupportText && renderSupportContent()]
276
- }), processedEndSlot ? /*#__PURE__*/_jsx(View, {
283
+ }), processedTrailing ? /*#__PURE__*/_jsx(View, {
277
284
  style: tokens.trailingWrapperStyle,
278
- children: processedEndSlot
285
+ children: processedTrailing
279
286
  }) : null, navArrow && /*#__PURE__*/_jsx(NavArrow, {
280
287
  direction: "Forward",
281
288
  modes: tokens.resolvedModes
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+
3
+ import React, { useState, useCallback } from 'react';
4
+ import { View, Text, Pressable, Platform } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
+ import Icon from '../../icons/Icon';
8
+
9
+ /**
10
+ * A single plan column header (the label column has no header of its own).
11
+ */
12
+
13
+ /**
14
+ * Value rendered inside a plan cell.
15
+ * - `string` / `number` → rendered as value text.
16
+ * - `false` → renders the muted "not available" cross icon.
17
+ * - any React node → rendered as-is (e.g. a `Badge`, `MoneyValue`, icon…).
18
+ * - `null` / `undefined` / `true` → empty cell.
19
+ */
20
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
+ const DEFAULT_COLUMNS = [{
22
+ label: 'Your plan'
23
+ }, {
24
+ label: 'JioFinance+',
25
+ brand: true
26
+ }];
27
+ const DEFAULT_ROWS = [{
28
+ label: 'JioPoints multiplier',
29
+ values: ['1x', '1.25x']
30
+ }, {
31
+ label: 'Cashback',
32
+ showInfo: true,
33
+ values: [false, 'Upto ₹5000']
34
+ }, {
35
+ label: 'Bonus JioGold',
36
+ showInfo: true,
37
+ values: [false, '1%']
38
+ }];
39
+
40
+ /**
41
+ * PlanComparisonCard renders a compact comparison table that pits the user's
42
+ * current plan against one or more alternative plans across a set of feature
43
+ * rows. Implementation of Figma node `4498:2968` (`PlanComparisonCard`).
44
+ *
45
+ * The leading column holds feature labels (with an optional info icon); every
46
+ * other column maps to a plan in `columns`. Each cell value can be plain text,
47
+ * a "not available" cross (`false`), or any custom React node.
48
+ *
49
+ * @component
50
+ * @example
51
+ * ```tsx
52
+ * <PlanComparisonCard
53
+ * columns={[{ label: 'Your plan' }, { label: 'JioFinance+', brand: true }]}
54
+ * rows={[
55
+ * { label: 'JioPoints multiplier', values: ['1x', '1.25x'] },
56
+ * { label: 'Cashback', showInfo: true, values: [false, 'Upto ₹5000'] },
57
+ * ]}
58
+ * />
59
+ * ```
60
+ */
61
+ /** Keeps every text layer on a single line; columns grow to fit content. */
62
+ const NO_WRAP_TEXT = {
63
+ flexShrink: 0,
64
+ ...(Platform.OS === 'web' ? {
65
+ whiteSpace: 'nowrap'
66
+ } : {})
67
+ };
68
+ function PlanComparisonCard({
69
+ columns = DEFAULT_COLUMNS,
70
+ rows = DEFAULT_ROWS,
71
+ labelColumnFlex = 0,
72
+ modes = EMPTY_MODES,
73
+ style
74
+ }) {
75
+ /** Natural widths from header labels (plan columns only). */
76
+ const [headerWidths, setHeaderWidths] = useState([]);
77
+ /** Natural widths from table body columns. */
78
+ const [bodyWidths, setBodyWidths] = useState([]);
79
+ const setMeasuredWidth = useCallback((setter, index, width) => {
80
+ setter(prev => {
81
+ if (prev[index] === width) return prev;
82
+ const next = [...prev];
83
+ next[index] = width;
84
+ return next;
85
+ });
86
+ }, []);
87
+ const onHeaderColumnLayout = useCallback((index, event) => {
88
+ setMeasuredWidth(setHeaderWidths, index, event.nativeEvent.layout.width);
89
+ }, [setMeasuredWidth]);
90
+ const onBodyColumnLayout = useCallback((index, event) => {
91
+ setMeasuredWidth(setBodyWidths, index, event.nativeEvent.layout.width);
92
+ }, [setMeasuredWidth]);
93
+
94
+ /**
95
+ * Shared width for header + body cells in a column (max of natural header
96
+ * label vs body content). No columnGap between columns — gaps would shift
97
+ * headers relative to the flush table grid below.
98
+ */
99
+ const columnWidthStyle = index => {
100
+ const width = Math.max(headerWidths[index] ?? 0, bodyWidths[index] ?? 0);
101
+ if (width > 0) {
102
+ return {
103
+ width,
104
+ minWidth: width,
105
+ flexShrink: 0,
106
+ flexGrow: 0
107
+ };
108
+ }
109
+ return {
110
+ flexShrink: 0,
111
+ flexGrow: 0
112
+ };
113
+ };
114
+
115
+ // Container
116
+ const gap = getVariableByName('planComparisonCard/gap', modes) ?? 16;
117
+
118
+ // Header
119
+ const headerFg = getVariableByName('planComparisonCard/header/fg', modes) ?? '#ffffff';
120
+ const headerBrandFg = getVariableByName('planComparisonCard/header/brand/fg', modes) ?? '#cea15a';
121
+ const headerFontSize = getVariableByName('planComparisonCard/header/fontSize', modes) ?? 14;
122
+ const headerFontFamily = getVariableByName('planComparisonCard/header/fontFamily', modes) ?? 'JioType Var';
123
+ const headerLineHeight = getVariableByName('planComparisonCard/header/lineHeight', modes) ?? 18;
124
+ const headerFontWeight = getVariableByName('planComparisonCard/header/fontWeight', modes) ?? '500';
125
+
126
+ // Table
127
+ const tableBackground = getVariableByName('planComparisonCard/tableRow/background', modes) ?? '#141414';
128
+ const tableRadius = getVariableByName('planComparisonCard/tableRow/radius', modes) ?? 16;
129
+ const tableBorderSize = getVariableByName('planComparisonCard/tableRow/border/size', modes) ?? 1;
130
+ const tableBorderColor = getVariableByName('planComparisonCard/tableRow/border/color', modes) ?? '#1e1a14';
131
+
132
+ // Cell
133
+ const cellPadding = getVariableByName('planComparisonCard/tableCell/padding', modes) ?? 12;
134
+ const cellGap = getVariableByName('planComparisonCard/tableCell/gap', modes) ?? 2;
135
+ const cellMinHeight = getVariableByName('planComparisonCard/tableCell/height', modes) ?? 46;
136
+ const cellBorderSize = getVariableByName('planComparisonCard/tableCell/border/size', modes) ?? 1;
137
+ const cellBorderColor = getVariableByName('planComparisonCard/tableCell/border/color', modes) ?? '#1e1a14';
138
+
139
+ // Cell label
140
+ const labelColor = getVariableByName('planComparisonCard/tableCell/label/color', modes) ?? '#ffffff';
141
+ const labelDisabledColor = getVariableByName('planComparisonCard/tableCell/label/disabled/color', modes) ?? '#91949c';
142
+ const labelFontSize = getVariableByName('planComparisonCard/tableCell/label/fontSize', modes) ?? 12;
143
+ const labelFontFamily = getVariableByName('planComparisonCard/tableCell/label/fontFamily', modes) ?? 'JioType Var';
144
+ const labelLineHeight = getVariableByName('planComparisonCard/tableCell/label/lineHeight', modes) ?? 16;
145
+ const labelFontWeight = getVariableByName('planComparisonCard/tableCell/label/fontWeight', modes) ?? '400';
146
+
147
+ // Cell value
148
+ const valueColor = getVariableByName('planComparisonCard/tableCell/value/color', modes) ?? '#ffffff';
149
+ const valueFontSize = getVariableByName('planComparisonCard/tableCell/value/fontSize', modes) ?? 12;
150
+ const valueFontFamily = getVariableByName('planComparisonCard/tableCell/value/fontFamily', modes) ?? 'JioType Var';
151
+ const valueLineHeight = getVariableByName('planComparisonCard/tableCell/value/lineHeight', modes) ?? 16;
152
+ const valueFontWeight = getVariableByName('planComparisonCard/tableCell/value/fontWeight', modes) ?? '500';
153
+
154
+ // Icon
155
+ const iconColor = getVariableByName('planComparisonCard/icon/color', modes) ?? '#ffffff';
156
+ const iconSize = getVariableByName('planComparisonCard/icon/size', modes) ?? 16;
157
+ const toWeight = w => typeof w === 'number' ? `${w}` : w;
158
+ const headerTextStyle = {
159
+ ...NO_WRAP_TEXT,
160
+ fontFamily: headerFontFamily,
161
+ fontSize: headerFontSize,
162
+ lineHeight: headerLineHeight,
163
+ fontWeight: toWeight(headerFontWeight),
164
+ textAlign: 'center'
165
+ };
166
+ const labelTextStyle = {
167
+ ...NO_WRAP_TEXT,
168
+ color: labelColor,
169
+ fontFamily: labelFontFamily,
170
+ fontSize: labelFontSize,
171
+ lineHeight: labelLineHeight,
172
+ fontWeight: toWeight(labelFontWeight)
173
+ };
174
+ const valueTextStyle = {
175
+ ...NO_WRAP_TEXT,
176
+ color: valueColor,
177
+ fontFamily: valueFontFamily,
178
+ fontSize: valueFontSize,
179
+ lineHeight: valueLineHeight,
180
+ fontWeight: toWeight(valueFontWeight),
181
+ textAlign: 'center'
182
+ };
183
+ const planHeaderColumnStyle = {
184
+ alignItems: 'center',
185
+ justifyContent: 'center'
186
+ };
187
+ const renderValue = (value, cellKey) => {
188
+ // "Not available" → muted cross icon.
189
+ if (value === false) {
190
+ return /*#__PURE__*/_jsx(Icon, {
191
+ name: "ic_close",
192
+ size: iconSize,
193
+ color: labelDisabledColor
194
+ }, cellKey);
195
+ }
196
+ // Empty cell.
197
+ if (value === null || value === undefined || value === true) {
198
+ return null;
199
+ }
200
+ // Text content.
201
+ if (typeof value === 'string' || typeof value === 'number') {
202
+ return /*#__PURE__*/_jsx(Text, {
203
+ style: valueTextStyle,
204
+ children: value
205
+ }, cellKey);
206
+ }
207
+ // Custom node — forward modes so themed children stay in sync.
208
+ return cloneChildrenWithModes(value, modes);
209
+ };
210
+ const labelCellStyle = {
211
+ flexDirection: 'row',
212
+ alignItems: 'center',
213
+ gap: cellGap,
214
+ padding: cellPadding,
215
+ minHeight: cellMinHeight,
216
+ flexShrink: 0
217
+ };
218
+ const valueCellStyle = {
219
+ flexDirection: 'row',
220
+ alignItems: 'center',
221
+ justifyContent: 'center',
222
+ padding: cellPadding,
223
+ minHeight: cellMinHeight,
224
+ flexShrink: 0
225
+ };
226
+ return /*#__PURE__*/_jsxs(View, {
227
+ style: [{
228
+ gap,
229
+ alignSelf: 'flex-start'
230
+ }, style],
231
+ children: [/*#__PURE__*/_jsxs(View, {
232
+ style: {
233
+ flexDirection: 'row',
234
+ alignItems: 'flex-end'
235
+ },
236
+ children: [/*#__PURE__*/_jsx(View, {
237
+ style: [columnWidthStyle(0), labelColumnFlex > 0 ? {
238
+ flexGrow: labelColumnFlex
239
+ } : undefined]
240
+ }), columns.map((column, index) => {
241
+ const colIndex = index + 1;
242
+ return /*#__PURE__*/_jsx(View, {
243
+ onLayout: e => onHeaderColumnLayout(colIndex, e),
244
+ style: [columnWidthStyle(colIndex), planHeaderColumnStyle],
245
+ children: /*#__PURE__*/_jsx(Text, {
246
+ style: [headerTextStyle, {
247
+ color: column.brand ? headerBrandFg : headerFg,
248
+ alignSelf: 'center'
249
+ }],
250
+ children: column.label
251
+ })
252
+ }, column.label ?? index);
253
+ })]
254
+ }), /*#__PURE__*/_jsxs(View, {
255
+ style: {
256
+ flexDirection: 'row',
257
+ alignSelf: 'flex-start',
258
+ backgroundColor: tableBackground,
259
+ borderWidth: tableBorderSize,
260
+ borderColor: tableBorderColor,
261
+ borderRadius: tableRadius,
262
+ overflow: 'hidden'
263
+ },
264
+ children: [/*#__PURE__*/_jsx(View, {
265
+ onLayout: e => onBodyColumnLayout(0, e),
266
+ style: [columnWidthStyle(0), labelColumnFlex > 0 ? {
267
+ flexGrow: labelColumnFlex
268
+ } : undefined],
269
+ children: rows.map((row, rowIndex) => {
270
+ const isLast = rowIndex === rows.length - 1;
271
+ const showInfo = row.showInfo || row.onInfoPress != null;
272
+ return /*#__PURE__*/_jsxs(View, {
273
+ style: [labelCellStyle, {
274
+ borderBottomWidth: isLast ? 0 : cellBorderSize,
275
+ borderBottomColor: cellBorderColor
276
+ }],
277
+ children: [/*#__PURE__*/_jsx(Text, {
278
+ style: labelTextStyle,
279
+ children: row.label
280
+ }), showInfo && (row.onInfoPress ? /*#__PURE__*/_jsx(Pressable, {
281
+ onPress: row.onInfoPress,
282
+ accessibilityRole: "button",
283
+ accessibilityLabel: `More information about ${row.label}`,
284
+ hitSlop: 8,
285
+ children: /*#__PURE__*/_jsx(Icon, {
286
+ name: "ic_info",
287
+ size: iconSize,
288
+ color: iconColor
289
+ })
290
+ }) : /*#__PURE__*/_jsx(Icon, {
291
+ name: "ic_info",
292
+ size: iconSize,
293
+ color: iconColor
294
+ }))]
295
+ }, row.key ?? `${row.label}-${rowIndex}`);
296
+ })
297
+ }), columns.map((column, colIndex) => {
298
+ const colIndexWidth = colIndex + 1;
299
+ return /*#__PURE__*/_jsx(View, {
300
+ onLayout: e => onBodyColumnLayout(colIndexWidth, e),
301
+ style: [columnWidthStyle(colIndexWidth), planHeaderColumnStyle],
302
+ children: rows.map((row, rowIndex) => {
303
+ const isLast = rowIndex === rows.length - 1;
304
+ return /*#__PURE__*/_jsx(View, {
305
+ style: [valueCellStyle, {
306
+ borderBottomWidth: isLast ? 0 : cellBorderSize,
307
+ borderBottomColor: cellBorderColor
308
+ }],
309
+ children: /*#__PURE__*/_jsx(View, {
310
+ style: {
311
+ flexShrink: 0
312
+ },
313
+ children: renderValue(row.values?.[colIndex], `${rowIndex}-${colIndex}`)
314
+ })
315
+ }, row.key ?? `${row.label}-${rowIndex}`);
316
+ })
317
+ }, column.label ?? colIndex);
318
+ })]
319
+ })]
320
+ });
321
+ }
322
+ export default PlanComparisonCard;